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