daily: 2021-09-28
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useImperativeHandle, useState } from "react";
|
import React, { useCallback, useImperativeHandle, useState } from "react";
|
||||||
import { Button, Input, Menu, Select } from "antd";
|
import { Button, Input, Menu } from "antd";
|
||||||
import PopoverOnSvg from "../PopoverOnSvg";
|
import PopoverOnSvg from "../PopoverOnSvg";
|
||||||
import Styles from "./index.module.css";
|
import Styles from "./index.module.css";
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ const EditableContent = function (
|
|||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}, [inputValue, options]);
|
}, [initialValue, options, handleConfirm]);
|
||||||
let renderContent, renderPopover;
|
let renderContent, renderPopover;
|
||||||
switch (inputType) {
|
switch (inputType) {
|
||||||
case "select":
|
case "select":
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ function PopoverOnSvg({
|
|||||||
document.removeEventListener("mouseup", handler);
|
document.removeEventListener("mouseup", handler);
|
||||||
document.removeEventListener("keydown", handler);
|
document.removeEventListener("keydown", handler);
|
||||||
};
|
};
|
||||||
}, [innerVisible, autoHide, changeVisibility]);
|
}, [innerVisible, autoHide, changeVisibility, trigger]);
|
||||||
|
|
||||||
if (visible !== undefined && visible !== innerVisible) {
|
if (visible !== undefined && visible !== innerVisible) {
|
||||||
if (initFlagRef.current) {
|
if (initFlagRef.current) {
|
||||||
|
|||||||
+62
-25
@@ -4,6 +4,8 @@ import { observable } from "mobx";
|
|||||||
let globalStore = observable({
|
let globalStore = observable({
|
||||||
canvasWidth: 1024,
|
canvasWidth: 1024,
|
||||||
canvasHeight: 1448,
|
canvasHeight: 1448,
|
||||||
|
defaultFontSize: 16,
|
||||||
|
defaultSubFontSize: 12,
|
||||||
title: "简谱",
|
title: "简谱",
|
||||||
tone: "♭D",
|
tone: "♭D",
|
||||||
marginHorizontal: 64,
|
marginHorizontal: 64,
|
||||||
@@ -11,79 +13,108 @@ let globalStore = observable({
|
|||||||
gapAfterTitle: 16,
|
gapAfterTitle: 16,
|
||||||
gapAfterHeader: 32,
|
gapAfterHeader: 32,
|
||||||
gapBetweenParagraph: 32,
|
gapBetweenParagraph: 32,
|
||||||
|
gapBetweenNotation: 8,
|
||||||
beat: [4, 4],
|
beat: [4, 4],
|
||||||
speed: 75,
|
speed: 75,
|
||||||
authors: ["Haven Mattuy 制谱", "杨瑞光 作词"],
|
authors: ["Haven Mattuy 制谱", "杨瑞光 作词"],
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
{
|
{
|
||||||
|
alignJustify: null,
|
||||||
notations: [
|
notations: [
|
||||||
{
|
{
|
||||||
note: "6",
|
note: "6",
|
||||||
octave: -2,
|
octave: -2,
|
||||||
dash: 1,
|
underline: 1,
|
||||||
dotted: true,
|
dotted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "6",
|
note: "6",
|
||||||
octave: -1,
|
octave: -1,
|
||||||
dash: 0,
|
underline: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "3",
|
note: "3",
|
||||||
octave: 0,
|
octave: 0,
|
||||||
dash: 0,
|
underline: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "3",
|
note: "3",
|
||||||
octave: 0,
|
octave: 0,
|
||||||
dash: 0,
|
underline: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
|
key: "hk",
|
||||||
|
tieTo: "ml",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "│",
|
note: "│",
|
||||||
octave: 0,
|
octave: 0,
|
||||||
dash: 0,
|
underline: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "ml",
|
||||||
|
tieTo: "hk",
|
||||||
note: "1",
|
note: "1",
|
||||||
octave: 0,
|
octave: 0,
|
||||||
dash: 0,
|
underline: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "1",
|
note: "2",
|
||||||
octave: 0,
|
octave: -1,
|
||||||
dash: 1,
|
underline: 2,
|
||||||
|
dotted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
note: "2",
|
||||||
|
octave: 2,
|
||||||
|
underline: 1,
|
||||||
|
breakUnderline: true,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "2",
|
note: "2",
|
||||||
octave: 0,
|
octave: 0,
|
||||||
dash: 1,
|
underline: 0,
|
||||||
dotted: false,
|
dotted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "2",
|
note: "─",
|
||||||
octave: 0,
|
octave: 0,
|
||||||
dash: 0,
|
underline: 0,
|
||||||
dotted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
note: "-",
|
|
||||||
octave: 0,
|
|
||||||
dash: 0,
|
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "│",
|
note: "│",
|
||||||
octave: 0,
|
octave: 0,
|
||||||
dash: 0,
|
underline: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
],
|
].map((obj) => ((obj.key = obj.key || Math.random()), obj)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
notations: [
|
||||||
|
{
|
||||||
|
note: "0",
|
||||||
|
octave: 2,
|
||||||
|
dotted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
note: "4",
|
||||||
|
octave: 1,
|
||||||
|
dotted: false,
|
||||||
|
prefixSups: ["♯"],
|
||||||
|
topDecorators: ["※"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
note: "2",
|
||||||
|
octave: 0,
|
||||||
|
dotted: false,
|
||||||
|
},
|
||||||
|
].map((obj) => ((obj.key = obj.key || Math.random()), obj)),
|
||||||
|
alignJustify: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
notations: [
|
notations: [
|
||||||
@@ -95,17 +126,23 @@ let globalStore = observable({
|
|||||||
{
|
{
|
||||||
note: "2",
|
note: "2",
|
||||||
octave: 0,
|
octave: 0,
|
||||||
dotted: false,
|
dotted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "4",
|
note: "4",
|
||||||
octave: 1,
|
octave: 1,
|
||||||
dotted: true,
|
dotted: true,
|
||||||
sharpFlat: 1,
|
prefixSups: ["♯"],
|
||||||
|
topDecorators: ["※"],
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
|
note: "2",
|
||||||
|
octave: 0,
|
||||||
|
dotted: false,
|
||||||
},
|
},
|
||||||
],
|
].map((obj) => ((obj.key = Math.random()), obj)),
|
||||||
|
},
|
||||||
|
].map((obj) => ((obj.key = Math.random()), obj)),
|
||||||
// TODO: do this
|
// TODO: do this
|
||||||
popoverRefs: {},
|
popoverRefs: {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const Notes = {
|
||||||
|
do: "1",
|
||||||
|
re: "2",
|
||||||
|
mi: "3",
|
||||||
|
fa: "4",
|
||||||
|
sol: "5",
|
||||||
|
la: "6",
|
||||||
|
si: "7",
|
||||||
|
extend: "─",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Notes };
|
||||||
+151
-17
@@ -1,7 +1,10 @@
|
|||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import store from "../store/global";
|
import store from "../store/global";
|
||||||
|
|
||||||
function calcFontData(fontSize) {
|
let canvas, context2D, defaultFontFamily;
|
||||||
|
let measureSvgEl, measureTextEl;
|
||||||
|
|
||||||
|
function _calcFontData(fontSize) {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.style.fontFamily = window.getComputedStyle(document.body).fontFamily;
|
span.style.fontFamily = window.getComputedStyle(document.body).fontFamily;
|
||||||
span.style.fontSize = `${fontSize}px`;
|
span.style.fontSize = `${fontSize}px`;
|
||||||
@@ -17,30 +20,85 @@ function calcFontData(fontSize) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _initializeCanvas(fontSize) {
|
||||||
|
if (!canvas) {
|
||||||
|
canvas = document.createElement("canvas");
|
||||||
|
context2D = canvas.getContext("2d");
|
||||||
|
defaultFontFamily = window.getComputedStyle(document.body).fontFamily;
|
||||||
|
}
|
||||||
|
context2D.font = `${
|
||||||
|
fontSize || store.defaultFontSize
|
||||||
|
}px ${defaultFontFamily}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _calcTextWidth(fontSize, text) {
|
||||||
|
_initializeCanvas(fontSize);
|
||||||
|
return context2D.measureText(text).width;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _calcTextBox(fontSize, text) {
|
||||||
|
if (!measureSvgEl) {
|
||||||
|
measureSvgEl = document.createElementNS(
|
||||||
|
"http://www.w3.org/2000/svg",
|
||||||
|
"svg"
|
||||||
|
);
|
||||||
|
measureTextEl = document.createElementNS(
|
||||||
|
"http://www.w3.org/2000/svg",
|
||||||
|
"text"
|
||||||
|
);
|
||||||
|
measureTextEl.style.visibility = "hidden";
|
||||||
|
measureSvgEl.appendChild(measureTextEl);
|
||||||
|
document.body.appendChild(measureSvgEl);
|
||||||
|
}
|
||||||
|
measureTextEl.style.fontSize = fontSize + "px";
|
||||||
|
measureTextEl.textContent = text;
|
||||||
|
return measureTextEl.getBBox();
|
||||||
|
}
|
||||||
|
|
||||||
const placement = observable({
|
const placement = observable({
|
||||||
get commonLineHeight() {
|
get maxContentWidth() {
|
||||||
return this.xHeight;
|
return Math.max(store.canvasWidth - store.marginHorizontal * 2, 0);
|
||||||
|
},
|
||||||
|
get underlineOffsetY() {
|
||||||
|
return 3;
|
||||||
|
},
|
||||||
|
get octaveStepOffsetY() {
|
||||||
|
return 5;
|
||||||
|
},
|
||||||
|
get octaveInitialOffsetAbove() {
|
||||||
|
return -(placement.xHeight / 2 + 2);
|
||||||
|
},
|
||||||
|
get octaveInitialOffsetBelow() {
|
||||||
|
return placement.xHeight / 2 - 2;
|
||||||
},
|
},
|
||||||
get titleOffsetY() {
|
get titleOffsetY() {
|
||||||
const titleHeight = 32;
|
const titleHeight = 32;
|
||||||
return store.marginTop + titleHeight + store.gapAfterTitle;
|
return store.marginTop + titleHeight + store.gapAfterTitle;
|
||||||
},
|
},
|
||||||
get headerOffsetY() {
|
get headerOffsetY() {
|
||||||
const leftInfoBlockHeight = this.commonLineHeight * 2;
|
const leftInfoBlockHeight = this.xHeight * 2;
|
||||||
const titleOffset = this.titleOffsetY;
|
const titleOffset = this.titleOffsetY;
|
||||||
const leftInfoBlockOffset = titleOffset + leftInfoBlockHeight;
|
const leftInfoBlockOffset = titleOffset + leftInfoBlockHeight;
|
||||||
const rightInfoBlockOffset =
|
const rightInfoBlockOffset =
|
||||||
titleOffset + this.commonLineHeight * store.authors.length;
|
titleOffset + this.xHeight * store.authors.length;
|
||||||
return (
|
return (
|
||||||
Math.max(titleOffset, leftInfoBlockOffset, rightInfoBlockOffset) +
|
Math.max(titleOffset, leftInfoBlockOffset, rightInfoBlockOffset) +
|
||||||
store.gapAfterHeader
|
store.gapAfterHeader
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
get baseFontData() {
|
get baseFontData() {
|
||||||
return calcFontData(16);
|
const data = _calcTextBox(store.defaultFontSize, "x");
|
||||||
|
return {
|
||||||
|
xWidth: data.width,
|
||||||
|
xHeight: data.height,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
get baseSubFontData() {
|
get baseSubFontData() {
|
||||||
return calcFontData(12);
|
const data = _calcTextBox(store.defaultSubFontSize, "x");
|
||||||
|
return {
|
||||||
|
xWidth: data.width,
|
||||||
|
xHeight: data.height,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
get xHeight() {
|
get xHeight() {
|
||||||
return this.baseFontData.xHeight;
|
return this.baseFontData.xHeight;
|
||||||
@@ -56,24 +114,100 @@ const placement = observable({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const calcTextWidth = _calcTextWidth.bind(null, store.defaultFontSize);
|
||||||
|
const calcSubTextWidth = _calcTextWidth.bind(null, store.defaultSubFontSize);
|
||||||
|
|
||||||
|
// 计算段落行高
|
||||||
function calcParagraphHeight(paragraph) {
|
function calcParagraphHeight(paragraph) {
|
||||||
|
// TODO: 连音符高度
|
||||||
const maxNoteHeight = Math.max(
|
const maxNoteHeight = Math.max(
|
||||||
...paragraph.notations.map((n) => calcNotationHeight(n))
|
...paragraph.notations.map((n) => calcNotationHeight(n))
|
||||||
);
|
);
|
||||||
return maxNoteHeight + store.gapBetweenParagraph;
|
return maxNoteHeight + store.gapBetweenParagraph;
|
||||||
}
|
}
|
||||||
function calcNotationWidth(notation) {
|
|
||||||
const noteWidth = 24;
|
function calcParagraphWidth(paragraph) {
|
||||||
const dotWidth = 8;
|
const notations = paragraph.notations || [];
|
||||||
return noteWidth + (notation.dotted ? dotWidth : 0);
|
if (notations.length === 0) {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
notations
|
||||||
|
.map((n) => calcNotationWidth(n))
|
||||||
|
.reduce((prev, curr) => prev + curr, 0) - store.gapBetweenNotation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算音符中心位置左边的宽度
|
||||||
|
function calcNotationPrefixOffset(notation) {
|
||||||
|
const prefixes = notation.prefixSups || [];
|
||||||
|
return calcSubTextWidth(prefixes.join("")) + prefixes.length * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算音符中心位置以上的高度
|
||||||
|
function calcNotationAboveOffset(notation) {
|
||||||
|
const offsets = [0];
|
||||||
|
const noteOffsetTop = placement.xHeight / 2;
|
||||||
|
offsets.push(noteOffsetTop);
|
||||||
|
let octaveOffset = 0;
|
||||||
|
if (notation.octave > 0) {
|
||||||
|
octaveOffset = Math.abs(
|
||||||
|
placement.octaveInitialOffsetAbove -
|
||||||
|
placement.octaveStepOffsetY * notation.octave
|
||||||
|
);
|
||||||
|
}
|
||||||
|
offsets.push(octaveOffset);
|
||||||
|
const topDecoratorOffset =
|
||||||
|
octaveOffset +
|
||||||
|
placement.subXHeight * (notation.topDecorators?.length | 0) -
|
||||||
|
2;
|
||||||
|
offsets.push(topDecoratorOffset);
|
||||||
|
return Math.max(...offsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算音符总体宽度
|
||||||
|
function calcNotationWidth(notation) {
|
||||||
|
const noteWidth = calcTextWidth(notation.note) + store.gapBetweenNotation;
|
||||||
|
const dotWidth = 8;
|
||||||
|
const prefixWidth = calcNotationPrefixOffset(notation);
|
||||||
|
return prefixWidth + noteWidth + (notation.dotted ? dotWidth : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算音符自身高度,包含其装饰符,八度点等,不包括连音符
|
||||||
function calcNotationHeight(notation) {
|
function calcNotationHeight(notation) {
|
||||||
const noteHeight = 20;
|
// 以音符中心为基准线,分别计算正方向和负方向的最大偏移,二者之和为音符自身高度
|
||||||
const oc = Math.abs(notation.octave) | 0;
|
// 0保证取正方向偏移最值时不会取到负值,负方向亦然
|
||||||
const octaveHeight = oc ? 2 + (oc - 1) * 5 : 0;
|
const offsets = [0];
|
||||||
return noteHeight + octaveHeight;
|
let noteOffsetTop = placement.xHeight / 2,
|
||||||
|
noteOffsetBottom = -placement.xHeight / 2,
|
||||||
|
octaveOffsetY,
|
||||||
|
supOffsetY;
|
||||||
|
offsets.push(noteOffsetTop, noteOffsetBottom);
|
||||||
|
const oc = notation.octave | 0;
|
||||||
|
if (oc > 0) {
|
||||||
|
octaveOffsetY = placement.octaveInitialOffsetAbove + 5 * oc;
|
||||||
|
} else if (oc < 0) {
|
||||||
|
octaveOffsetY = placement.octaveInitialOffsetBelow + 5 * oc;
|
||||||
|
} else {
|
||||||
|
octaveOffsetY = 0;
|
||||||
|
}
|
||||||
|
offsets.push(octaveOffsetY);
|
||||||
|
// 前置上标的top偏移,其定位基准为字符中心,y位置为(-P.subXHeight + 4),再
|
||||||
|
// 加上基准以上的半个字符的高度,得到前置上标的最大纵向偏移
|
||||||
|
supOffsetY = notation.prefixSups?.length ? placement.subXHeight - 4 : 0;
|
||||||
|
offsets.push(supOffsetY);
|
||||||
|
const maxOffsetY = Math.max(...offsets);
|
||||||
|
const minOffsetY = Math.min(...offsets);
|
||||||
|
return maxOffsetY - minOffsetY;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default placement;
|
export default placement;
|
||||||
|
export {
|
||||||
export { calcParagraphHeight, calcNotationWidth };
|
calcNotationWidth,
|
||||||
|
calcNotationPrefixOffset,
|
||||||
|
calcNotationAboveOffset,
|
||||||
|
calcParagraphWidth,
|
||||||
|
calcParagraphHeight,
|
||||||
|
calcTextWidth,
|
||||||
|
calcSubTextWidth,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import SubMenu from "antd/lib/menu/SubMenu";
|
import SubMenu from "antd/lib/menu/SubMenu";
|
||||||
import { Button, Dropdown, Menu, Tabs } from "antd";
|
import { Button, Dropdown, Menu } from "antd";
|
||||||
import {
|
import {
|
||||||
DownOutlined,
|
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FileOutlined,
|
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
FolderOpenOutlined,
|
FolderOpenOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
ProfileOutlined,
|
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
SettingOutlined,
|
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import store from "../../store/global";
|
import store from "../../store/global";
|
||||||
@@ -28,7 +24,6 @@ function Editor() {
|
|||||||
const p = store.paragraphs[i];
|
const p = store.paragraphs[i];
|
||||||
heightCache[i] = heightCache[i] || calcParagraphHeight(p);
|
heightCache[i] = heightCache[i] || calcParagraphHeight(p);
|
||||||
height += heightCache[i];
|
height += heightCache[i];
|
||||||
console.log(store.paragraphs[i], heightCache[i]);
|
|
||||||
}
|
}
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
@@ -84,7 +79,18 @@ function Editor() {
|
|||||||
<Header />
|
<Header />
|
||||||
<Row offsetX={store.marginHorizontal} offsetY={P.headerOffsetY}>
|
<Row offsetX={store.marginHorizontal} offsetY={P.headerOffsetY}>
|
||||||
{store.paragraphs.map((p, i) => {
|
{store.paragraphs.map((p, i) => {
|
||||||
return <Paragraph key={p} paragraph={p} offsetY={accumulate(i)} />;
|
let alignJustify = p.alignJustify;
|
||||||
|
if (typeof alignJustify !== "boolean") {
|
||||||
|
alignJustify = !(i === store.paragraphs.length - 1);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Paragraph
|
||||||
|
key={p.key}
|
||||||
|
paragraph={p}
|
||||||
|
offsetY={accumulate(i)}
|
||||||
|
alignJustify={alignJustify}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</Row>
|
</Row>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ function LeftInfoBlock() {
|
|||||||
>
|
>
|
||||||
<Text>1 = </Text>
|
<Text>1 = </Text>
|
||||||
{store.tone.startsWith("♭") && (
|
{store.tone.startsWith("♭") && (
|
||||||
<Text x="27" y="-2" fontSize="12">
|
<Text x="27" y="-2" fontSize={store.defaultSubFontSize}>
|
||||||
♭
|
♭
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -89,7 +89,7 @@ function LeftInfoBlock() {
|
|||||||
<Text x="0" y="12" textAnchor="middle">
|
<Text x="0" y="12" textAnchor="middle">
|
||||||
{store.beat[1]}
|
{store.beat[1]}
|
||||||
</Text>
|
</Text>
|
||||||
<line x1="-8" y1="8" x2="8" y2="8" stroke="black" />
|
<line x1="-8" y1="8" x2="8" y2="8" stroke="currentColor" />
|
||||||
</Row>
|
</Row>
|
||||||
</EditableContent>
|
</EditableContent>
|
||||||
<EditableContent
|
<EditableContent
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons";
|
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
import { Modal } from "antd";
|
import { Modal } from "antd";
|
||||||
import { action } from "mobx";
|
import { action } from "mobx";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import EditableContent from "../../../component/EditableContent";
|
import EditableContent from "../../../component/EditableContent";
|
||||||
import store from "../../../store/global";
|
|
||||||
import P from "../../../util/placement";
|
import P from "../../../util/placement";
|
||||||
|
import store from "../../../store/global";
|
||||||
import Row from "../../Row";
|
import Row from "../../Row";
|
||||||
import Text from "../../Text";
|
import Text from "../../Text";
|
||||||
|
|
||||||
@@ -66,8 +66,8 @@ const handleChangeAuthor = action((index, value) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function RightInfoBlock() {
|
function RightInfoBlock() {
|
||||||
return (
|
|
||||||
// TODO: working
|
// TODO: working
|
||||||
|
return (
|
||||||
<Row
|
<Row
|
||||||
type="authors"
|
type="authors"
|
||||||
offsetX={store.canvasWidth - store.marginHorizontal}
|
offsetX={store.canvasWidth - store.marginHorizontal}
|
||||||
@@ -79,7 +79,6 @@ function RightInfoBlock() {
|
|||||||
popoverProps={{ trigger: "context" }}
|
popoverProps={{ trigger: "context" }}
|
||||||
onChange={handleSelectBlockMenu}
|
onChange={handleSelectBlockMenu}
|
||||||
>
|
>
|
||||||
// TODO: 添加新增指引
|
|
||||||
<rect
|
<rect
|
||||||
x={store.marginHorizontal - 150}
|
x={store.marginHorizontal - 150}
|
||||||
y="-64"
|
y="-64"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function Title() {
|
|||||||
editable
|
editable
|
||||||
x="50%"
|
x="50%"
|
||||||
fontSize="32"
|
fontSize="32"
|
||||||
fill="black"
|
fill="currentColor"
|
||||||
stroke="none"
|
stroke="none"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
>
|
>
|
||||||
|
|||||||
+81
-19
@@ -1,4 +1,10 @@
|
|||||||
import P, { calcParagraphHeight } from "../../util/placement";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import store from "../../store/global";
|
||||||
|
import P, {
|
||||||
|
calcNotationPrefixOffset,
|
||||||
|
calcSubTextWidth,
|
||||||
|
} from "../../util/placement";
|
||||||
|
import { Notes } from "../../util/note";
|
||||||
import Row from "../Row";
|
import Row from "../Row";
|
||||||
import Text from "../Text";
|
import Text from "../Text";
|
||||||
|
|
||||||
@@ -10,38 +16,94 @@ function composeArray(octave) {
|
|||||||
return Array(len).fill(0, 0, len);
|
return Array(len).fill(0, 0, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Notation({ offsetX, notation }) {
|
function Notation({ offsetX, notation }) {
|
||||||
|
const underlineOffset = P.underlineOffsetY * (notation.underline | 0);
|
||||||
const octaveInitialOffset =
|
const octaveInitialOffset =
|
||||||
notation.octave > 0 ? -(P.xHeight / 2 + 2) : P.xHeight / 2 - 2;
|
notation.octave > 0
|
||||||
const octaveStepOffset = notation.octave > 0 ? -5 : 5;
|
? P.octaveInitialOffsetAbove
|
||||||
return (
|
: P.octaveInitialOffsetBelow + underlineOffset;
|
||||||
<Row type="notation" offsetX={offsetX}>
|
const octaveStepOffset = (notation.octave > 0 ? -1 : 1) * P.octaveStepOffsetY;
|
||||||
{notation.sharpFlat && (
|
let topDecoratorOffset = 0;
|
||||||
|
if (notation.octave > 0) {
|
||||||
|
// 如果有高八度圆点,将顶部装饰符渲染到其上方
|
||||||
|
topDecoratorOffset =
|
||||||
|
octaveInitialOffset - octaveStepOffset * Math.abs(notation.octave);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPrefixSups = () => {
|
||||||
|
const sups = notation.prefixSups || [];
|
||||||
|
const rendered = sups.map((s, i) => (
|
||||||
<Text
|
<Text
|
||||||
|
key={i + s}
|
||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fontSize="12"
|
fontSize={store.defaultSubFontSize}
|
||||||
y={-P.subXHeight / 2 + 4}
|
x={-P.xWidth - (calcSubTextWidth(sups.slice(0, i).join("")) + 2 * i)}
|
||||||
|
y={-P.subXHeight / 2 + 2}
|
||||||
>
|
>
|
||||||
{notation.sharpFlat > 0 ? "#" : "♭"}
|
{s}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
));
|
||||||
<Row offsetX={notation.sharpFlat ? P.subXWidth + 2 : 0}>
|
return rendered;
|
||||||
<Text dominantBaseline="middle" textAnchor="middle">
|
};
|
||||||
|
|
||||||
|
const renderTopDecorators = () => {
|
||||||
|
const decs = notation.topDecorators || [];
|
||||||
|
const rendered = decs.map((d, i) => (
|
||||||
|
<Text
|
||||||
|
key={i + d}
|
||||||
|
dominantBaseline="middle"
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize={store.defaultSubFontSize}
|
||||||
|
y={topDecoratorOffset - (i + 1) * P.subXHeight + 2}
|
||||||
|
>
|
||||||
|
{d}
|
||||||
|
</Text>
|
||||||
|
));
|
||||||
|
return rendered;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNote = () => {
|
||||||
|
let transform;
|
||||||
|
if (notation.note === Notes.extend) {
|
||||||
|
// 延音符太长了缩短一点
|
||||||
|
transform = "scale(0.8, 1)";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text dominantBaseline="middle" textAnchor="middle" transform={transform}>
|
||||||
{notation.note}
|
{notation.note}
|
||||||
</Text>
|
</Text>
|
||||||
{notation.dotted && (
|
);
|
||||||
<circle type="dot" cx={P.xWidth + 4} cy="0" r="2"></circle>
|
};
|
||||||
)}
|
|
||||||
{composeArray(notation.octave).map((oc, i) => (
|
const renderOctave = () => {
|
||||||
|
return composeArray(notation.octave).map((oc, i) => (
|
||||||
<circle
|
<circle
|
||||||
|
key={i}
|
||||||
type="octave"
|
type="octave"
|
||||||
cx="0"
|
cx="0"
|
||||||
cy={octaveStepOffset * i + octaveInitialOffset}
|
cy={octaveStepOffset * i + octaveInitialOffset}
|
||||||
r="2"
|
r="2"
|
||||||
></circle>
|
></circle>
|
||||||
))}
|
));
|
||||||
</Row>
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row type="notation" offsetX={offsetX}>
|
||||||
|
{renderPrefixSups()}
|
||||||
|
{renderTopDecorators()}
|
||||||
|
{renderNote()}
|
||||||
|
{notation.dotted && (
|
||||||
|
<circle type="dot" cx={P.xWidth + 4} cy="-2" r="2"></circle>
|
||||||
|
)}
|
||||||
|
{renderOctave()}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Notation.defaultProps = {
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(Notation);
|
||||||
|
|||||||
+142
-7
@@ -1,24 +1,159 @@
|
|||||||
import P, { calcNotationWidth } from "../../util/placement";
|
import { useMemo } from "react";
|
||||||
|
import P, {
|
||||||
|
calcNotationAboveOffset,
|
||||||
|
calcNotationPrefixOffset,
|
||||||
|
calcNotationWidth,
|
||||||
|
calcParagraphHeight,
|
||||||
|
calcParagraphWidth,
|
||||||
|
} from "../../util/placement";
|
||||||
import Notation from "../Notation";
|
import Notation from "../Notation";
|
||||||
import Row from "../Row";
|
import Row from "../Row";
|
||||||
|
|
||||||
function Paragraph({ paragraph, ...props }) {
|
function Paragraph({ paragraph, offsetY, alignJustify }) {
|
||||||
|
console.log("render paragraph");
|
||||||
|
const notations = paragraph.notations || [];
|
||||||
const widthCache = [];
|
const widthCache = [];
|
||||||
function accumulate(index) {
|
const paraOffsetY = Math.max(
|
||||||
|
...paragraph.notations.map((n) => calcNotationAboveOffset(n))
|
||||||
|
);
|
||||||
|
let itemFlexOffset = 0;
|
||||||
|
if (alignJustify && paragraph.notations?.length > 1) {
|
||||||
|
const realWidth = calcParagraphWidth(paragraph);
|
||||||
|
itemFlexOffset =
|
||||||
|
(P.maxContentWidth - realWidth) / (paragraph.notations.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算前n个音符的宽度
|
||||||
|
const accumulate = (index) => {
|
||||||
let width = 0;
|
let width = 0;
|
||||||
for (let i = 0; i < index; i++) {
|
for (let i = 0; i < index; i++) {
|
||||||
const n = paragraph.notations[i];
|
const n = notations[i];
|
||||||
widthCache[i] = widthCache[i] || calcNotationWidth(n);
|
widthCache[i] = widthCache[i] || calcNotationWidth(n);
|
||||||
width += widthCache[i];
|
width += widthCache[i];
|
||||||
}
|
}
|
||||||
return width;
|
return width + calcNotationPrefixOffset(notations[index]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const noteOffsets = notations.map((_, i) => {
|
||||||
|
return (
|
||||||
|
// 渲染音符时坐标为其中心,故偏移半个音符宽
|
||||||
|
P.xWidth / 2 +
|
||||||
|
// 偏移前面音符的宽度
|
||||||
|
accumulate(i) +
|
||||||
|
// 两端对齐时自动偏移
|
||||||
|
itemFlexOffset * i
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 绘制音符的增减时线
|
||||||
|
const renderUnderLine = (offsetY, fromIndex, toIndex) => {
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
key={`${fromIndex}_${toIndex}_${offsetY}`}
|
||||||
|
x1={noteOffsets[fromIndex] - P.xWidth}
|
||||||
|
x2={noteOffsets[toIndex] + P.xWidth}
|
||||||
|
y1={offsetY}
|
||||||
|
y2={offsetY}
|
||||||
|
stroke="currentColor"
|
||||||
|
></line>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const renderUnderlines = () => {
|
||||||
|
const lines = [];
|
||||||
|
// 记录音符当前需要绘制的增减时线的条数
|
||||||
|
let helpMap = notations.map((n) => (n.underline > 0 ? n.underline : 0));
|
||||||
|
let baseOffsetY = P.xHeight / 2 - P.underlineOffsetY;
|
||||||
|
while (helpMap.some((n) => n > 0)) {
|
||||||
|
let fromIndex = -1;
|
||||||
|
let toIndex = fromIndex;
|
||||||
|
for (let i = 0; i < notations.length; ++i) {
|
||||||
|
const n = notations[i];
|
||||||
|
if (helpMap[i] > 0) {
|
||||||
|
// 当前音符需要添加增减时线
|
||||||
|
if (fromIndex >= 0) {
|
||||||
|
// 其前面的音符也需要添加增减时线
|
||||||
|
if (n.breakUnderline) {
|
||||||
|
// 当前音符禁止和前面的增减时线连续,绘制前面的增减时线
|
||||||
|
lines.push(renderUnderLine(baseOffsetY, fromIndex, toIndex));
|
||||||
|
// 记录当前音符位置等待绘制
|
||||||
|
fromIndex = toIndex = i;
|
||||||
|
} else {
|
||||||
|
// 当前音符可能和下一个音符的增减时线连续,本次循环不绘制
|
||||||
|
toIndex = i;
|
||||||
}
|
}
|
||||||
|
if (i === notations.length - 1) {
|
||||||
|
// 已是最后一个音符,直接绘制增减时线
|
||||||
|
lines.push(renderUnderLine(baseOffsetY, fromIndex, toIndex));
|
||||||
|
fromIndex = toIndex = -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fromIndex = toIndex = i;
|
||||||
|
}
|
||||||
|
--helpMap[i];
|
||||||
|
} else {
|
||||||
|
// 当前音符不需要绘制增减时线
|
||||||
|
if (fromIndex >= 0) {
|
||||||
|
lines.push(renderUnderLine(baseOffsetY, fromIndex, toIndex));
|
||||||
|
fromIndex = toIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
baseOffsetY += P.underlineOffsetY;
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染连音线
|
||||||
|
const renderTie = (offsetY, fromIndex, toIndex) => {
|
||||||
|
const bezierX1 = 0; //noteOffsets[fromIndex];
|
||||||
|
const bezierX2 = bezierX1 + 4;
|
||||||
|
const bezierX4 = 32// noteOffsets[toIndex];
|
||||||
|
const bezierX3 = bezierX4 - 4;
|
||||||
|
const bezierY = offsetY - 12;
|
||||||
|
console.log(bezierX4 - bezierX1, bezierY - offsetY);
|
||||||
|
return (
|
||||||
|
<path
|
||||||
|
d={`M${bezierX1} ${offsetY} C${bezierX2} ${bezierY} ${bezierX3} ${bezierY} ${bezierX4} ${offsetY}`}
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
></path>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const renderTies = () => {
|
||||||
|
const ties = [];
|
||||||
|
const offsetMap = new Map();
|
||||||
|
for (let i = 0; i < notations.length; ++i) {
|
||||||
|
const n = notations[i];
|
||||||
|
if (n.tieTo) {
|
||||||
|
const toIndex = notations.findIndex((nn) => nn.key === n.tieTo);
|
||||||
|
if (toIndex <= i) {
|
||||||
|
// tieTo属性应该是双向的,仅遍历到第一个节点时渲染
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let minOffsetY = 0;
|
||||||
|
for (let j = i; j <= toIndex; ++j) {
|
||||||
|
let offset;
|
||||||
|
if (offsetMap.has(notations[j].key)) {
|
||||||
|
offset = offsetMap.get(notations[j].key);
|
||||||
|
} else {
|
||||||
|
offset = -calcNotationAboveOffset(notations[j]);
|
||||||
|
offsetMap.set(notations[j].key, offset);
|
||||||
|
}
|
||||||
|
minOffsetY = Math.min(minOffsetY, offset);
|
||||||
|
}
|
||||||
|
ties.push(renderTie(minOffsetY, i, toIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ties;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row type="paragraph" {...props}>
|
<Row type="paragraph" offsetY={offsetY + paraOffsetY}>
|
||||||
|
{renderTies()}
|
||||||
{paragraph.notations.map((n, i) => (
|
{paragraph.notations.map((n, i) => (
|
||||||
<Notation key={n} notation={n} offsetX={accumulate(i)} />
|
<Notation key={n.key} notation={n} offsetX={noteOffsets[i]} />
|
||||||
))}
|
))}
|
||||||
|
{renderUnderlines()}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user