daily: 2021-09-29 Night
This commit is contained in:
@@ -70,18 +70,21 @@ const EditableContent = function (
|
|||||||
const renderSelectionPopover = useCallback(() => {
|
const renderSelectionPopover = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
<Menu className={Styles.menu}>
|
<Menu className={Styles.menu}>
|
||||||
{(options || []).map((option) => (
|
{(options || [])
|
||||||
<Menu.Item
|
.filter((op) => op.visible !== false)
|
||||||
icon={option.icon}
|
.map((option) => (
|
||||||
key={option.key}
|
<Menu.Item
|
||||||
className={
|
icon={option.icon}
|
||||||
Styles.menuItem + (option.key === initialValue ? " selected" : "")
|
key={option.key}
|
||||||
}
|
className={
|
||||||
onClick={() => handleConfirm(option.key)}
|
Styles.menuItem +
|
||||||
>
|
(option.key === initialValue ? " selected" : "")
|
||||||
{option.text}
|
}
|
||||||
</Menu.Item>
|
onClick={() => handleConfirm(option.key)}
|
||||||
))}
|
>
|
||||||
|
{option.text}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}, [initialValue, options, handleConfirm]);
|
}, [initialValue, options, handleConfirm]);
|
||||||
@@ -98,7 +101,7 @@ const EditableContent = function (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverOnSvg
|
<PopoverOnSvg
|
||||||
trigger="click"
|
trigger={inputType === "select" ? "context" : "click"}
|
||||||
title={title}
|
title={title}
|
||||||
renderContent={renderContent}
|
renderContent={renderContent}
|
||||||
renderPopover={renderPopover}
|
renderPopover={renderPopover}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ function PopoverOnSvg({
|
|||||||
trigger,
|
trigger,
|
||||||
visible,
|
visible,
|
||||||
autoHide,
|
autoHide,
|
||||||
// TODO: complete this
|
|
||||||
showArrow,
|
showArrow,
|
||||||
onVisibilityChange,
|
onVisibilityChange,
|
||||||
}) {
|
}) {
|
||||||
@@ -199,7 +198,6 @@ function PopoverOnSvg({
|
|||||||
makeOffset(getContainerOffset().x, getContainerOffset().y);
|
makeOffset(getContainerOffset().x, getContainerOffset().y);
|
||||||
mergedStyles.transform += ` translate(${offsetX}px, ${offsetY}px)`;
|
mergedStyles.transform += ` translate(${offsetX}px, ${offsetY}px)`;
|
||||||
} else {
|
} else {
|
||||||
//TODO:
|
|
||||||
mergedStyles.left = lastContextCord.x;
|
mergedStyles.left = lastContextCord.x;
|
||||||
mergedStyles.top = lastContextCord.y;
|
mergedStyles.top = lastContextCord.y;
|
||||||
mergedStyles.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
mergedStyles.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
||||||
@@ -242,10 +240,12 @@ function PopoverOnSvg({
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
{showArrow && (
|
||||||
className={arrowClassNames.join(" ")}
|
<div
|
||||||
style={arrowStyles}
|
className={arrowClassNames.join(" ")}
|
||||||
></div>
|
style={arrowStyles}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
{title && (
|
{title && (
|
||||||
<header className={headerClassNames.join(" ")}>
|
<header className={headerClassNames.join(" ")}>
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import {
|
||||||
|
FolderOpenOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
SaveOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { Menu } from "antd";
|
||||||
|
import { action } from "mobx";
|
||||||
|
import Notation from "../view/Notation";
|
||||||
|
import state from "../store/state";
|
||||||
|
import store from "../store/global";
|
||||||
|
import { createNotation, isNote, notations as N } from "../util/notation";
|
||||||
|
import { findParagraphAndNotation } from "../util/editor";
|
||||||
|
import { go, runInWrappedAction, wrappedAction } 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-title">重置歌曲名称</Menu.Item>
|
||||||
|
<Menu.Item key="reset-authors">重置作者信息</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
const convertMenu = (
|
||||||
|
<Menu key="tone">
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = wrappedAction((ev) => {
|
||||||
|
if (state.shouldNotationBlurAfterClick) {
|
||||||
|
state.selectedNotationKey = null;
|
||||||
|
}
|
||||||
|
state.shouldNotationBlurAfterClick = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleKeyPress = wrappedAction((ev) => {
|
||||||
|
const inputKey = ev.key.toLowerCase();
|
||||||
|
const shift = ev.shiftKey;
|
||||||
|
const ctrl = ev.ctrlKey;
|
||||||
|
|
||||||
|
if (state.selectedNotationKey) {
|
||||||
|
// 仅选中符号时作用
|
||||||
|
const {
|
||||||
|
paragraph,
|
||||||
|
notation,
|
||||||
|
paragraphIndex,
|
||||||
|
notationIndex,
|
||||||
|
} = findParagraphAndNotation(state.selectedNotationKey);
|
||||||
|
if (!notation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (true) {
|
||||||
|
case isNote(inputKey) && !ctrl && !shift:
|
||||||
|
notation.note = inputKey;
|
||||||
|
break;
|
||||||
|
case inputKey === "-" && !ctrl && !shift:
|
||||||
|
notation.note = N.extend;
|
||||||
|
break;
|
||||||
|
case inputKey === "|" && !ctrl:
|
||||||
|
notation.note = N.separator;
|
||||||
|
break;
|
||||||
|
case inputKey === "u" && !ctrl && !shift:
|
||||||
|
notation.underline += 1;
|
||||||
|
break;
|
||||||
|
case inputKey === "u" && !ctrl && shift:
|
||||||
|
notation.underline -= 1;
|
||||||
|
notation.underline = Math.max(0, notation.underline);
|
||||||
|
break;
|
||||||
|
case inputKey === "." && !ctrl && !shift:
|
||||||
|
notation.dotted = !notation.dotted;
|
||||||
|
break;
|
||||||
|
case inputKey === "8" && !ctrl && !shift:
|
||||||
|
notation.octave += 1;
|
||||||
|
break;
|
||||||
|
case inputKey === "*" && !ctrl:
|
||||||
|
notation.octave -= 1;
|
||||||
|
break;
|
||||||
|
case inputKey === "#" && !ctrl:
|
||||||
|
if (notation.prefixSups.indexOf("♯") !== 0) {
|
||||||
|
if (notation.prefixSups[0] === "♭") {
|
||||||
|
notation.prefixSups.shift();
|
||||||
|
}
|
||||||
|
notation.prefixSups.unshift("♯");
|
||||||
|
} else {
|
||||||
|
notation.prefixSups.shift();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case inputKey === "b" && !ctrl && !shift:
|
||||||
|
if (notation.prefixSups.indexOf("♭") !== 0) {
|
||||||
|
if (notation.prefixSups[0] === "♯") {
|
||||||
|
notation.prefixSups.shift();
|
||||||
|
}
|
||||||
|
notation.prefixSups.unshift("♭");
|
||||||
|
} else {
|
||||||
|
notation.prefixSups.shift();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case inputKey === "~" && !ctrl:
|
||||||
|
if (notation.topDecorators.includes("~")) {
|
||||||
|
notation.topDecorators.splice(notation.topDecorators.indexOf("~"), 1);
|
||||||
|
} else {
|
||||||
|
notation.topDecorators.push("~");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case inputKey === "enter" && !ctrl && !shift:
|
||||||
|
if (paragraph.notations[notationIndex + 1]) {
|
||||||
|
state.selectedNotationKey =
|
||||||
|
paragraph.notations[notationIndex + 1].key;
|
||||||
|
} else {
|
||||||
|
const nextNotation = createNotation();
|
||||||
|
paragraph.notations.push(nextNotation);
|
||||||
|
state.selectedNotationKey = nextNotation.key;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case inputKey === "enter" && !ctrl && shift:
|
||||||
|
if (paragraph.notations[notationIndex - 1]) {
|
||||||
|
state.selectedNotationKey =
|
||||||
|
paragraph.notations[notationIndex - 1].key;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case inputKey === "enter" && ctrl && !shift:
|
||||||
|
const newNotation = createNotation();
|
||||||
|
paragraph.notations.splice(notationIndex + 1, 0, newNotation);
|
||||||
|
state.selectedNotationKey = newNotation.key;
|
||||||
|
break;
|
||||||
|
case inputKey === "delete":
|
||||||
|
paragraph.notations.splice(notationIndex, 1);
|
||||||
|
const currNotation =
|
||||||
|
paragraph.notations[notationIndex] ||
|
||||||
|
paragraph.notations[notationIndex - 1];
|
||||||
|
state.selectedNotationKey = currNotation?.key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (true) {
|
||||||
|
case inputKey === "z" && ctrl && !shift:
|
||||||
|
console.log("undo");
|
||||||
|
go(-1);
|
||||||
|
break;
|
||||||
|
case inputKey === "y" && ctrl && !shift:
|
||||||
|
case inputKey === "z" && ctrl && shift:
|
||||||
|
console.log("redo");
|
||||||
|
go(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { fileMenu, editMenu, convertMenu, handleKeyPress, handleClick };
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { message } from "antd";
|
||||||
|
import state from "../store/state";
|
||||||
|
import { findParagraphAndNotation } from "../util/editor";
|
||||||
|
import { isNote } from "../util/notation";
|
||||||
|
import { runInWrappedAction, wrappedAction } from "../store/history";
|
||||||
|
|
||||||
|
const getNotationContextItems = (notation, paragraph) => {
|
||||||
|
const hasTie = notation.tieTo;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "octaveUp",
|
||||||
|
text: "升高一个八度",
|
||||||
|
visible: isNote(notation),
|
||||||
|
onClick: wrappedAction(() => {
|
||||||
|
notation.octave += 1;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "octaveDown",
|
||||||
|
text: "降低一个八度",
|
||||||
|
visible: isNote(notation),
|
||||||
|
onClick: wrappedAction(() => {
|
||||||
|
notation.octave -= 1;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tie",
|
||||||
|
text: notation.tieTo ? "删除连音线" : "从此处添加连音线到...",
|
||||||
|
visible: isNote(notation),
|
||||||
|
onClick: wrappedAction(() => {
|
||||||
|
if (!hasTie) {
|
||||||
|
state.tieSourceKey = notation.key;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { notation: tieDesc } = findParagraphAndNotation(notation.tieTo);
|
||||||
|
if (tieDesc) {
|
||||||
|
tieDesc.tieTo = null;
|
||||||
|
}
|
||||||
|
notation.tieTo = null;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "break-underline",
|
||||||
|
text: notation.breakUnderline
|
||||||
|
? "在此处延续增减时线"
|
||||||
|
: "在此处打断增减时线",
|
||||||
|
visible: isNote(notation),
|
||||||
|
onClick: wrappedAction(() => {
|
||||||
|
notation.breakUnderline = !notation.breakUnderline;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "delete",
|
||||||
|
text: "删除",
|
||||||
|
onClick: wrappedAction(() => {
|
||||||
|
const index = paragraph.notations.indexOf(notation);
|
||||||
|
if (index !== -1) {
|
||||||
|
paragraph.notations.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
message.error("符号不存在");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ENHANCE: 改变<EditableContent>组件这个反人类的回调执行
|
||||||
|
// 执行选项回调。。
|
||||||
|
function handleNotationContext(options, key) {
|
||||||
|
runInWrappedAction(() => (state.shouldNotationBlurAfterClick = false));
|
||||||
|
const callback = options.find((ops) => ops.key === key)?.onClick;
|
||||||
|
callback && callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getNotationContextItems, handleNotationContext };
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
EnterOutlined,
|
||||||
|
MenuOutlined,
|
||||||
|
PlusCircleOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { Menu } from "antd";
|
||||||
|
import state from "../store/state";
|
||||||
|
import store from "../store/global";
|
||||||
|
import { go, runInWrappedAction, wrappedAction } from "../store/history";
|
||||||
|
|
||||||
|
function getParagraphMenuOptions(paragraph) {
|
||||||
|
return [];
|
||||||
|
const handleMenu = wrappedAction(({ key }) => {
|
||||||
|
switch (key) {
|
||||||
|
case "justify-auto":
|
||||||
|
paragraph.alignJustify = null;
|
||||||
|
break;
|
||||||
|
case "justify-enable":
|
||||||
|
paragraph.alignJustify = true;
|
||||||
|
break;
|
||||||
|
case "justify-disable":
|
||||||
|
paragraph.alignJustify = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu onClick={handleMenu}>
|
||||||
|
<Menu.Item key="addNotation" icon={<PlusOutlined />}>
|
||||||
|
添加符号
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="addParagraph" icon={<EnterOutlined />}>
|
||||||
|
添加段落
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.SubMenu key="justify" title="两端对齐" icon={<MenuOutlined />}>
|
||||||
|
<Menu.Item key="justify-auto">自动</Menu.Item>
|
||||||
|
<Menu.Item key="justify-enable">启用</Menu.Item>
|
||||||
|
<Menu.Item key="justify-disable">禁用</Menu.Item>
|
||||||
|
</Menu.SubMenu>
|
||||||
|
<Menu.Item danger key="delete" icon={<DeleteOutlined />}>
|
||||||
|
删除段落
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENHANCE: 同notation的菜单项处理函数,改改改
|
||||||
|
// TODO: working 现在就改!然后EditableContent应该直接传入antd的菜单项
|
||||||
|
_function handleSelectParagraphMenu(options, key) {
|
||||||
|
runInWrappedAction(() => (state.shouldNotationBlurAfterClick = false));
|
||||||
|
const callback = options.find((ops) => ops.key === key)?.onClick;
|
||||||
|
callback && callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getParagraphMenuOptions, handleSelectParagraphMenu };
|
||||||
+7
-5
@@ -1,7 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
|
import { createNotation } from "../util/notation";
|
||||||
|
|
||||||
|
// 全局数据及配置,需要持久化
|
||||||
let globalStore = observable({
|
let globalStore = observable({
|
||||||
|
version: 1,
|
||||||
canvasWidth: 1024,
|
canvasWidth: 1024,
|
||||||
canvasHeight: 1448,
|
canvasHeight: 1448,
|
||||||
defaultFontSize: 16,
|
defaultFontSize: 16,
|
||||||
@@ -12,7 +15,7 @@ let globalStore = observable({
|
|||||||
marginTop: 32,
|
marginTop: 32,
|
||||||
gapAfterTitle: 16,
|
gapAfterTitle: 16,
|
||||||
gapAfterHeader: 32,
|
gapAfterHeader: 32,
|
||||||
gapBetweenParagraph: 0,
|
gapBetweenParagraph: 32,
|
||||||
gapBetweenNotation: 8,
|
gapBetweenNotation: 8,
|
||||||
beat: [4, 4],
|
beat: [4, 4],
|
||||||
speed: 75,
|
speed: 75,
|
||||||
@@ -92,7 +95,7 @@ let globalStore = observable({
|
|||||||
underline: 0,
|
underline: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
].map((obj) => ((obj.key = obj.key || Math.random()), obj)),
|
].map((obj) => createNotation(obj)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
notations: [
|
notations: [
|
||||||
@@ -113,7 +116,7 @@ let globalStore = observable({
|
|||||||
octave: 0,
|
octave: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
].map((obj) => ((obj.key = obj.key || Math.random()), obj)),
|
].map((obj) => createNotation(obj)),
|
||||||
alignJustify: false,
|
alignJustify: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -140,13 +143,12 @@ let globalStore = observable({
|
|||||||
octave: 0,
|
octave: 0,
|
||||||
dotted: false,
|
dotted: false,
|
||||||
},
|
},
|
||||||
].map((obj) => ((obj.key = Math.random()), obj)),
|
].map((obj) => createNotation(obj)),
|
||||||
},
|
},
|
||||||
].map((obj) => ((obj.key = Math.random()), obj)),
|
].map((obj) => ((obj.key = Math.random()), obj)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const GlobalContext = React.createContext(globalStore);
|
const GlobalContext = React.createContext(globalStore);
|
||||||
|
|
||||||
|
|
||||||
export default globalStore;
|
export default globalStore;
|
||||||
export { GlobalContext };
|
export { GlobalContext };
|
||||||
|
|||||||
+48
-10
@@ -1,13 +1,41 @@
|
|||||||
import { action, intercept, runInAction, toJS } from "mobx";
|
import { action, intercept, keys, runInAction, toJS } from "mobx";
|
||||||
import globalStore from "./global";
|
import globalStore from "./global";
|
||||||
|
|
||||||
const storeHistory = [];
|
const storeHistory = [];
|
||||||
let cursor = -1;
|
let cursor = -1;
|
||||||
|
|
||||||
intercept(globalStore, (change) => {
|
// ENHANCE: 使用性能更好的方案
|
||||||
storeHistory.push(toJS(globalStore));
|
// 比较历史记录
|
||||||
return change;
|
function _isEqual(objA, objB) {
|
||||||
});
|
const cacheSet = new Set();
|
||||||
|
function _isCommonObject(obj) {
|
||||||
|
if (typeof obj !== "object" || obj === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const unRegularConstructors = [Promise, Map, Set, WeakMap, WeakSet];
|
||||||
|
return !unRegularConstructors.some((c) => obj instanceof c);
|
||||||
|
}
|
||||||
|
function _deepEqual(obj1, obj2) {
|
||||||
|
if (!_isCommonObject(obj1) || !_isCommonObject(obj2)) {
|
||||||
|
return obj1 === obj2;
|
||||||
|
}
|
||||||
|
if (obj1 === obj2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cacheSet.has(obj1) || cacheSet.has(obj2)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cacheSet.add(obj1);
|
||||||
|
cacheSet.add(obj2);
|
||||||
|
const keys1 = Object.keys(obj1);
|
||||||
|
const keys2 = Object.keys(obj2);
|
||||||
|
if (keys1.length !== keys2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return keys1.every((key) => _deepEqual(obj1[key], obj2[key]));
|
||||||
|
}
|
||||||
|
return _deepEqual(objA, objB);
|
||||||
|
}
|
||||||
|
|
||||||
// 在历史记录中漫游
|
// 在历史记录中漫游
|
||||||
function go(span) {
|
function go(span) {
|
||||||
@@ -15,21 +43,31 @@ function go(span) {
|
|||||||
Math.max(0, cursor + span),
|
Math.max(0, cursor + span),
|
||||||
storeHistory.length - 1
|
storeHistory.length - 1
|
||||||
);
|
);
|
||||||
|
console.log("go ", span);
|
||||||
if (newCursor === cursor) {
|
if (newCursor === cursor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cursor = newCursor;
|
cursor = newCursor;
|
||||||
|
console.log("gone to ", newCursor);
|
||||||
Object.assign(globalStore, storeHistory[newCursor]);
|
Object.assign(globalStore, storeHistory[newCursor]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 包装mobx的action以实现历史记录
|
// 包装mobx的action以实现历史记录
|
||||||
function executeAction(actionFunc, ...args) {
|
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);
|
actionFunc.apply(this, args);
|
||||||
|
const current = toJS(globalStore);
|
||||||
|
if (!_isEqual(storeHistory[cursor], current)) {
|
||||||
|
storeHistory[++cursor] = current;
|
||||||
|
storeHistory.length = cursor + 1;
|
||||||
|
console.log(
|
||||||
|
"pushed history, length & cursor: ",
|
||||||
|
storeHistory.length,
|
||||||
|
cursor
|
||||||
|
);
|
||||||
|
if (storeHistory.length > 256) {
|
||||||
|
storeHistory.splice(0, 256 - storeHistory.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function wrappedAction(...args) {
|
function wrappedAction(...args) {
|
||||||
let name, func;
|
let name, func;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { observable } from "mobx";
|
||||||
|
|
||||||
|
// 非持久化全局状态
|
||||||
|
let globalState = observable({
|
||||||
|
// 当前选中的符号
|
||||||
|
selectedNotationKey: null,
|
||||||
|
// 当前点击事件是否应该让符号失去焦点。应该在符号自身和菜单等点击事件中将其置为false
|
||||||
|
shouldNotationBlurAfterClick: true,
|
||||||
|
// 正在操作的连音线起点
|
||||||
|
tieSourceKey: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default globalState;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import store from "../store/global";
|
||||||
|
import { VERSION } from "./version";
|
||||||
|
|
||||||
|
const initialData = {
|
||||||
|
version: VERSION,
|
||||||
|
canvasWidth: 1024,
|
||||||
|
canvasHeight: 1448,
|
||||||
|
defaultFontSize: 16,
|
||||||
|
defaultSubFontSize: 12,
|
||||||
|
title: "无标题",
|
||||||
|
tone: "C",
|
||||||
|
marginHorizontal: 64,
|
||||||
|
marginTop: 32,
|
||||||
|
gapAfterTitle: 16,
|
||||||
|
gapAfterHeader: 32,
|
||||||
|
gapBetweenParagraph: 32,
|
||||||
|
gapBetweenNotation: 8,
|
||||||
|
beat: [4, 4],
|
||||||
|
speed: 75,
|
||||||
|
authors: ["佚名 作词", "简谱编辑器 制谱"],
|
||||||
|
notations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getInitialGlobalData() {
|
||||||
|
return JSON.parse(JSON.stringify(initialData));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据key查找符号在全局记录中的位置
|
||||||
|
function findParagraphAndNotation(key) {
|
||||||
|
let res = {};
|
||||||
|
const paras = store.paragraphs || [];
|
||||||
|
paras.some((p, i) => {
|
||||||
|
const notationIndex = (p.notations || []).findIndex((n) => n.key === key);
|
||||||
|
if (notationIndex !== -1) {
|
||||||
|
res.notation = p.notations[notationIndex];
|
||||||
|
res.notationIndex = notationIndex;
|
||||||
|
res.paragraph = p;
|
||||||
|
res.paragraphIndex = i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getInitialGlobalData, findParagraphAndNotation };
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
const notations = {
|
||||||
|
zero: "0",
|
||||||
|
do: "1",
|
||||||
|
re: "2",
|
||||||
|
mi: "3",
|
||||||
|
fa: "4",
|
||||||
|
sol: "5",
|
||||||
|
la: "6",
|
||||||
|
si: "7",
|
||||||
|
extend: "─",
|
||||||
|
separator: "│",
|
||||||
|
};
|
||||||
|
const notes = [
|
||||||
|
notations.zero,
|
||||||
|
notations.do,
|
||||||
|
notations.re,
|
||||||
|
notations.mi,
|
||||||
|
notations.fa,
|
||||||
|
notations.sol,
|
||||||
|
notations.la,
|
||||||
|
notations.si,
|
||||||
|
];
|
||||||
|
const separators = ["│", "‖"];
|
||||||
|
|
||||||
|
function isNote(notationOrNote) {
|
||||||
|
return notes.includes(notationOrNote?.note || notationOrNote);
|
||||||
|
}
|
||||||
|
function isSeparator(notationOrNote) {
|
||||||
|
return separators.includes(notationOrNote?.note || notationOrNote);
|
||||||
|
}
|
||||||
|
// 创建符号
|
||||||
|
function createNotation(initial) {
|
||||||
|
const n = {
|
||||||
|
key: `n_${String(Math.random())}`,
|
||||||
|
note: "0",
|
||||||
|
octave: 0,
|
||||||
|
dotted: false,
|
||||||
|
underline: 0,
|
||||||
|
breakUnderline: false,
|
||||||
|
prefixSups: [],
|
||||||
|
topDecorators: [],
|
||||||
|
tieTo: null,
|
||||||
|
};
|
||||||
|
if (initial) {
|
||||||
|
Object.assign(n, initial);
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { notations, isNote, isSeparator, createNotation };
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
const Notes = {
|
|
||||||
do: "1",
|
|
||||||
re: "2",
|
|
||||||
mi: "3",
|
|
||||||
fa: "4",
|
|
||||||
sol: "5",
|
|
||||||
la: "6",
|
|
||||||
si: "7",
|
|
||||||
extend: "─",
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Notes };
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 新建段落
|
||||||
|
function createParagraph(initial) {
|
||||||
|
const p = {
|
||||||
|
key: `p_${String(Math.random())}`,
|
||||||
|
// 符号列表
|
||||||
|
notations: [],
|
||||||
|
// 是否两端对齐
|
||||||
|
alignJustify: null,
|
||||||
|
};
|
||||||
|
if (initial) {
|
||||||
|
Object.assign(p, initial);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createParagraph };
|
||||||
+16
-2
@@ -73,6 +73,9 @@ const placement = observable({
|
|||||||
return 8;
|
return 8;
|
||||||
},
|
},
|
||||||
get maxTieHeight() {
|
get maxTieHeight() {
|
||||||
|
return 20;
|
||||||
|
},
|
||||||
|
get maxTieBezierOffset() {
|
||||||
return 25;
|
return 25;
|
||||||
},
|
},
|
||||||
get underlineStepOffsetY() {
|
get underlineStepOffsetY() {
|
||||||
@@ -136,9 +139,16 @@ const placement = observable({
|
|||||||
const calcTextWidth = _calcTextWidth.bind(null, store.defaultFontSize);
|
const calcTextWidth = _calcTextWidth.bind(null, store.defaultFontSize);
|
||||||
const calcSubTextWidth = _calcTextWidth.bind(null, store.defaultSubFontSize);
|
const calcSubTextWidth = _calcTextWidth.bind(null, store.defaultSubFontSize);
|
||||||
|
|
||||||
// 计算段落行高
|
// 计算段落占的空间高
|
||||||
function calcParagraphHeight(paragraph) {
|
function calcParagraphHeight(paragraph) {
|
||||||
|
return calcParagraphContentHeight(paragraph) + store.gapBetweenParagraph;
|
||||||
|
}
|
||||||
|
// 计算段落内容的高度
|
||||||
|
function calcParagraphContentHeight(paragraph) {
|
||||||
const notations = paragraph.notations || [];
|
const notations = paragraph.notations || [];
|
||||||
|
if (notations.length === 0) {
|
||||||
|
return placement.xHeight;
|
||||||
|
}
|
||||||
let tieHeight = 0;
|
let tieHeight = 0;
|
||||||
if (_hasTie(paragraph)) {
|
if (_hasTie(paragraph)) {
|
||||||
tieHeight = placement.maxTieHeight;
|
tieHeight = placement.maxTieHeight;
|
||||||
@@ -150,7 +160,7 @@ function calcParagraphHeight(paragraph) {
|
|||||||
const belowOffsetMap = noteHeightMap.map((v, i) => v - aboveOffsetMap[i]);
|
const belowOffsetMap = noteHeightMap.map((v, i) => v - aboveOffsetMap[i]);
|
||||||
const maxNoteOffset =
|
const maxNoteOffset =
|
||||||
Math.max(...belowOffsetMap) + Math.max(...aboveOffsetMap);
|
Math.max(...belowOffsetMap) + Math.max(...aboveOffsetMap);
|
||||||
return tieHeight + maxNoteOffset + store.gapBetweenParagraph;
|
return tieHeight + maxNoteOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算一行中所有音符的宽度和
|
// 计算一行中所有音符的宽度和
|
||||||
@@ -169,6 +179,9 @@ function calcParagraphWidth(paragraph) {
|
|||||||
// 计算段落中的音符中心以上偏移量的最大值
|
// 计算段落中的音符中心以上偏移量的最大值
|
||||||
function calcParagraphAboveOffset(paragraph) {
|
function calcParagraphAboveOffset(paragraph) {
|
||||||
const notations = paragraph.notations || [];
|
const notations = paragraph.notations || [];
|
||||||
|
if (notations.length === 0) {
|
||||||
|
return placement.xHeight / 2;
|
||||||
|
}
|
||||||
let tieHeight = 0;
|
let tieHeight = 0;
|
||||||
if (_hasTie(paragraph)) {
|
if (_hasTie(paragraph)) {
|
||||||
tieHeight = placement.maxTieHeight;
|
tieHeight = placement.maxTieHeight;
|
||||||
@@ -270,6 +283,7 @@ export {
|
|||||||
calcNotationAboveOffset,
|
calcNotationAboveOffset,
|
||||||
calcParagraphWidth,
|
calcParagraphWidth,
|
||||||
calcParagraphHeight,
|
calcParagraphHeight,
|
||||||
|
calcParagraphContentHeight,
|
||||||
calcParagraphAboveOffset,
|
calcParagraphAboveOffset,
|
||||||
calcTextWidth,
|
calcTextWidth,
|
||||||
calcSubTextWidth,
|
calcSubTextWidth,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const VERSION = 1;
|
||||||
|
|
||||||
|
export { VERSION };
|
||||||
@@ -2,22 +2,29 @@ import { Button, Dropdown } from "antd";
|
|||||||
import {
|
import {
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
|
RetweetOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
} 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 { useEffect, useState } from "react";
|
||||||
import store from "../../store/global";
|
import store from "../../store/global";
|
||||||
import P, {
|
import P, {
|
||||||
calcParagraphAboveOffset,
|
calcParagraphAboveOffset,
|
||||||
calcParagraphHeight,
|
calcParagraphHeight,
|
||||||
} from "../../util/placement";
|
} from "../../util/placement";
|
||||||
|
import {
|
||||||
|
convertMenu,
|
||||||
|
editMenu,
|
||||||
|
fileMenu,
|
||||||
|
handleClick,
|
||||||
|
handleKeyPress,
|
||||||
|
} from "../../menu/editor";
|
||||||
import Canvas from "../Canvas";
|
import Canvas from "../Canvas";
|
||||||
import ConfigModal from "../ConfigModal";
|
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() {
|
||||||
@@ -36,6 +43,15 @@ function Editor() {
|
|||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener("keydown", handleKeyPress);
|
||||||
|
document.addEventListener("click", handleClick);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyPress);
|
||||||
|
document.removeEventListener("click", handleClick);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={Styles.container}
|
className={Styles.container}
|
||||||
@@ -57,7 +73,7 @@ function Editor() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown overlay={convertMenu} placement="bottomLeft">
|
<Dropdown overlay={convertMenu} placement="bottomLeft">
|
||||||
<Button icon={<SyncOutlined />} type="text">
|
<Button icon={<RetweetOutlined />} type="text">
|
||||||
转调
|
转调
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ const handleChangeAuthor = wrappedAction((index, value) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function RightInfoBlock() {
|
function RightInfoBlock() {
|
||||||
// TODO: working
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
type="authors"
|
type="authors"
|
||||||
|
|||||||
+76
-12
@@ -1,12 +1,23 @@
|
|||||||
|
import { action } from "mobx";
|
||||||
|
import { message } from "antd";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import EditableContent from "../../component/EditableContent";
|
||||||
|
import state from "../../store/state";
|
||||||
import store from "../../store/global";
|
import store from "../../store/global";
|
||||||
import P, {
|
import P, {
|
||||||
calcNotationPrefixOffset,
|
calcNotationPrefixOffset,
|
||||||
calcSubTextWidth,
|
calcSubTextWidth,
|
||||||
} from "../../util/placement";
|
} from "../../util/placement";
|
||||||
import { Notes } from "../../util/note";
|
import { findParagraphAndNotation } from "../../util/editor";
|
||||||
|
import {
|
||||||
|
getNotationContextItems,
|
||||||
|
handleNotationContext,
|
||||||
|
} from "../../menu/notation";
|
||||||
|
import { notations } from "../../util/notation";
|
||||||
|
import { wrappedAction } from "../../store/history";
|
||||||
import Row from "../Row";
|
import Row from "../Row";
|
||||||
import Text from "../Text";
|
import Text from "../Text";
|
||||||
|
import Styles from "./index.module.css";
|
||||||
|
|
||||||
function composeArray(octave) {
|
function composeArray(octave) {
|
||||||
const len = Math.abs(Number(octave));
|
const len = Math.abs(Number(octave));
|
||||||
@@ -16,7 +27,35 @@ function composeArray(octave) {
|
|||||||
return Array(len).fill(0, 0, len);
|
return Array(len).fill(0, 0, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Notation({ offsetX, notation }) {
|
function Notation({ offsetX, notation, paragraph }) {
|
||||||
|
const handleClick = wrappedAction(() => {
|
||||||
|
state.selectedNotationKey = notation.key;
|
||||||
|
state.shouldNotationBlurAfterClick = false;
|
||||||
|
if (state.tieSourceKey) {
|
||||||
|
// 处理连音线
|
||||||
|
if (state.tieSourceKey === notation.key) {
|
||||||
|
state.tieSourceKey = null;
|
||||||
|
// NOTICE: 如果要添加其他逻辑注意这里的return
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
notation: sourceNotation,
|
||||||
|
paragraph: sourceParagraph,
|
||||||
|
} = findParagraphAndNotation(state.tieSourceKey);
|
||||||
|
if (!sourceNotation) {
|
||||||
|
state.tieSourceKey = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sourceParagraph.notations.includes(notation)) {
|
||||||
|
sourceNotation.tieTo = notation.key;
|
||||||
|
notation.tieTo = sourceNotation.key;
|
||||||
|
} else {
|
||||||
|
message.warn("只允许相同段落的音符相连!");
|
||||||
|
}
|
||||||
|
state.tieSourceKey = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const underlineOffset = P.underlineStepOffsetY * (notation.underline | 0);
|
const underlineOffset = P.underlineStepOffsetY * (notation.underline | 0);
|
||||||
const octaveInitialOffset =
|
const octaveInitialOffset =
|
||||||
notation.octave > 0
|
notation.octave > 0
|
||||||
@@ -65,7 +104,7 @@ function Notation({ offsetX, notation }) {
|
|||||||
|
|
||||||
const renderNote = () => {
|
const renderNote = () => {
|
||||||
let transform;
|
let transform;
|
||||||
if (notation.note === Notes.extend) {
|
if (notation.note === notations.extend) {
|
||||||
// 延音符太长了缩短一点
|
// 延音符太长了缩短一点
|
||||||
transform = "scale(0.8, 1)";
|
transform = "scale(0.8, 1)";
|
||||||
}
|
}
|
||||||
@@ -83,21 +122,46 @@ function Notation({ offsetX, notation }) {
|
|||||||
type="octave"
|
type="octave"
|
||||||
cx="0"
|
cx="0"
|
||||||
cy={octaveInitialOffset + octaveStepOffset * i}
|
cy={octaveInitialOffset + octaveStepOffset * i}
|
||||||
|
fill="currentColor"
|
||||||
r="2"
|
r="2"
|
||||||
></circle>
|
></circle>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const options = getNotationContextItems(notation, paragraph);
|
||||||
return (
|
return (
|
||||||
<Row type="notation" offsetX={offsetX}>
|
<EditableContent
|
||||||
{renderPrefixSups()}
|
inputType="select"
|
||||||
{renderTopDecorators()}
|
options={options}
|
||||||
{renderNote()}
|
popoverProps={{ trigger: "context" }}
|
||||||
{notation.dotted && (
|
onChange={handleNotationContext.bind(null, options)}
|
||||||
<circle type="dot" cx={P.xWidth + 4} cy="-2" r="2"></circle>
|
>
|
||||||
)}
|
<Row
|
||||||
{renderOctave()}
|
editable
|
||||||
</Row>
|
type="notation"
|
||||||
|
offsetX={offsetX}
|
||||||
|
onClick={handleClick}
|
||||||
|
className={
|
||||||
|
state.selectedNotationKey === notation.key
|
||||||
|
? Styles.selectedNotation
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{renderPrefixSups()}
|
||||||
|
{renderTopDecorators()}
|
||||||
|
{renderNote()}
|
||||||
|
{notation.dotted && (
|
||||||
|
<circle
|
||||||
|
type="dot"
|
||||||
|
cx={P.xWidth + 4}
|
||||||
|
cy="-2"
|
||||||
|
r="2"
|
||||||
|
fill="currentColor"
|
||||||
|
></circle>
|
||||||
|
)}
|
||||||
|
{renderOctave()}
|
||||||
|
</Row>
|
||||||
|
</EditableContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.selectedNotation {
|
||||||
|
color: coral;
|
||||||
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
import { useMemo } from "react";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import EditableContent from "../../component/EditableContent";
|
||||||
import P, {
|
import P, {
|
||||||
calcNotationAboveOffset,
|
calcNotationAboveOffset,
|
||||||
calcNotationPrefixOffset,
|
calcNotationPrefixOffset,
|
||||||
calcNotationWidth,
|
calcNotationWidth,
|
||||||
calcParagraphAboveOffset,
|
calcParagraphAboveOffset,
|
||||||
calcParagraphHeight,
|
calcParagraphContentHeight,
|
||||||
calcParagraphWidth,
|
calcParagraphWidth,
|
||||||
} from "../../util/placement";
|
} from "../../util/placement";
|
||||||
|
import {
|
||||||
|
getParagraphMenuOptions,
|
||||||
|
handleSelectParagraphMenu,
|
||||||
|
} from "../../menu/paragraph";
|
||||||
import Notation from "../Notation";
|
import Notation from "../Notation";
|
||||||
import Row from "../Row";
|
import Row from "../Row";
|
||||||
|
|
||||||
@@ -34,6 +39,7 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
|
|||||||
return width;
|
return width;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 缓存音符偏移量
|
||||||
const noteOffsets = notations.map((_, i) => {
|
const noteOffsets = notations.map((_, i) => {
|
||||||
return (
|
return (
|
||||||
// 渲染音符时坐标为其中心,故偏移半个音符宽
|
// 渲染音符时坐标为其中心,故偏移半个音符宽
|
||||||
@@ -116,10 +122,11 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
|
|||||||
0.0062948773732605465 * x * x +
|
0.0062948773732605465 * x * x +
|
||||||
0.6378785891964498 * x -
|
0.6378785891964498 * x -
|
||||||
0.6843414475200537;
|
0.6843414475200537;
|
||||||
y = Math.max(Math.min(y, P.maxTieHeight), P.minTieHeight);
|
y = Math.max(Math.min(y, P.maxTieBezierOffset), P.minTieHeight);
|
||||||
const bezierY = offsetY - y;
|
const bezierY = offsetY - y;
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
|
key={`${fromIndex}_${toIndex}_${offsetY}`}
|
||||||
d={`M${bezierX1} ${offsetY} C${bezierX2} ${bezierY} ${bezierX3} ${bezierY} ${bezierX4} ${offsetY}`}
|
d={`M${bezierX1} ${offsetY} C${bezierX2} ${bezierY} ${bezierX3} ${bezierY} ${bezierX4} ${offsetY}`}
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -154,15 +161,40 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
|
|||||||
return ties;
|
return ties;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderParagraphMask = () => {
|
||||||
|
const height = calcParagraphContentHeight(paragraph);
|
||||||
|
return (
|
||||||
|
<EditableContent
|
||||||
|
inputType="select"
|
||||||
|
options={getParagraphMenuOptions(paragraph)}
|
||||||
|
onChange={handleSelectParagraphMenu}
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x={-P.xWidth / 2}
|
||||||
|
y={-calcParagraphAboveOffset(paragraph)}
|
||||||
|
width={P.maxContentWidth}
|
||||||
|
height={height}
|
||||||
|
fill="transparent"
|
||||||
|
></rect>
|
||||||
|
</EditableContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row type="paragraph" offsetY={offsetY}>
|
<Row type="paragraph" offsetY={offsetY}>
|
||||||
|
{renderParagraphMask()}
|
||||||
{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}
|
||||||
|
paragraph={paragraph}
|
||||||
|
offsetX={noteOffsets[i]}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
{renderUnderlines()}
|
{renderUnderlines()}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Paragraph;
|
export default observer(Paragraph);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function Text({ children, editable, ...props }) {
|
|||||||
dominantBaseline="hanging"
|
dominantBaseline="hanging"
|
||||||
stroke="none"
|
stroke="none"
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
fill="black"
|
fill="currentColor"
|
||||||
dangerouslySetInnerHTML={{ __html: escapeHtml(children) }}
|
dangerouslySetInnerHTML={{ __html: escapeHtml(children) }}
|
||||||
{...props}
|
{...props}
|
||||||
></text>
|
></text>
|
||||||
@@ -36,7 +36,7 @@ export default function Text({ children, editable, ...props }) {
|
|||||||
dominantBaseline="hanging"
|
dominantBaseline="hanging"
|
||||||
stroke="none"
|
stroke="none"
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
fill="black"
|
fill="currentColor"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
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