daily: 2021-09-29 Night

This commit is contained in:
2021-09-29 22:32:20 +08:00
parent 488a2532b2
commit 615ba6ffba
21 changed files with 709 additions and 168 deletions
+16 -13
View File
@@ -70,18 +70,21 @@ const EditableContent = function (
const renderSelectionPopover = useCallback(() => {
return (
<Menu className={Styles.menu}>
{(options || []).map((option) => (
<Menu.Item
icon={option.icon}
key={option.key}
className={
Styles.menuItem + (option.key === initialValue ? " selected" : "")
}
onClick={() => handleConfirm(option.key)}
>
{option.text}
</Menu.Item>
))}
{(options || [])
.filter((op) => op.visible !== false)
.map((option) => (
<Menu.Item
icon={option.icon}
key={option.key}
className={
Styles.menuItem +
(option.key === initialValue ? " selected" : "")
}
onClick={() => handleConfirm(option.key)}
>
{option.text}
</Menu.Item>
))}
</Menu>
);
}, [initialValue, options, handleConfirm]);
@@ -98,7 +101,7 @@ const EditableContent = function (
return (
<PopoverOnSvg
trigger="click"
trigger={inputType === "select" ? "context" : "click"}
title={title}
renderContent={renderContent}
renderPopover={renderPopover}
+6 -6
View File
@@ -17,7 +17,6 @@ function PopoverOnSvg({
trigger,
visible,
autoHide,
// TODO: complete this
showArrow,
onVisibilityChange,
}) {
@@ -199,7 +198,6 @@ function PopoverOnSvg({
makeOffset(getContainerOffset().x, getContainerOffset().y);
mergedStyles.transform += ` translate(${offsetX}px, ${offsetY}px)`;
} else {
//TODO:
mergedStyles.left = lastContextCord.x;
mergedStyles.top = lastContextCord.y;
mergedStyles.transform = `translate(${offsetX}px, ${offsetY}px)`;
@@ -242,10 +240,12 @@ function PopoverOnSvg({
)
) : (
<>
<div
className={arrowClassNames.join(" ")}
style={arrowStyles}
></div>
{showArrow && (
<div
className={arrowClassNames.join(" ")}
style={arrowStyles}
></div>
)}
{title && (
<header className={headerClassNames.join(" ")}>
{title}
+220
View File
@@ -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 };
+75
View File
@@ -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 };
+57
View File
@@ -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
View File
@@ -1,7 +1,10 @@
import React from "react";
import { observable } from "mobx";
import { createNotation } from "../util/notation";
// 全局数据及配置,需要持久化
let globalStore = observable({
version: 1,
canvasWidth: 1024,
canvasHeight: 1448,
defaultFontSize: 16,
@@ -12,7 +15,7 @@ let globalStore = observable({
marginTop: 32,
gapAfterTitle: 16,
gapAfterHeader: 32,
gapBetweenParagraph: 0,
gapBetweenParagraph: 32,
gapBetweenNotation: 8,
beat: [4, 4],
speed: 75,
@@ -92,7 +95,7 @@ let globalStore = observable({
underline: 0,
dotted: false,
},
].map((obj) => ((obj.key = obj.key || Math.random()), obj)),
].map((obj) => createNotation(obj)),
},
{
notations: [
@@ -113,7 +116,7 @@ let globalStore = observable({
octave: 0,
dotted: false,
},
].map((obj) => ((obj.key = obj.key || Math.random()), obj)),
].map((obj) => createNotation(obj)),
alignJustify: false,
},
{
@@ -140,13 +143,12 @@ let globalStore = observable({
octave: 0,
dotted: false,
},
].map((obj) => ((obj.key = Math.random()), obj)),
].map((obj) => createNotation(obj)),
},
].map((obj) => ((obj.key = Math.random()), obj)),
});
const GlobalContext = React.createContext(globalStore);
export default globalStore;
export { GlobalContext };
+48 -10
View File
@@ -1,13 +1,41 @@
import { action, intercept, runInAction, toJS } from "mobx";
import { action, intercept, keys, runInAction, toJS } from "mobx";
import globalStore from "./global";
const storeHistory = [];
let cursor = -1;
intercept(globalStore, (change) => {
storeHistory.push(toJS(globalStore));
return change;
});
// ENHANCE: 使用性能更好的方案
// 比较历史记录
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) {
@@ -15,21 +43,31 @@ function go(span) {
Math.max(0, cursor + span),
storeHistory.length - 1
);
console.log("go ", span);
if (newCursor === cursor) {
return;
}
cursor = newCursor;
console.log("gone to ", 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);
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) {
let name, func;
+13
View File
@@ -0,0 +1,13 @@
import { observable } from "mobx";
// 非持久化全局状态
let globalState = observable({
// 当前选中的符号
selectedNotationKey: null,
// 当前点击事件是否应该让符号失去焦点。应该在符号自身和菜单等点击事件中将其置为false
shouldNotationBlurAfterClick: true,
// 正在操作的连音线起点
tieSourceKey: null,
});
export default globalState;
+45
View File
@@ -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 };
+50
View File
@@ -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 };
-12
View File
@@ -1,12 +0,0 @@
const Notes = {
do: "1",
re: "2",
mi: "3",
fa: "4",
sol: "5",
la: "6",
si: "7",
extend: "─",
};
export { Notes };
+16
View File
@@ -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
View File
@@ -73,6 +73,9 @@ const placement = observable({
return 8;
},
get maxTieHeight() {
return 20;
},
get maxTieBezierOffset() {
return 25;
},
get underlineStepOffsetY() {
@@ -136,9 +139,16 @@ const placement = observable({
const calcTextWidth = _calcTextWidth.bind(null, store.defaultFontSize);
const calcSubTextWidth = _calcTextWidth.bind(null, store.defaultSubFontSize);
// 计算段落
// 计算段落占的空间
function calcParagraphHeight(paragraph) {
return calcParagraphContentHeight(paragraph) + store.gapBetweenParagraph;
}
// 计算段落内容的高度
function calcParagraphContentHeight(paragraph) {
const notations = paragraph.notations || [];
if (notations.length === 0) {
return placement.xHeight;
}
let tieHeight = 0;
if (_hasTie(paragraph)) {
tieHeight = placement.maxTieHeight;
@@ -150,7 +160,7 @@ function calcParagraphHeight(paragraph) {
const belowOffsetMap = noteHeightMap.map((v, i) => v - aboveOffsetMap[i]);
const maxNoteOffset =
Math.max(...belowOffsetMap) + Math.max(...aboveOffsetMap);
return tieHeight + maxNoteOffset + store.gapBetweenParagraph;
return tieHeight + maxNoteOffset;
}
// 计算一行中所有音符的宽度和
@@ -169,6 +179,9 @@ function calcParagraphWidth(paragraph) {
// 计算段落中的音符中心以上偏移量的最大值
function calcParagraphAboveOffset(paragraph) {
const notations = paragraph.notations || [];
if (notations.length === 0) {
return placement.xHeight / 2;
}
let tieHeight = 0;
if (_hasTie(paragraph)) {
tieHeight = placement.maxTieHeight;
@@ -270,6 +283,7 @@ export {
calcNotationAboveOffset,
calcParagraphWidth,
calcParagraphHeight,
calcParagraphContentHeight,
calcParagraphAboveOffset,
calcTextWidth,
calcSubTextWidth,
+3
View File
@@ -0,0 +1,3 @@
const VERSION = 1;
export { VERSION };
+19 -3
View File
@@ -2,22 +2,29 @@ import { Button, Dropdown } from "antd";
import {
EditOutlined,
FileTextOutlined,
RetweetOutlined,
SettingOutlined,
SyncOutlined,
} from "@ant-design/icons";
import { observer } from "mobx-react-lite";
import { useState } from "react";
import { useEffect, useState } from "react";
import store from "../../store/global";
import P, {
calcParagraphAboveOffset,
calcParagraphHeight,
} from "../../util/placement";
import {
convertMenu,
editMenu,
fileMenu,
handleClick,
handleKeyPress,
} from "../../menu/editor";
import Canvas from "../Canvas";
import ConfigModal from "../ConfigModal";
import Header from "../Header";
import Paragraph from "../Paragraph";
import Row from "../Row";
import { convertMenu, editMenu, fileMenu } from "../menu/editor";
import Styles from "./index.module.css";
function Editor() {
@@ -36,6 +43,15 @@ function Editor() {
return height;
}
useEffect(() => {
document.addEventListener("keydown", handleKeyPress);
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("keydown", handleKeyPress);
document.removeEventListener("click", handleClick);
};
}, []);
return (
<div
className={Styles.container}
@@ -57,7 +73,7 @@ function Editor() {
</Button>
</Dropdown>
<Dropdown overlay={convertMenu} placement="bottomLeft">
<Button icon={<SyncOutlined />} type="text">
<Button icon={<RetweetOutlined />} type="text">
转调
</Button>
</Dropdown>
-1
View File
@@ -66,7 +66,6 @@ const handleChangeAuthor = wrappedAction((index, value) => {
});
function RightInfoBlock() {
// TODO: working
return (
<Row
type="authors"
+76 -12
View File
@@ -1,12 +1,23 @@
import { action } from "mobx";
import { message } from "antd";
import { observer } from "mobx-react-lite";
import EditableContent from "../../component/EditableContent";
import state from "../../store/state";
import store from "../../store/global";
import P, {
calcNotationPrefixOffset,
calcSubTextWidth,
} 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 Text from "../Text";
import Styles from "./index.module.css";
function composeArray(octave) {
const len = Math.abs(Number(octave));
@@ -16,7 +27,35 @@ function composeArray(octave) {
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 octaveInitialOffset =
notation.octave > 0
@@ -65,7 +104,7 @@ function Notation({ offsetX, notation }) {
const renderNote = () => {
let transform;
if (notation.note === Notes.extend) {
if (notation.note === notations.extend) {
// 延音符太长了缩短一点
transform = "scale(0.8, 1)";
}
@@ -83,21 +122,46 @@ function Notation({ offsetX, notation }) {
type="octave"
cx="0"
cy={octaveInitialOffset + octaveStepOffset * i}
fill="currentColor"
r="2"
></circle>
));
};
const options = getNotationContextItems(notation, paragraph);
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>
<EditableContent
inputType="select"
options={options}
popoverProps={{ trigger: "context" }}
onChange={handleNotationContext.bind(null, options)}
>
<Row
editable
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>
);
}
+3
View File
@@ -0,0 +1,3 @@
.selectedNotation {
color: coral;
}
+37 -5
View File
@@ -1,12 +1,17 @@
import { useMemo } from "react";
import { observer } from "mobx-react-lite";
import EditableContent from "../../component/EditableContent";
import P, {
calcNotationAboveOffset,
calcNotationPrefixOffset,
calcNotationWidth,
calcParagraphAboveOffset,
calcParagraphHeight,
calcParagraphContentHeight,
calcParagraphWidth,
} from "../../util/placement";
import {
getParagraphMenuOptions,
handleSelectParagraphMenu,
} from "../../menu/paragraph";
import Notation from "../Notation";
import Row from "../Row";
@@ -34,6 +39,7 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
return width;
};
// 缓存音符偏移量
const noteOffsets = notations.map((_, i) => {
return (
// 渲染音符时坐标为其中心,故偏移半个音符宽
@@ -116,10 +122,11 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
0.0062948773732605465 * x * x +
0.6378785891964498 * x -
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;
return (
<path
key={`${fromIndex}_${toIndex}_${offsetY}`}
d={`M${bezierX1} ${offsetY} C${bezierX2} ${bezierY} ${bezierX3} ${bezierY} ${bezierX4} ${offsetY}`}
stroke="currentColor"
fill="none"
@@ -154,15 +161,40 @@ function Paragraph({ paragraph, offsetY, alignJustify }) {
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 (
<Row type="paragraph" offsetY={offsetY}>
{renderParagraphMask()}
{renderTies()}
{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()}
</Row>
);
}
export default Paragraph;
export default observer(Paragraph);
+2 -2
View File
@@ -16,7 +16,7 @@ export default function Text({ children, editable, ...props }) {
dominantBaseline="hanging"
stroke="none"
fontWeight="bold"
fill="black"
fill="currentColor"
dangerouslySetInnerHTML={{ __html: escapeHtml(children) }}
{...props}
></text>
@@ -36,7 +36,7 @@ export default function Text({ children, editable, ...props }) {
dominantBaseline="hanging"
stroke="none"
fontWeight="bold"
fill="black"
fill="currentColor"
{...props}
>
{children}
-97
View File
@@ -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 };