daily: 2021-09-29
This commit is contained in:
+16
-15
@@ -12,7 +12,7 @@ let globalStore = observable({
|
|||||||
marginTop: 32,
|
marginTop: 32,
|
||||||
gapAfterTitle: 16,
|
gapAfterTitle: 16,
|
||||||
gapAfterHeader: 32,
|
gapAfterHeader: 32,
|
||||||
gapBetweenParagraph: 32,
|
gapBetweenParagraph: 0,
|
||||||
gapBetweenNotation: 8,
|
gapBetweenNotation: 8,
|
||||||
beat: [4, 4],
|
beat: [4, 4],
|
||||||
speed: 75,
|
speed: 75,
|
||||||
@@ -24,13 +24,13 @@ let globalStore = observable({
|
|||||||
{
|
{
|
||||||
note: "6",
|
note: "6",
|
||||||
octave: -2,
|
octave: -2,
|
||||||
underline: 1,
|
underline: 2,
|
||||||
dotted: true,
|
dotted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "6",
|
note: "6",
|
||||||
octave: -1,
|
octave: -1,
|
||||||
underline: 0,
|
underline: 1,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,7 +45,7 @@ let globalStore = observable({
|
|||||||
underline: 0,
|
underline: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
key: "hk",
|
key: "hk",
|
||||||
tieTo: "ml",
|
// tieTo: "ml",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "│",
|
note: "│",
|
||||||
@@ -55,7 +55,7 @@ let globalStore = observable({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "ml",
|
key: "ml",
|
||||||
tieTo: "hk",
|
// tieTo: "hk",
|
||||||
note: "1",
|
note: "1",
|
||||||
octave: 0,
|
octave: 0,
|
||||||
underline: 0,
|
underline: 0,
|
||||||
@@ -63,14 +63,14 @@ let globalStore = observable({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "2",
|
note: "2",
|
||||||
octave: -1,
|
// octave: -1,
|
||||||
underline: 2,
|
// underline: 2,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "2",
|
note: "2",
|
||||||
octave: 2,
|
octave: 2,
|
||||||
underline: 1,
|
// underline: 1,
|
||||||
breakUnderline: true,
|
breakUnderline: true,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
@@ -98,15 +98,15 @@ let globalStore = observable({
|
|||||||
notations: [
|
notations: [
|
||||||
{
|
{
|
||||||
note: "0",
|
note: "0",
|
||||||
octave: 2,
|
octave: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "4",
|
note: "4",
|
||||||
octave: 1,
|
octave: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
prefixSups: ["♯"],
|
// prefixSups: ["♯"],
|
||||||
topDecorators: ["※"],
|
// topDecorators: ["※", "※", "※"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
note: "2",
|
note: "2",
|
||||||
@@ -143,9 +143,10 @@ let globalStore = observable({
|
|||||||
].map((obj) => ((obj.key = Math.random()), obj)),
|
].map((obj) => ((obj.key = Math.random()), obj)),
|
||||||
},
|
},
|
||||||
].map((obj) => ((obj.key = Math.random()), obj)),
|
].map((obj) => ((obj.key = Math.random()), obj)),
|
||||||
// TODO: do this
|
|
||||||
popoverRefs: {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const GlobalContext = React.createContext(globalStore);
|
||||||
|
|
||||||
|
|
||||||
export default globalStore;
|
export default globalStore;
|
||||||
export const GlobalContext = React.createContext(globalStore);
|
export { GlobalContext };
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { action, intercept, runInAction, toJS } from "mobx";
|
||||||
|
import globalStore from "./global";
|
||||||
|
|
||||||
|
const storeHistory = [];
|
||||||
|
let cursor = -1;
|
||||||
|
|
||||||
|
intercept(globalStore, (change) => {
|
||||||
|
storeHistory.push(toJS(globalStore));
|
||||||
|
return change;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在历史记录中漫游
|
||||||
|
function go(span) {
|
||||||
|
const newCursor = Math.min(
|
||||||
|
Math.max(0, cursor + span),
|
||||||
|
storeHistory.length - 1
|
||||||
|
);
|
||||||
|
if (newCursor === cursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor = newCursor;
|
||||||
|
Object.assign(globalStore, storeHistory[newCursor]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包装mobx的action以实现历史记录
|
||||||
|
function executeAction(actionFunc, ...args) {
|
||||||
|
storeHistory[++cursor] = toJS(globalStore);
|
||||||
|
storeHistory.length = cursor + 1;
|
||||||
|
if (storeHistory.length > 256) {
|
||||||
|
storeHistory.splice(0, 256 - storeHistory.length);
|
||||||
|
}
|
||||||
|
actionFunc.apply(this, args);
|
||||||
|
}
|
||||||
|
function wrappedAction(...args) {
|
||||||
|
let name, func;
|
||||||
|
if (typeof args[0] === "function") {
|
||||||
|
func = args[0];
|
||||||
|
} else {
|
||||||
|
name = args[0];
|
||||||
|
func = args[1];
|
||||||
|
}
|
||||||
|
const realArgs = [name, executeAction.bind(this, func)];
|
||||||
|
if (name === undefined) {
|
||||||
|
realArgs.shift();
|
||||||
|
}
|
||||||
|
return action.apply(this, realArgs);
|
||||||
|
}
|
||||||
|
function runInWrappedAction(func) {
|
||||||
|
runInAction(() => {
|
||||||
|
executeAction.call(this, func);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { go, wrappedAction, runInWrappedAction };
|
||||||
+76
-13
@@ -1,6 +1,8 @@
|
|||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import store from "../store/global";
|
import store from "../store/global";
|
||||||
|
|
||||||
|
// ENHANCE: 通过mobx的computed机制缓存一些计算值
|
||||||
|
|
||||||
let canvas, context2D, defaultFontFamily;
|
let canvas, context2D, defaultFontFamily;
|
||||||
let measureSvgEl, measureTextEl;
|
let measureSvgEl, measureTextEl;
|
||||||
|
|
||||||
@@ -55,18 +57,35 @@ function _calcTextBox(fontSize, text) {
|
|||||||
return measureTextEl.getBBox();
|
return measureTextEl.getBBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断段落中的音符是否有连音线
|
||||||
|
function _hasTie(paragraph) {
|
||||||
|
const notations = paragraph?.notations || [];
|
||||||
|
return notations.some(
|
||||||
|
(n, i) => n.tieTo && notations.findIndex((nn) => nn.key === n.tieTo) > i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const placement = observable({
|
const placement = observable({
|
||||||
get maxContentWidth() {
|
get maxContentWidth() {
|
||||||
return Math.max(store.canvasWidth - store.marginHorizontal * 2, 0);
|
return Math.max(store.canvasWidth - store.marginHorizontal * 2, 0);
|
||||||
},
|
},
|
||||||
get underlineOffsetY() {
|
get minTieHeight() {
|
||||||
|
return 8;
|
||||||
|
},
|
||||||
|
get maxTieHeight() {
|
||||||
|
return 25;
|
||||||
|
},
|
||||||
|
get underlineStepOffsetY() {
|
||||||
return 3;
|
return 3;
|
||||||
},
|
},
|
||||||
|
get underlineInitialOffsetY() {
|
||||||
|
return this.xHeight / 2 - 3;
|
||||||
|
},
|
||||||
get octaveStepOffsetY() {
|
get octaveStepOffsetY() {
|
||||||
return 5;
|
return 5;
|
||||||
},
|
},
|
||||||
get octaveInitialOffsetAbove() {
|
get octaveInitialOffsetAbove() {
|
||||||
return -(placement.xHeight / 2 + 2);
|
return placement.xHeight / 2 + 2;
|
||||||
},
|
},
|
||||||
get octaveInitialOffsetBelow() {
|
get octaveInitialOffsetBelow() {
|
||||||
return placement.xHeight / 2 - 2;
|
return placement.xHeight / 2 - 2;
|
||||||
@@ -119,13 +138,22 @@ const calcSubTextWidth = _calcTextWidth.bind(null, store.defaultSubFontSize);
|
|||||||
|
|
||||||
// 计算段落行高
|
// 计算段落行高
|
||||||
function calcParagraphHeight(paragraph) {
|
function calcParagraphHeight(paragraph) {
|
||||||
// TODO: 连音符高度
|
const notations = paragraph.notations || [];
|
||||||
const maxNoteHeight = Math.max(
|
let tieHeight = 0;
|
||||||
...paragraph.notations.map((n) => calcNotationHeight(n))
|
if (_hasTie(paragraph)) {
|
||||||
);
|
tieHeight = placement.maxTieHeight;
|
||||||
return maxNoteHeight + store.gapBetweenParagraph;
|
}
|
||||||
|
// 段落高度不是最高的音符的高度,而是上边偏移最大的音符的上部分偏移量+下边偏移最
|
||||||
|
// 大的音符的下部分偏移量
|
||||||
|
const noteHeightMap = notations.map((n) => calcNotationHeight(n));
|
||||||
|
const aboveOffsetMap = notations.map((n) => calcNotationAboveOffset(n));
|
||||||
|
const belowOffsetMap = noteHeightMap.map((v, i) => v - aboveOffsetMap[i]);
|
||||||
|
const maxNoteOffset =
|
||||||
|
Math.max(...belowOffsetMap) + Math.max(...aboveOffsetMap);
|
||||||
|
return tieHeight + maxNoteOffset + store.gapBetweenParagraph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算一行中所有音符的宽度和
|
||||||
function calcParagraphWidth(paragraph) {
|
function calcParagraphWidth(paragraph) {
|
||||||
const notations = paragraph.notations || [];
|
const notations = paragraph.notations || [];
|
||||||
if (notations.length === 0) {
|
if (notations.length === 0) {
|
||||||
@@ -138,6 +166,19 @@ function calcParagraphWidth(paragraph) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算段落中的音符中心以上偏移量的最大值
|
||||||
|
function calcParagraphAboveOffset(paragraph) {
|
||||||
|
const notations = paragraph.notations || [];
|
||||||
|
let tieHeight = 0;
|
||||||
|
if (_hasTie(paragraph)) {
|
||||||
|
tieHeight = placement.maxTieHeight;
|
||||||
|
}
|
||||||
|
const notationOffset = notations
|
||||||
|
.map((n) => calcNotationAboveOffset(n))
|
||||||
|
.reduce((prev, curr) => Math.max(prev, curr), placement.xHeight / 2);
|
||||||
|
return notationOffset + tieHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// 计算音符中心位置左边的宽度
|
// 计算音符中心位置左边的宽度
|
||||||
function calcNotationPrefixOffset(notation) {
|
function calcNotationPrefixOffset(notation) {
|
||||||
const prefixes = notation.prefixSups || [];
|
const prefixes = notation.prefixSups || [];
|
||||||
@@ -151,10 +192,9 @@ function calcNotationAboveOffset(notation) {
|
|||||||
offsets.push(noteOffsetTop);
|
offsets.push(noteOffsetTop);
|
||||||
let octaveOffset = 0;
|
let octaveOffset = 0;
|
||||||
if (notation.octave > 0) {
|
if (notation.octave > 0) {
|
||||||
octaveOffset = Math.abs(
|
octaveOffset =
|
||||||
placement.octaveInitialOffsetAbove -
|
placement.octaveInitialOffsetAbove +
|
||||||
placement.octaveStepOffsetY * notation.octave
|
placement.octaveStepOffsetY * Math.abs(notation.octave);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
offsets.push(octaveOffset);
|
offsets.push(octaveOffset);
|
||||||
const topDecoratorOffset =
|
const topDecoratorOffset =
|
||||||
@@ -184,10 +224,20 @@ function calcNotationHeight(notation) {
|
|||||||
supOffsetY;
|
supOffsetY;
|
||||||
offsets.push(noteOffsetTop, noteOffsetBottom);
|
offsets.push(noteOffsetTop, noteOffsetBottom);
|
||||||
const oc = notation.octave | 0;
|
const oc = notation.octave | 0;
|
||||||
|
const underlineOffsetY =
|
||||||
|
notation.underline > 0
|
||||||
|
? placement.underlineInitialOffsetY +
|
||||||
|
placement.underlineStepOffsetY * (notation.underline | 0)
|
||||||
|
: 0;
|
||||||
|
offsets.push(underlineOffsetY);
|
||||||
|
const octaveInitialOffsetY =
|
||||||
|
notation.octave > 0
|
||||||
|
? -placement.octaveInitialOffsetAbove
|
||||||
|
: placement.octaveInitialOffsetBelow + underlineOffsetY;
|
||||||
if (oc > 0) {
|
if (oc > 0) {
|
||||||
octaveOffsetY = placement.octaveInitialOffsetAbove + 5 * oc;
|
octaveOffsetY = octaveInitialOffsetY - 5 * Math.abs(oc);
|
||||||
} else if (oc < 0) {
|
} else if (oc < 0) {
|
||||||
octaveOffsetY = placement.octaveInitialOffsetBelow + 5 * oc;
|
octaveOffsetY = octaveInitialOffsetY + 5 * Math.abs(oc);
|
||||||
} else {
|
} else {
|
||||||
octaveOffsetY = 0;
|
octaveOffsetY = 0;
|
||||||
}
|
}
|
||||||
@@ -196,6 +246,18 @@ function calcNotationHeight(notation) {
|
|||||||
// 加上基准以上的半个字符的高度,得到前置上标的最大纵向偏移
|
// 加上基准以上的半个字符的高度,得到前置上标的最大纵向偏移
|
||||||
supOffsetY = notation.prefixSups?.length ? placement.subXHeight - 4 : 0;
|
supOffsetY = notation.prefixSups?.length ? placement.subXHeight - 4 : 0;
|
||||||
offsets.push(supOffsetY);
|
offsets.push(supOffsetY);
|
||||||
|
// 顶部装饰符在高八度点之上
|
||||||
|
let octaveOffsetYAbove = 0;
|
||||||
|
if (notation.octave > 0) {
|
||||||
|
octaveOffsetYAbove =
|
||||||
|
placement.octaveInitialOffsetAbove -
|
||||||
|
placement.octaveStepOffsetY * notation.octave;
|
||||||
|
}
|
||||||
|
const topDecoratorOffset =
|
||||||
|
octaveOffsetYAbove -
|
||||||
|
placement.subXHeight * (notation.topDecorators?.length | 0) -
|
||||||
|
2;
|
||||||
|
offsets.push(topDecoratorOffset);
|
||||||
const maxOffsetY = Math.max(...offsets);
|
const maxOffsetY = Math.max(...offsets);
|
||||||
const minOffsetY = Math.min(...offsets);
|
const minOffsetY = Math.min(...offsets);
|
||||||
return maxOffsetY - minOffsetY;
|
return maxOffsetY - minOffsetY;
|
||||||
@@ -208,6 +270,7 @@ export {
|
|||||||
calcNotationAboveOffset,
|
calcNotationAboveOffset,
|
||||||
calcParagraphWidth,
|
calcParagraphWidth,
|
||||||
calcParagraphHeight,
|
calcParagraphHeight,
|
||||||
|
calcParagraphAboveOffset,
|
||||||
calcTextWidth,
|
calcTextWidth,
|
||||||
calcSubTextWidth,
|
calcSubTextWidth,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { observer } from "mobx-react-lite";
|
|||||||
import store from "../../store/global";
|
import store from "../../store/global";
|
||||||
import Styles from "./index.module.css";
|
import Styles from "./index.module.css";
|
||||||
|
|
||||||
|
// ENHANCE: 增加整体缩放能力
|
||||||
|
|
||||||
function Canvas({ children, ...props }) {
|
function Canvas({ children, ...props }) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
function ConfigModal({ visible, onVisibleChange }) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: do this
|
||||||
|
export default ConfigModal;
|
||||||
+27
-32
@@ -1,58 +1,41 @@
|
|||||||
import SubMenu from "antd/lib/menu/SubMenu";
|
import { Button, Dropdown } from "antd";
|
||||||
import { Button, Dropdown, Menu } from "antd";
|
|
||||||
import {
|
import {
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
FolderOpenOutlined,
|
SettingOutlined,
|
||||||
PlusOutlined,
|
SyncOutlined,
|
||||||
SaveOutlined,
|
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useState } from "react";
|
||||||
import store from "../../store/global";
|
import store from "../../store/global";
|
||||||
import P, { calcParagraphHeight } from "../../util/placement";
|
import P, {
|
||||||
|
calcParagraphAboveOffset,
|
||||||
|
calcParagraphHeight,
|
||||||
|
} from "../../util/placement";
|
||||||
import Canvas from "../Canvas";
|
import Canvas from "../Canvas";
|
||||||
|
import ConfigModal from "../ConfigModal";
|
||||||
import Header from "../Header";
|
import Header from "../Header";
|
||||||
import Paragraph from "../Paragraph";
|
import Paragraph from "../Paragraph";
|
||||||
import Row from "../Row";
|
import Row from "../Row";
|
||||||
|
import { convertMenu, editMenu, fileMenu } from "../menu/editor";
|
||||||
import Styles from "./index.module.css";
|
import Styles from "./index.module.css";
|
||||||
|
|
||||||
function Editor() {
|
function Editor() {
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const heightCache = [];
|
const heightCache = [];
|
||||||
function accumulate(index) {
|
function accumulate(index) {
|
||||||
|
const paragraphs = store.paragraphs || [];
|
||||||
let height = 0;
|
let height = 0;
|
||||||
for (let i = 0; i < index; i++) {
|
for (let i = 0; i < index; i++) {
|
||||||
const p = store.paragraphs[i];
|
const p = paragraphs[i];
|
||||||
heightCache[i] = heightCache[i] || calcParagraphHeight(p);
|
heightCache[i] = heightCache[i] || calcParagraphHeight(p);
|
||||||
height += heightCache[i];
|
height += heightCache[i];
|
||||||
}
|
}
|
||||||
|
// 段落渲染定位基于段落中心,因此加上上半部分的偏移量
|
||||||
|
height += calcParagraphAboveOffset(paragraphs[index]);
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileMenu = (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key="create" icon={<PlusOutlined />}>
|
|
||||||
新建
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="open" icon={<FolderOpenOutlined />}>
|
|
||||||
打开
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="save" icon={<SaveOutlined />}>
|
|
||||||
保存
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
const editMenu = (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key="title">编辑曲名</Menu.Item>
|
|
||||||
<SubMenu key="tone" title="转调">
|
|
||||||
<Menu.Item key="setting:1">Option 1</Menu.Item>
|
|
||||||
<Menu.Item key="setting:2">Option 2</Menu.Item>
|
|
||||||
<Menu.Item key="setting:3">Option 3</Menu.Item>
|
|
||||||
<Menu.Item key="setting:4">Option 4</Menu.Item>
|
|
||||||
</SubMenu>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={Styles.container}
|
className={Styles.container}
|
||||||
@@ -73,6 +56,14 @@ function Editor() {
|
|||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<Dropdown overlay={convertMenu} placement="bottomLeft">
|
||||||
|
<Button icon={<SyncOutlined />} type="text">
|
||||||
|
转调
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<Button icon={<SettingOutlined />} type="text">
|
||||||
|
配置
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Canvas>
|
<Canvas>
|
||||||
@@ -94,6 +85,10 @@ function Editor() {
|
|||||||
})}
|
})}
|
||||||
</Row>
|
</Row>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
<ConfigModal
|
||||||
|
visible={isModalVisible}
|
||||||
|
onVisibleChange={setIsModalVisible}
|
||||||
|
></ConfigModal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,32 +4,33 @@ import { observer } from "mobx-react-lite";
|
|||||||
import EditableContent from "../../../component/EditableContent";
|
import EditableContent from "../../../component/EditableContent";
|
||||||
import P from "../../../util/placement";
|
import P from "../../../util/placement";
|
||||||
import store from "../../../store/global";
|
import store from "../../../store/global";
|
||||||
|
import { wrappedAction } from "../../../store/history";
|
||||||
import Row from "../../Row";
|
import Row from "../../Row";
|
||||||
import Text from "../../Text";
|
import Text from "../../Text";
|
||||||
|
|
||||||
const tones = [
|
const tones = [
|
||||||
"A",
|
|
||||||
"B",
|
|
||||||
"C",
|
"C",
|
||||||
"D",
|
"D",
|
||||||
"E",
|
"E",
|
||||||
"F",
|
"F",
|
||||||
"G",
|
"G",
|
||||||
"♭A",
|
"A",
|
||||||
"♭B",
|
"B",
|
||||||
"♭C",
|
"♭C",
|
||||||
"♭D",
|
"♭D",
|
||||||
"♭E",
|
"♭E",
|
||||||
"♭F",
|
"♭F",
|
||||||
"♭G",
|
"♭G",
|
||||||
|
"♭A",
|
||||||
|
"♭B",
|
||||||
].map((t) => ({ key: t, text: t }));
|
].map((t) => ({ key: t, text: t }));
|
||||||
const handleChangeTone = action(function (value) {
|
const handleChangeTone = wrappedAction(function (value) {
|
||||||
store.tone = value;
|
store.tone = value;
|
||||||
});
|
});
|
||||||
const handleChangeSpeed = action(function (value) {
|
const handleChangeSpeed = wrappedAction(function (value) {
|
||||||
store.speed = value;
|
store.speed = value;
|
||||||
});
|
});
|
||||||
const handleChangeBeat = action(function (value) {
|
const handleChangeBeat = wrappedAction(function (value) {
|
||||||
const beat = String(value).split("/");
|
const beat = String(value).split("/");
|
||||||
if (beat.length !== 2) {
|
if (beat.length !== 2) {
|
||||||
message.error("请以【*/*】的格式输入节拍!");
|
message.error("请以【*/*】的格式输入节拍!");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import EditableContent from "../../../component/EditableContent";
|
import EditableContent from "../../../component/EditableContent";
|
||||||
import P from "../../../util/placement";
|
import P from "../../../util/placement";
|
||||||
import store from "../../../store/global";
|
import store from "../../../store/global";
|
||||||
|
import { wrappedAction } from "../../../store/history";
|
||||||
import Row from "../../Row";
|
import Row from "../../Row";
|
||||||
import Text from "../../Text";
|
import Text from "../../Text";
|
||||||
|
|
||||||
@@ -32,20 +33,19 @@ const blockContextMenu = [
|
|||||||
text: "添加作者信息",
|
text: "添加作者信息",
|
||||||
icon: <PlusOutlined style={{ color: "grey" }} />,
|
icon: <PlusOutlined style={{ color: "grey" }} />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
store.authors.push("【作曲者】 作曲");
|
|
||||||
store.authors.push("【记谱者】 记谱");
|
store.authors.push("【记谱者】 记谱");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const handleSelectMenu = action((index, value) => {
|
const handleSelectMenu = wrappedAction((index, value) => {
|
||||||
const menu = authorContextMenu.find((m) => m.key === value);
|
const menu = authorContextMenu.find((m) => m.key === value);
|
||||||
menu?.onClick(index);
|
menu?.onClick(index);
|
||||||
});
|
});
|
||||||
const handleSelectBlockMenu = action((value) => {
|
const handleSelectBlockMenu = wrappedAction((value) => {
|
||||||
const menu = blockContextMenu.find((m) => m.key === value);
|
const menu = blockContextMenu.find((m) => m.key === value);
|
||||||
menu?.onClick();
|
menu?.onClick();
|
||||||
});
|
});
|
||||||
const handleChangeAuthor = action((index, value) => {
|
const handleChangeAuthor = wrappedAction((index, value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
|
|||||||
@@ -3,20 +3,15 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import EditableContent from "../../../component/EditableContent";
|
import EditableContent from "../../../component/EditableContent";
|
||||||
import store from "../../../store/global";
|
import store from "../../../store/global";
|
||||||
|
import { wrappedAction } from "../../../store/history";
|
||||||
import Row from "../../Row";
|
import Row from "../../Row";
|
||||||
import Text from "../../Text";
|
import Text from "../../Text";
|
||||||
const handleChangeTitle = action((value) => {
|
const handleChangeTitle = wrappedAction((value) => {
|
||||||
store.title = value;
|
store.title = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
function Title() {
|
function Title() {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
useEffect(() => {
|
|
||||||
store.popoverRefs.title = ref;
|
|
||||||
return () => {
|
|
||||||
store.popoverRefs.title = null;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<Row type="title" offsetY={store.marginTop}>
|
<Row type="title" offsetY={store.marginTop}>
|
||||||
<EditableContent
|
<EditableContent
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ function composeArray(octave) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Notation({ offsetX, notation }) {
|
function Notation({ offsetX, notation }) {
|
||||||
const underlineOffset = P.underlineOffsetY * (notation.underline | 0);
|
const underlineOffset = P.underlineStepOffsetY * (notation.underline | 0);
|
||||||
const octaveInitialOffset =
|
const octaveInitialOffset =
|
||||||
notation.octave > 0
|
notation.octave > 0
|
||||||
? P.octaveInitialOffsetAbove
|
? -P.octaveInitialOffsetAbove
|
||||||
: P.octaveInitialOffsetBelow + underlineOffset;
|
: P.octaveInitialOffsetBelow + underlineOffset;
|
||||||
const octaveStepOffset = (notation.octave > 0 ? -1 : 1) * P.octaveStepOffsetY;
|
const octaveStepOffset = (notation.octave > 0 ? -1 : 1) * P.octaveStepOffsetY;
|
||||||
let topDecoratorOffset = 0;
|
let topDecoratorOffset = 0;
|
||||||
@@ -82,7 +82,7 @@ function Notation({ offsetX, notation }) {
|
|||||||
key={i}
|
key={i}
|
||||||
type="octave"
|
type="octave"
|
||||||
cx="0"
|
cx="0"
|
||||||
cy={octaveStepOffset * i + octaveInitialOffset}
|
cy={octaveInitialOffset + octaveStepOffset * i}
|
||||||
r="2"
|
r="2"
|
||||||
></circle>
|
></circle>
|
||||||
));
|
));
|
||||||
|
|||||||
+18
-11
@@ -3,6 +3,7 @@ import P, {
|
|||||||
calcNotationAboveOffset,
|
calcNotationAboveOffset,
|
||||||
calcNotationPrefixOffset,
|
calcNotationPrefixOffset,
|
||||||
calcNotationWidth,
|
calcNotationWidth,
|
||||||
|
calcParagraphAboveOffset,
|
||||||
calcParagraphHeight,
|
calcParagraphHeight,
|
||||||
calcParagraphWidth,
|
calcParagraphWidth,
|
||||||
} from "../../util/placement";
|
} from "../../util/placement";
|
||||||
@@ -13,9 +14,6 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
|
|||||||
console.log("render paragraph");
|
console.log("render paragraph");
|
||||||
const notations = paragraph.notations || [];
|
const notations = paragraph.notations || [];
|
||||||
const widthCache = [];
|
const widthCache = [];
|
||||||
const paraOffsetY = Math.max(
|
|
||||||
...paragraph.notations.map((n) => calcNotationAboveOffset(n))
|
|
||||||
);
|
|
||||||
let itemFlexOffset = 0;
|
let itemFlexOffset = 0;
|
||||||
if (alignJustify && paragraph.notations?.length > 1) {
|
if (alignJustify && paragraph.notations?.length > 1) {
|
||||||
const realWidth = calcParagraphWidth(paragraph);
|
const realWidth = calcParagraphWidth(paragraph);
|
||||||
@@ -31,7 +29,9 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
|
|||||||
widthCache[i] = widthCache[i] || calcNotationWidth(n);
|
widthCache[i] = widthCache[i] || calcNotationWidth(n);
|
||||||
width += widthCache[i];
|
width += widthCache[i];
|
||||||
}
|
}
|
||||||
return width + calcNotationPrefixOffset(notations[index]);
|
// 音符的定位基准是其中心,因此要加上当前音符的前缀部分偏移
|
||||||
|
width += calcNotationPrefixOffset(notations[index]);
|
||||||
|
return width;
|
||||||
};
|
};
|
||||||
|
|
||||||
const noteOffsets = notations.map((_, i) => {
|
const noteOffsets = notations.map((_, i) => {
|
||||||
@@ -62,7 +62,7 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
|
|||||||
const lines = [];
|
const lines = [];
|
||||||
// 记录音符当前需要绘制的增减时线的条数
|
// 记录音符当前需要绘制的增减时线的条数
|
||||||
let helpMap = notations.map((n) => (n.underline > 0 ? n.underline : 0));
|
let helpMap = notations.map((n) => (n.underline > 0 ? n.underline : 0));
|
||||||
let baseOffsetY = P.xHeight / 2 - P.underlineOffsetY;
|
let baseOffsetY = P.underlineInitialOffsetY;
|
||||||
while (helpMap.some((n) => n > 0)) {
|
while (helpMap.some((n) => n > 0)) {
|
||||||
let fromIndex = -1;
|
let fromIndex = -1;
|
||||||
let toIndex = fromIndex;
|
let toIndex = fromIndex;
|
||||||
@@ -98,19 +98,26 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
baseOffsetY += P.underlineOffsetY;
|
baseOffsetY += P.underlineStepOffsetY;
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染连音线
|
// 渲染连音线
|
||||||
const renderTie = (offsetY, fromIndex, toIndex) => {
|
const renderTie = (offsetY, fromIndex, toIndex) => {
|
||||||
const bezierX1 = 0; //noteOffsets[fromIndex];
|
const bezierX1 = noteOffsets[fromIndex];
|
||||||
const bezierX2 = bezierX1 + 4;
|
const bezierX2 = bezierX1 + 4;
|
||||||
const bezierX4 = 32// noteOffsets[toIndex];
|
const bezierX4 = noteOffsets[toIndex];
|
||||||
const bezierX3 = bezierX4 - 4;
|
const bezierX3 = bezierX4 - 4;
|
||||||
const bezierY = offsetY - 12;
|
const x = bezierX4 - bezierX1;
|
||||||
console.log(bezierX4 - bezierX1, bezierY - offsetY);
|
// ENHANCE: 优化贝赛尔曲线
|
||||||
|
let y =
|
||||||
|
0.000021917145491103032 * x * x * x -
|
||||||
|
0.0062948773732605465 * x * x +
|
||||||
|
0.6378785891964498 * x -
|
||||||
|
0.6843414475200537;
|
||||||
|
y = Math.max(Math.min(y, P.maxTieHeight), P.minTieHeight);
|
||||||
|
const bezierY = offsetY - y;
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
d={`M${bezierX1} ${offsetY} C${bezierX2} ${bezierY} ${bezierX3} ${bezierY} ${bezierX4} ${offsetY}`}
|
d={`M${bezierX1} ${offsetY} C${bezierX2} ${bezierY} ${bezierX3} ${bezierY} ${bezierX4} ${offsetY}`}
|
||||||
@@ -148,7 +155,7 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row type="paragraph" offsetY={offsetY + paraOffsetY}>
|
<Row type="paragraph" offsetY={offsetY}>
|
||||||
{renderTies()}
|
{renderTies()}
|
||||||
{paragraph.notations.map((n, i) => (
|
{paragraph.notations.map((n, i) => (
|
||||||
<Notation key={n.key} notation={n} offsetX={noteOffsets[i]} />
|
<Notation key={n.key} notation={n} offsetX={noteOffsets[i]} />
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
EditOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
FolderOpenOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
SaveOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { Menu } from "antd";
|
||||||
|
import store from "../../store/global";
|
||||||
|
import { go, runInWrappedAction } from "../../store/history";
|
||||||
|
|
||||||
|
const { SubMenu } = Menu;
|
||||||
|
const fileMenu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item key="create" icon={<PlusOutlined />}>
|
||||||
|
新建
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="open" icon={<FolderOpenOutlined />}>
|
||||||
|
打开
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="save" icon={<SaveOutlined />}>
|
||||||
|
保存
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
const editMenu = (
|
||||||
|
<Menu onClick={handleMenu}>
|
||||||
|
<Menu.Item key="undo">撤销</Menu.Item>
|
||||||
|
<Menu.Item key="redo">重做</Menu.Item>
|
||||||
|
<Menu.Item key="reset-title">重置歌曲名称</Menu.Item>
|
||||||
|
<Menu.Item key="reset-authors">重置作者信息</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
const convertMenu = (
|
||||||
|
<Menu key="tone" title="转调">
|
||||||
|
<SubMenu key="convert-to" title="转到...">
|
||||||
|
<Menu.Item key="convert-to-C">1 = C</Menu.Item>
|
||||||
|
<Menu.Item key="convert-to-D">1 = D</Menu.Item>
|
||||||
|
<Menu.Item key="convert-to-E">1 = E</Menu.Item>
|
||||||
|
<Menu.Item key="convert-to-F">1 = F</Menu.Item>
|
||||||
|
<Menu.Item key="convert-to-G">1 = G</Menu.Item>
|
||||||
|
<Menu.Item key="convert-to-A">1 = A</Menu.Item>
|
||||||
|
<Menu.Item key="convert-to-B">1 = B</Menu.Item>
|
||||||
|
<Menu.Item key="convert-to-bC">
|
||||||
|
1 = <sup>♭</sup>C
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="convert-to-bD">
|
||||||
|
1 = <sup>♭</sup>D
|
||||||
|
</Menu.Item>{" "}
|
||||||
|
<Menu.Item key="convert-to-bE">
|
||||||
|
1 = <sup>♭</sup>E
|
||||||
|
</Menu.Item>{" "}
|
||||||
|
<Menu.Item key="convert-to-bF">
|
||||||
|
1 = <sup>♭</sup>F
|
||||||
|
</Menu.Item>{" "}
|
||||||
|
<Menu.Item key="convert-to-bG">
|
||||||
|
1 = <sup>♭</sup>G
|
||||||
|
</Menu.Item>{" "}
|
||||||
|
<Menu.Item key="convert-to-bA">
|
||||||
|
1 = <sup>♭</sup>A
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="convert-to-bB">
|
||||||
|
1 = <sup>♭</sup>B
|
||||||
|
</Menu.Item>
|
||||||
|
</SubMenu>
|
||||||
|
<Menu.Item key="convert-up">升高一个音</Menu.Item>
|
||||||
|
<Menu.Item key="convert-down">降低一个音</Menu.Item>
|
||||||
|
<Menu.Item key="convert-up8">升高一个八度</Menu.Item>
|
||||||
|
<Menu.Item key="convert-down8">降低一个八度</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleMenu({ key }) {
|
||||||
|
switch (key) {
|
||||||
|
case "undo":
|
||||||
|
go(-1);
|
||||||
|
break;
|
||||||
|
case "redo":
|
||||||
|
go(1);
|
||||||
|
break;
|
||||||
|
case "reset-title":
|
||||||
|
runInWrappedAction(() => {
|
||||||
|
store.title = "【歌曲名称】";
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "reset-authors":
|
||||||
|
store.authors = [
|
||||||
|
"【作曲者】 作曲",
|
||||||
|
"【填词者】 填词",
|
||||||
|
"【记谱者】 记谱",
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fileMenu, editMenu, convertMenu };
|
||||||
Reference in New Issue
Block a user