From 615ba6ffbaa89a1fe595b89e384f571bac3f8b5a Mon Sep 17 00:00:00 2001 From: mattuy Date: Wed, 29 Sep 2021 22:32:20 +0800 Subject: [PATCH] daily: 2021-09-29 Night --- src/component/EditableContent/index.js | 29 ++-- src/component/PopoverOnSvg/index.js | 12 +- src/menu/editor.js | 220 ++++++++++++++++++++++++ src/menu/notation.js | 75 ++++++++ src/menu/paragraph.js | 57 ++++++ src/store/global.js | 12 +- src/store/history.js | 58 +++++-- src/store/state.js | 13 ++ src/util/editor.js | 45 +++++ src/util/notation.js | 50 ++++++ src/util/note.js | 12 -- src/util/paragraph.js | 16 ++ src/util/placement.js | 18 +- src/util/version.js | 3 + src/view/Editor/index.js | 22 ++- src/view/Header/RightInfoBlock/index.js | 1 - src/view/Notation/index.js | 88 ++++++++-- src/view/Notation/index.module.css | 3 + src/view/Paragraph/index.js | 42 ++++- src/view/Text/index.js | 4 +- src/view/menu/editor.js | 97 ----------- 21 files changed, 709 insertions(+), 168 deletions(-) create mode 100644 src/menu/editor.js create mode 100644 src/menu/notation.js create mode 100644 src/menu/paragraph.js create mode 100644 src/store/state.js create mode 100644 src/util/editor.js create mode 100644 src/util/notation.js delete mode 100644 src/util/note.js create mode 100644 src/util/paragraph.js create mode 100644 src/util/version.js create mode 100644 src/view/Notation/index.module.css delete mode 100644 src/view/menu/editor.js diff --git a/src/component/EditableContent/index.js b/src/component/EditableContent/index.js index 7659b71..8c36a5c 100644 --- a/src/component/EditableContent/index.js +++ b/src/component/EditableContent/index.js @@ -70,18 +70,21 @@ const EditableContent = function ( const renderSelectionPopover = useCallback(() => { return ( - {(options || []).map((option) => ( - handleConfirm(option.key)} - > - {option.text} - - ))} + {(options || []) + .filter((op) => op.visible !== false) + .map((option) => ( + handleConfirm(option.key)} + > + {option.text} + + ))} ); }, [initialValue, options, handleConfirm]); @@ -98,7 +101,7 @@ const EditableContent = function ( return ( -
+ {showArrow && ( +
+ )} {title && (
{title} diff --git a/src/menu/editor.js b/src/menu/editor.js new file mode 100644 index 0000000..29b7dd9 --- /dev/null +++ b/src/menu/editor.js @@ -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 = ( + + }> + 新建 + + }> + 打开 + + }> + 保存 + + +); +const editMenu = ( + + 撤销 + 重做 + 添加段落 + 重置歌曲名称 + 重置作者信息 + +); + +const convertMenu = ( + + + 1 = C + 1 = D + 1 = E + 1 = F + 1 = G + 1 = A + 1 = B + + 1 = C + + + 1 = D + + + 1 = E + + + 1 = F + + + 1 = G + + + 1 = A + + + 1 = B + + + 升高一个音 + 降低一个音 + 升高一个八度 + 降低一个八度 + +); + +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 }; diff --git a/src/menu/notation.js b/src/menu/notation.js new file mode 100644 index 0000000..0685965 --- /dev/null +++ b/src/menu/notation.js @@ -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: 改变组件这个反人类的回调执行 +// 执行选项回调。。 +function handleNotationContext(options, key) { + runInWrappedAction(() => (state.shouldNotationBlurAfterClick = false)); + const callback = options.find((ops) => ops.key === key)?.onClick; + callback && callback(); +} + +export { getNotationContextItems, handleNotationContext }; diff --git a/src/menu/paragraph.js b/src/menu/paragraph.js new file mode 100644 index 0000000..fabdb7f --- /dev/null +++ b/src/menu/paragraph.js @@ -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 ( + + }> + 添加符号 + + }> + 添加段落 + + }> + 自动 + 启用 + 禁用 + + }> + 删除段落 + + + ); +} + +// 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 }; diff --git a/src/store/global.js b/src/store/global.js index b4ae971..0ba01a5 100644 --- a/src/store/global.js +++ b/src/store/global.js @@ -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 }; diff --git a/src/store/history.js b/src/store/history.js index 461bc21..392c2ee 100644 --- a/src/store/history.js +++ b/src/store/history.js @@ -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; diff --git a/src/store/state.js b/src/store/state.js new file mode 100644 index 0000000..8125d6b --- /dev/null +++ b/src/store/state.js @@ -0,0 +1,13 @@ +import { observable } from "mobx"; + +// 非持久化全局状态 +let globalState = observable({ + // 当前选中的符号 + selectedNotationKey: null, + // 当前点击事件是否应该让符号失去焦点。应该在符号自身和菜单等点击事件中将其置为false + shouldNotationBlurAfterClick: true, + // 正在操作的连音线起点 + tieSourceKey: null, +}); + +export default globalState; diff --git a/src/util/editor.js b/src/util/editor.js new file mode 100644 index 0000000..f3c3139 --- /dev/null +++ b/src/util/editor.js @@ -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 }; diff --git a/src/util/notation.js b/src/util/notation.js new file mode 100644 index 0000000..ad475c0 --- /dev/null +++ b/src/util/notation.js @@ -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 }; diff --git a/src/util/note.js b/src/util/note.js deleted file mode 100644 index f4f4724..0000000 --- a/src/util/note.js +++ /dev/null @@ -1,12 +0,0 @@ -const Notes = { - do: "1", - re: "2", - mi: "3", - fa: "4", - sol: "5", - la: "6", - si: "7", - extend: "─", -}; - -export { Notes }; diff --git a/src/util/paragraph.js b/src/util/paragraph.js new file mode 100644 index 0000000..04cf667 --- /dev/null +++ b/src/util/paragraph.js @@ -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 }; diff --git a/src/util/placement.js b/src/util/placement.js index 6d32f13..ce453b9 100644 --- a/src/util/placement.js +++ b/src/util/placement.js @@ -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, diff --git a/src/util/version.js b/src/util/version.js new file mode 100644 index 0000000..285e8d4 --- /dev/null +++ b/src/util/version.js @@ -0,0 +1,3 @@ +const VERSION = 1; + +export { VERSION }; diff --git a/src/view/Editor/index.js b/src/view/Editor/index.js index efdb396..80c48a6 100644 --- a/src/view/Editor/index.js +++ b/src/view/Editor/index.js @@ -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 (
- diff --git a/src/view/Header/RightInfoBlock/index.js b/src/view/Header/RightInfoBlock/index.js index 7869987..1ec64f4 100644 --- a/src/view/Header/RightInfoBlock/index.js +++ b/src/view/Header/RightInfoBlock/index.js @@ -66,7 +66,6 @@ const handleChangeAuthor = wrappedAction((index, value) => { }); function RightInfoBlock() { - // TODO: working return ( { + 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" > )); }; + const options = getNotationContextItems(notation, paragraph); return ( - - {renderPrefixSups()} - {renderTopDecorators()} - {renderNote()} - {notation.dotted && ( - - )} - {renderOctave()} - + + + {renderPrefixSups()} + {renderTopDecorators()} + {renderNote()} + {notation.dotted && ( + + )} + {renderOctave()} + + ); } diff --git a/src/view/Notation/index.module.css b/src/view/Notation/index.module.css new file mode 100644 index 0000000..3b78966 --- /dev/null +++ b/src/view/Notation/index.module.css @@ -0,0 +1,3 @@ +.selectedNotation { + color: coral; +} \ No newline at end of file diff --git a/src/view/Paragraph/index.js b/src/view/Paragraph/index.js index 281fd44..ee1cfab 100644 --- a/src/view/Paragraph/index.js +++ b/src/view/Paragraph/index.js @@ -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 ( { + const height = calcParagraphContentHeight(paragraph); + return ( + + + + ); + }; + return ( + {renderParagraphMask()} {renderTies()} {paragraph.notations.map((n, i) => ( - + ))} {renderUnderlines()} ); } -export default Paragraph; +export default observer(Paragraph); diff --git a/src/view/Text/index.js b/src/view/Text/index.js index 4bbc7ae..24f4df3 100644 --- a/src/view/Text/index.js +++ b/src/view/Text/index.js @@ -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} > @@ -36,7 +36,7 @@ export default function Text({ children, editable, ...props }) { dominantBaseline="hanging" stroke="none" fontWeight="bold" - fill="black" + fill="currentColor" {...props} > {children} diff --git a/src/view/menu/editor.js b/src/view/menu/editor.js deleted file mode 100644 index 3f6d632..0000000 --- a/src/view/menu/editor.js +++ /dev/null @@ -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 = ( - - }> - 新建 - - }> - 打开 - - }> - 保存 - - -); -const editMenu = ( - - 撤销 - 重做 - 重置歌曲名称 - 重置作者信息 - -); - -const convertMenu = ( - - - 1 = C - 1 = D - 1 = E - 1 = F - 1 = G - 1 = A - 1 = B - - 1 = C - - - 1 = D - {" "} - - 1 = E - {" "} - - 1 = F - {" "} - - 1 = G - {" "} - - 1 = A - - - 1 = B - - - 升高一个音 - 降低一个音 - 升高一个八度 - 降低一个八度 - -); - -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 };