daily: 2021-09-27

This commit is contained in:
2021-09-27 23:17:38 +08:00
parent fb671cd62a
commit 6634024333
18 changed files with 600 additions and 224 deletions
+1 -76
View File
@@ -1,76 +1 @@
<<<<<<< HEAD # 一个基于svg构建的Web简谱编辑器
# numerical-notation-editor
一个基于svg构建的Web简谱编辑器
=======
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
>>>>>>> f03cfb9 (Initialize project using Create React App)
+15 -14
View File
@@ -8,6 +8,7 @@
"name": "numerical-notation-editor", "name": "numerical-notation-editor",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.7.0",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7", "@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3", "@testing-library/user-event": "^12.8.3",
@@ -29,12 +30,12 @@
} }
}, },
"node_modules/@ant-design/icons": { "node_modules/@ant-design/icons": {
"version": "4.6.4", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.6.4.tgz", "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.7.0.tgz",
"integrity": "sha512-li02J8ym721E24N3bw1oXRzFDV7m2MYQWs+WtJgVVjhNRv4sc6vL2a2M7SS8rWX3Uc/3GJfrokIJnMrmbIMuXQ==", "integrity": "sha512-aoB4Z7JA431rt6d4u+8xcNPPCrdufSRMUOpxa1ab6mz1JCQZOEVolj2WVs/tDFmN62zzK30mNelEsprLYsSF3g==",
"dependencies": { "dependencies": {
"@ant-design/colors": "^6.0.0", "@ant-design/colors": "^6.0.0",
"@ant-design/icons-svg": "^4.0.0", "@ant-design/icons-svg": "^4.2.1",
"@babel/runtime": "^7.11.2", "@babel/runtime": "^7.11.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"rc-util": "^5.9.4" "rc-util": "^5.9.4"
@@ -48,9 +49,9 @@
} }
}, },
"node_modules/@ant-design/icons-svg": { "node_modules/@ant-design/icons-svg": {
"version": "4.1.0", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz",
"integrity": "sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ==" "integrity": "sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw=="
}, },
"node_modules/@ant-design/react-slick": { "node_modules/@ant-design/react-slick": {
"version": "0.28.4", "version": "0.28.4",
@@ -22658,21 +22659,21 @@
} }
}, },
"@ant-design/icons": { "@ant-design/icons": {
"version": "4.6.4", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.6.4.tgz", "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.7.0.tgz",
"integrity": "sha512-li02J8ym721E24N3bw1oXRzFDV7m2MYQWs+WtJgVVjhNRv4sc6vL2a2M7SS8rWX3Uc/3GJfrokIJnMrmbIMuXQ==", "integrity": "sha512-aoB4Z7JA431rt6d4u+8xcNPPCrdufSRMUOpxa1ab6mz1JCQZOEVolj2WVs/tDFmN62zzK30mNelEsprLYsSF3g==",
"requires": { "requires": {
"@ant-design/colors": "^6.0.0", "@ant-design/colors": "^6.0.0",
"@ant-design/icons-svg": "^4.0.0", "@ant-design/icons-svg": "^4.2.1",
"@babel/runtime": "^7.11.2", "@babel/runtime": "^7.11.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"rc-util": "^5.9.4" "rc-util": "^5.9.4"
} }
}, },
"@ant-design/icons-svg": { "@ant-design/icons-svg": {
"version": "4.1.0", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz",
"integrity": "sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ==" "integrity": "sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw=="
}, },
"@ant-design/react-slick": { "@ant-design/react-slick": {
"version": "0.28.4", "version": "0.28.4",
+1
View File
@@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.7.0",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7", "@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3", "@testing-library/user-event": "^12.8.3",
+17 -11
View File
@@ -1,18 +1,22 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useImperativeHandle, useState } from "react";
import { Button, Input, Menu, Select } from "antd"; import { Button, Input, Menu, Select } from "antd";
import PopoverOnSvg from "../PopoverOnSvg"; import PopoverOnSvg from "../PopoverOnSvg";
import Styles from "./index.module.css"; import Styles from "./index.module.css";
function EditableContent({ const EditableContent = function (
children, { children, title, initialValue, inputType, options, onChange, popoverProps },
title, ref
initialValue, ) {
inputType,
options,
onChange,
}) {
const [inputValue, setInputValue] = useState(initialValue); const [inputValue, setInputValue] = useState(initialValue);
const [popoverVisible, setPopoverVisible] = useState(false); const [popoverVisible, setPopoverVisible] = useState(false);
useImperativeHandle(
ref,
() => ({
showPopover: setPopoverVisible.bind(null, true),
hidePopover: setPopoverVisible.bind(null, false),
}),
[]
);
const handleVisibilityChange = useCallback( const handleVisibilityChange = useCallback(
function (value) { function (value) {
setPopoverVisible(value); setPopoverVisible(value);
@@ -68,6 +72,7 @@ function EditableContent({
<Menu className={Styles.menu}> <Menu className={Styles.menu}>
{(options || []).map((option) => ( {(options || []).map((option) => (
<Menu.Item <Menu.Item
icon={option.icon}
key={option.key} key={option.key}
className={ className={
Styles.menuItem + (option.key === initialValue ? " selected" : "") Styles.menuItem + (option.key === initialValue ? " selected" : "")
@@ -100,10 +105,11 @@ function EditableContent({
visible={popoverVisible} visible={popoverVisible}
placement="bottom" placement="bottom"
onVisibilityChange={handleVisibilityChange} onVisibilityChange={handleVisibilityChange}
{...popoverProps}
> >
{children} {children}
</PopoverOnSvg> </PopoverOnSvg>
); );
} };
export default EditableContent; export default React.forwardRef(EditableContent);
+32
View File
@@ -0,0 +1,32 @@
let dirty = false;
let memoizedRect = { x: 0, y: 0 };
let container;
function getPopoverContainer() {
if (!container) {
container = document.createElement("div");
container.id = "svg-popover-container";
container.style = `
position: absolute;
left: 0;
top: 0;
`;
document.body.appendChild(container);
document.addEventListener("scroll", () => void (dirty = true));
}
return container;
}
function getContainerOffset() {
if (dirty) {
const rect = getPopoverContainer().getBoundingClientRect();
memoizedRect = {
x: -rect.x,
y: -rect.y,
};
dirty = false;
}
return memoizedRect;
}
export { getPopoverContainer, getContainerOffset };
+79 -80
View File
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import Styles from "./index.module.css"; import Styles from "./index.module.css";
import { getContainerOffset, getPopoverContainer } from "./container";
// ENHANCE: smart placement // ENHANCE: smart placement
function PopoverOnSvg({ function PopoverOnSvg({
@@ -20,7 +21,6 @@ function PopoverOnSvg({
showArrow, showArrow,
onVisibilityChange, onVisibilityChange,
}) { }) {
const containerRef = useRef();
const triggerBoxRef = useRef({ const triggerBoxRef = useRef({
x: 0, x: 0,
y: 0, y: 0,
@@ -38,6 +38,7 @@ function PopoverOnSvg({
// true, we'll update it after dom mounted, because we cannot calculate // true, we'll update it after dom mounted, because we cannot calculate
// correct position of popover before react mounting it. // correct position of popover before react mounting it.
const [innerVisible, setInnerVisible] = useState(false); const [innerVisible, setInnerVisible] = useState(false);
const [lastContextCord, setLastContextCord] = useState({ x: 0, y: 0 });
const [, forceUpdate] = useState(); const [, forceUpdate] = useState();
const popoverClassNames = [Styles.popover, globalClassName].filter(Boolean); const popoverClassNames = [Styles.popover, globalClassName].filter(Boolean);
const arrowClassNames = [Styles.arrow]; const arrowClassNames = [Styles.arrow];
@@ -74,8 +75,7 @@ function PopoverOnSvg({
); );
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(ev) => { (ev) => {
// TODO: working setLastContextCord({ x: ev.pageX, y: ev.pageY });
console.log(ev.pageX, ev.pageY)
ev.preventDefault(); ev.preventDefault();
changeVisibility(true); changeVisibility(true);
}, },
@@ -128,84 +128,82 @@ function PopoverOnSvg({
}, 0); }, 0);
} }
} }
if (!containerRef.current) { if (trigger !== "context") {
containerRef.current = document.body.querySelector( const box = triggerBoxRef.current;
"body > div#svg-popover-container" switch (true) {
); case /^(left|right)(top|center|bottom)?$/i.test(placement):
if (!containerRef.current) { mergedStyles.minHeight = "32px";
containerRef.current = document.createElement("div"); if (placement.startsWith("right")) {
containerRef.current.id = "svg-popover-container"; mergedStyles.left = box.right + "px";
document.body.appendChild(containerRef.current); } else {
mergedStyles.left = box.left + "px";
mergedStyles.transform += " translate(-100%, 0)";
}
if (placement.endsWith("top")) {
mergedStyles.top = box.top + "px";
mergedStyles.transform += " translate(0, 0)";
arrowStyles.top = "16px";
} else if (placement.endsWith("bottom")) {
mergedStyles.top = box.bottom + "px";
mergedStyles.transform += " translate(0, -100%)";
arrowStyles.bottom = "0";
} else {
mergedStyles.top = box.y + box.height / 2 + "px";
mergedStyles.transform += " translate(0, -50%)";
arrowStyles.top = "50%";
}
break;
case /^(top|bottom)(left|center|right)?$/i.test(placement):
default:
if (placement.startsWith("bottom")) {
mergedStyles.top = box.bottom + "px";
} else {
mergedStyles.top = box.y + "px";
mergedStyles.transform += " translate(0, -100%)";
}
if (placement.endsWith("left")) {
mergedStyles.left = box.left + "px";
arrowStyles.left = "16px";
} else if (placement.endsWith("right")) {
mergedStyles.left = box.right + "px";
mergedStyles.transform += " translate(-100%, 0)";
arrowStyles.right = "0";
} else {
mergedStyles.left = box.right - box.width / 2 + "px";
mergedStyles.transform += " translateX(-50%)";
arrowStyles.left = "50%";
}
break;
} }
switch (true) {
case !showArrow:
break;
case placement.startsWith("left"):
makeOffset(-8);
arrowClassNames.push("left");
break;
case placement.startsWith("right"):
makeOffset(8);
arrowClassNames.push("right");
break;
case placement.startsWith("bottom"):
makeOffset(0, 8);
arrowClassNames.push("bottom");
break;
case placement.startsWith("top"):
default:
makeOffset(0, -8);
arrowClassNames.push("top");
break;
}
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)`;
} }
const box = triggerBoxRef.current;
switch (true) {
case /^(left|right)(top|center|bottom)?$/i.test(placement):
mergedStyles.minHeight = "32px";
if (placement.startsWith("right")) {
mergedStyles.left = box.right + "px";
} else {
mergedStyles.left = box.left + "px";
mergedStyles.transform += " translate(-100%, 0)";
}
if (placement.endsWith("top")) {
mergedStyles.top = box.top + "px";
mergedStyles.transform += " translate(0, 0)";
arrowStyles.top = "16px";
} else if (placement.endsWith("bottom")) {
mergedStyles.top = box.bottom + "px";
mergedStyles.transform += " translate(0, -100%)";
arrowStyles.bottom = "0";
} else {
mergedStyles.top = box.y + box.height / 2 + "px";
mergedStyles.transform += " translate(0, -50%)";
arrowStyles.top = "50%";
}
break;
case /^(top|bottom)(left|center|right)?$/i.test(placement):
default:
if (placement.startsWith("bottom")) {
mergedStyles.top = box.bottom + "px";
} else {
mergedStyles.top = box.y + "px";
mergedStyles.transform += " translate(0, -100%)";
}
if (placement.endsWith("left")) {
mergedStyles.left = box.left + "px";
arrowStyles.left = "16px";
} else if (placement.endsWith("right")) {
mergedStyles.left = box.right + "px";
mergedStyles.transform += " translate(-100%, 0)";
arrowStyles.right = "0";
} else {
mergedStyles.left = box.right - box.width / 2 + "px";
mergedStyles.transform += " translateX(-50%)";
arrowStyles.left = "50%";
}
break;
}
switch (true) {
case !showArrow:
break;
case placement.startsWith("left"):
makeOffset(-8);
arrowClassNames.push("left");
break;
case placement.startsWith("right"):
makeOffset(8);
arrowClassNames.push("right");
break;
case placement.startsWith("bottom"):
makeOffset(0, 8);
arrowClassNames.push("bottom");
break;
case placement.startsWith("top"):
default:
makeOffset(0, -8);
arrowClassNames.push("top");
break;
}
mergedStyles.transform += ` translate(${offsetX}px, ${offsetY}px)`;
if (darkMode) { if (darkMode) {
popoverClassNames.push("dark"); popoverClassNames.push("dark");
arrowClassNames.push("dark"); arrowClassNames.push("dark");
@@ -261,7 +259,7 @@ function PopoverOnSvg({
</> </>
)} )}
</div>, </div>,
containerRef.current getPopoverContainer()
)} )}
</> </>
); );
@@ -270,6 +268,7 @@ function PopoverOnSvg({
PopoverOnSvg.defaultProps = { PopoverOnSvg.defaultProps = {
trigger: "click", trigger: "click",
placement: "top", placement: "top",
showArrow: true,
}; };
export default React.memo(PopoverOnSvg); export default React.memo(PopoverOnSvg);
+99 -2
View File
@@ -6,11 +6,108 @@ let globalStore = observable({
canvasHeight: 1448, canvasHeight: 1448,
title: "简谱", title: "简谱",
tone: "♭D", tone: "♭D",
marginHorizontal: 32, marginHorizontal: 64,
marginTop: 32,
gapAfterTitle: 16,
gapAfterHeader: 32,
gapBetweenParagraph: 32,
beat: [4, 4], beat: [4, 4],
speed: 75, speed: 75,
authors: ["Haven Mattuy 制谱", "杨瑞光 作词"], authors: ["Haven Mattuy 制谱", "杨瑞光 作词"],
data: {}, paragraphs: [
{
notations: [
{
note: "6",
octave: -2,
dash: 1,
dotted: true,
},
{
note: "6",
octave: -1,
dash: 0,
dotted: false,
},
{
note: "3",
octave: 0,
dash: 0,
dotted: false,
},
{
note: "3",
octave: 0,
dash: 0,
dotted: false,
},
{
note: "│",
octave: 0,
dash: 0,
dotted: false,
},
{
note: "1",
octave: 0,
dash: 0,
dotted: false,
},
{
note: "1",
octave: 0,
dash: 1,
dotted: false,
},
{
note: "2",
octave: 0,
dash: 1,
dotted: false,
},
{
note: "2",
octave: 0,
dash: 0,
dotted: false,
},
{
note: "-",
octave: 0,
dash: 0,
dotted: false,
},
{
note: "│",
octave: 0,
dash: 0,
dotted: false,
},
],
},
{
notations: [
{
note: "1",
octave: 2,
dotted: false,
},
{
note: "2",
octave: 0,
dotted: false,
},
{
note: "4",
octave: 1,
dotted: true,
sharpFlat: 1,
},
],
},
],
// TODO: do this
popoverRefs: {},
}); });
export default globalStore; export default globalStore;
+79
View File
@@ -0,0 +1,79 @@
import { observable } from "mobx";
import store from "../store/global";
function calcFontData(fontSize) {
const span = document.createElement("span");
span.style.fontFamily = window.getComputedStyle(document.body).fontFamily;
span.style.fontSize = `${fontSize}px`;
span.style.visibility = "hidden";
span.style.position = "absolute";
span.style.display = "inline-block";
span.textContent = "x";
document.body.appendChild(span);
const data = span.getBoundingClientRect();
return {
xWidth: data.width,
xHeight: data.height,
};
}
const placement = observable({
get commonLineHeight() {
return this.xHeight;
},
get titleOffsetY() {
const titleHeight = 32;
return store.marginTop + titleHeight + store.gapAfterTitle;
},
get headerOffsetY() {
const leftInfoBlockHeight = this.commonLineHeight * 2;
const titleOffset = this.titleOffsetY;
const leftInfoBlockOffset = titleOffset + leftInfoBlockHeight;
const rightInfoBlockOffset =
titleOffset + this.commonLineHeight * store.authors.length;
return (
Math.max(titleOffset, leftInfoBlockOffset, rightInfoBlockOffset) +
store.gapAfterHeader
);
},
get baseFontData() {
return calcFontData(16);
},
get baseSubFontData() {
return calcFontData(12);
},
get xHeight() {
return this.baseFontData.xHeight;
},
get xWidth() {
return this.baseFontData.xWidth;
},
get subXHeight() {
return this.baseSubFontData.xHeight;
},
get subXWidth() {
return this.baseSubFontData.xWidth;
},
});
function calcParagraphHeight(paragraph) {
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 calcNotationHeight(notation) {
const noteHeight = 20;
const oc = Math.abs(notation.octave) | 0;
const octaveHeight = oc ? 2 + (oc - 1) * 5 : 0;
return noteHeight + octaveHeight;
}
export default placement;
export { calcParagraphHeight, calcNotationWidth };
+3 -1
View File
@@ -6,10 +6,12 @@ function Canvas({ children, ...props }) {
return ( return (
<svg <svg
xmlns=" http://www.w3.org/2000/svg" xmlns=" http://www.w3.org/2000/svg"
{...props}
className={Styles.svg} className={Styles.svg}
width={store.canvasWidth} width={store.canvasWidth}
height={store.canvasHeight} height={store.canvasHeight}
// TODO: do this
onContextMenu={(ev) => ev.preventDefault()}
{...props}
> >
{children} {children}
</svg> </svg>
+1
View File
@@ -1,6 +1,7 @@
.svg { .svg {
border: 1px solid ghostwhite; border: 1px solid ghostwhite;
box-shadow: ghostwhite 0px 0px 10px; box-shadow: ghostwhite 0px 0px 10px;
font-size: 16px;
} }
.svg :global(text) { .svg :global(text) {
user-select: none; user-select: none;
+84 -2
View File
@@ -1,13 +1,95 @@
import SubMenu from "antd/lib/menu/SubMenu";
import { Button, Dropdown, Menu, Tabs } 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";
import P, { calcParagraphHeight } from "../../util/placement";
import Canvas from "../Canvas"; import Canvas from "../Canvas";
import Header from "../Header"; import Header from "../Header";
import Paragraph from "../Paragraph";
import Row from "../Row";
import Styles from "./index.module.css"; import Styles from "./index.module.css";
export default function Editor() { function Editor() {
const heightCache = [];
function accumulate(index) {
let height = 0;
for (let i = 0; i < index; i++) {
const p = store.paragraphs[i];
heightCache[i] = heightCache[i] || calcParagraphHeight(p);
height += heightCache[i];
console.log(store.paragraphs[i], heightCache[i]);
}
return height;
}
const fileMenu = (
<Menu>
<Menu.Item key="create" icon={<PlusOutlined />}>
新建
</Menu.Item>
<Menu.Item key="open" icon={<FolderOpenOutlined />}>
打开
</Menu.Item>
<Menu.Item key="save" icon={<SaveOutlined />}>
保存
</Menu.Item>
</Menu>
);
const editMenu = (
<Menu>
<Menu.Item key="title">编辑曲名</Menu.Item>
<SubMenu key="tone" title="转调">
<Menu.Item key="setting:1">Option 1</Menu.Item>
<Menu.Item key="setting:2">Option 2</Menu.Item>
<Menu.Item key="setting:3">Option 3</Menu.Item>
<Menu.Item key="setting:4">Option 4</Menu.Item>
</SubMenu>
</Menu>
);
return ( return (
<div className={Styles.container}> <div
className={Styles.container}
style={{ width: store.canvasWidth + "px" }}
>
<div className={Styles.headerWrapper}>
<div
className={Styles.header}
style={{ width: store.canvasWidth + "px" }}
>
<Dropdown overlay={fileMenu} placement="bottomLeft">
<Button icon={<FileTextOutlined />} type="text">
文件
</Button>
</Dropdown>
<Dropdown overlay={editMenu} placement="bottomLeft">
<Button icon={<EditOutlined />} type="text">
编辑
</Button>
</Dropdown>
</div>
</div>
<Canvas> <Canvas>
<Header /> <Header />
<Row offsetX={store.marginHorizontal} offsetY={P.headerOffsetY}>
{store.paragraphs.map((p, i) => {
return <Paragraph key={p} paragraph={p} offsetY={accumulate(i)} />;
})}
</Row>
</Canvas> </Canvas>
</div> </div>
); );
} }
export default observer(Editor);
+17 -6
View File
@@ -1,7 +1,18 @@
.container { .headerWrapper {
background-color: white; margin-bottom: 16px;
height: 100%; border-bottom: 1px solid whitesmoke;
display: flex; width: 100%;
justify-content: center; }
padding-top: 8px; .header {
margin: auto;
padding: 8px 0;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: white;
min-width: 100%;
height: 100%;
} }
+12 -7
View File
@@ -2,6 +2,7 @@ import { action } from "mobx";
import { message } from "antd"; import { message } from "antd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import EditableContent from "../../../component/EditableContent"; import EditableContent from "../../../component/EditableContent";
import P from "../../../util/placement";
import store from "../../../store/global"; import store from "../../../store/global";
import Row from "../../Row"; import Row from "../../Row";
import Text from "../../Text"; import Text from "../../Text";
@@ -22,7 +23,6 @@ const tones = [
"♭F", "♭F",
"♭G", "♭G",
].map((t) => ({ key: t, text: t })); ].map((t) => ({ key: t, text: t }));
const beats = ["4/4"].map((t) => ({ key: t, text: t }));
const handleChangeTone = action(function (value) { const handleChangeTone = action(function (value) {
store.tone = value; store.tone = value;
}); });
@@ -53,15 +53,20 @@ function LeftInfoBlock() {
options={tones} options={tones}
onChange={handleChangeTone} onChange={handleChangeTone}
> >
<Row type="tone" editable offsetX={store.marginHorizontal} offsetY="64"> <Row
type="tone"
editable
offsetX={store.marginHorizontal}
offsetY={P.titleOffsetY}
>
<Text>1&nbsp;&nbsp;= </Text> <Text>1&nbsp;&nbsp;= </Text>
{store.tone.startsWith("♭") && ( {store.tone.startsWith("♭") && (
<Text x="25" y="-2" fontSize="12"> <Text x="27" y="-2" fontSize="12">
</Text> </Text>
)} )}
<Text editable x={store.tone.startsWith("♭") ? 34 : 28}> <Text editable x={store.tone.startsWith("♭") ? 36 : 29}>
{store.tone.at(-1)} {store.tone.at(-1)}
</Text> </Text>
</Row> </Row>
@@ -76,7 +81,7 @@ function LeftInfoBlock() {
type="beat" type="beat"
editable editable
offsetX={store.marginHorizontal + 64} offsetX={store.marginHorizontal + 64}
offsetY="64" offsetY={P.titleOffsetY}
> >
<Text x="0" y="-8" textAnchor="middle"> <Text x="0" y="-8" textAnchor="middle">
{store.beat[0]} {store.beat[0]}
@@ -97,10 +102,10 @@ function LeftInfoBlock() {
editable editable
type="speed" type="speed"
offsetX={store.marginHorizontal} offsetX={store.marginHorizontal}
offsetY="86" offsetY={P.titleOffsetY + 22}
> >
<Text x="-4"></Text> <Text x="-4"></Text>
<Text x="14">=&nbsp;&nbsp;{store.speed}</Text> <Text x="16">=&nbsp;{store.speed}</Text>
</Row> </Row>
</EditableContent> </EditableContent>
</> </>
+84 -21
View File
@@ -1,40 +1,103 @@
import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons";
import { Modal } from "antd";
import { action } from "mobx"; import { action } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import EditableContent from "../../../component/EditableContent"; import EditableContent from "../../../component/EditableContent";
import PopoverOnSvg from "../../../component/PopoverOnSvg";
import escapeHtml from "../../../util/html-escape";
import store from "../../../store/global"; import store from "../../../store/global";
import P from "../../../util/placement";
import Row from "../../Row"; import Row from "../../Row";
import Text from "../../Text"; import Text from "../../Text";
const authorContextMenu = [
{
key: "create",
text: "添加",
icon: <PlusOutlined style={{ color: "grey" }} />,
onClick: () => {
store.authors.push("作者信息");
},
},
{
key: "delete",
text: "删除",
icon: <DeleteOutlined style={{ color: "grey" }} />,
onClick: (index) => {
store.authors.splice(index, 1);
},
},
];
const blockContextMenu = [
{
key: "create",
text: "添加作者信息",
icon: <PlusOutlined style={{ color: "grey" }} />,
onClick: () => {
store.authors.push("【作曲者】 作曲");
store.authors.push("【记谱者】 记谱");
},
},
];
const handleSelectMenu = action((index, value) => {
const menu = authorContextMenu.find((m) => m.key === value);
menu?.onClick(index);
});
const handleSelectBlockMenu = action((value) => {
const menu = blockContextMenu.find((m) => m.key === value);
menu?.onClick();
});
const handleChangeAuthor = action((index, value) => { const handleChangeAuthor = action((index, value) => {
if (!value) {
return new Promise((resolve) => {
Modal.confirm({
title: "删除这条作者信息?",
okText: "确定",
cancelText: "取消",
onOk: () => {
resolve(true);
store.authors.splice(index, 1);
},
onCancel: () => {
resolve(true);
},
});
});
}
store.authors[index] = value; store.authors[index] = value;
}); });
function RightInfoBlock() { function RightInfoBlock() {
return ( return (
// TODO: working // TODO: working
<PopoverOnSvg <Row
trigger="context" type="authors"
placement="leftCenter" offsetX={store.canvasWidth - store.marginHorizontal}
renderPopover="x" offsetY={P.titleOffsetY}
offset={{ x: -32 }}
> >
<Row <EditableContent
type="authors" inputType="select"
offsetX={store.canvasWidth - store.marginHorizontal} options={blockContextMenu}
offsetY="64" popoverProps={{ trigger: "context" }}
onChange={handleSelectBlockMenu}
> >
// TODO: 添加新增指引
<rect <rect
x="-100" x={store.marginHorizontal - 150}
y="-4" y="-64"
width="100" width={150}
height={store.authors.length * 22} height={150}
fill="transparent" fill={store.authors.length ? "none" : "transparent"}
// className={Styles.hotCorner}
></rect> ></rect>
{store.authors.map((author, i) => ( </EditableContent>
{store.authors.map((author, i) => (
<EditableContent
key={i + author}
inputType="select"
options={authorContextMenu}
popoverProps={{ trigger: "context" }}
onChange={handleSelectMenu.bind(null, i)}
>
<EditableContent <EditableContent
key={author}
title="作者信息:" title="作者信息:"
initialValue={author} initialValue={author}
onChange={handleChangeAuthor.bind(null, i)} onChange={handleChangeAuthor.bind(null, i)}
@@ -43,9 +106,9 @@ function RightInfoBlock() {
{author} {author}
</Text> </Text>
</EditableContent> </EditableContent>
))} </EditableContent>
</Row> ))}
</PopoverOnSvg> </Row>
); );
} }
+10 -1
View File
@@ -1,5 +1,6 @@
import { action } from "mobx"; import { action } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useRef } from "react";
import EditableContent from "../../../component/EditableContent"; import EditableContent from "../../../component/EditableContent";
import store from "../../../store/global"; import store from "../../../store/global";
import Row from "../../Row"; import Row from "../../Row";
@@ -9,9 +10,17 @@ const handleChangeTitle = action((value) => {
}); });
function Title() { function Title() {
const ref = useRef();
useEffect(() => {
store.popoverRefs.title = ref;
return () => {
store.popoverRefs.title = null;
};
}, []);
return ( return (
<Row type="title" offsetY="32"> <Row type="title" offsetY={store.marginTop}>
<EditableContent <EditableContent
ref={ref}
title="歌曲名称:" title="歌曲名称:"
initialValue={store.title} initialValue={store.title}
onChange={handleChangeTitle} onChange={handleChangeTitle}
+40 -3
View File
@@ -1,10 +1,47 @@
import P, { calcParagraphHeight } from "../../util/placement";
import Row from "../Row"; import Row from "../Row";
import Text from "../Text"; import Text from "../Text";
export default function Notation(...props) { function composeArray(octave) {
const len = Math.abs(Number(octave));
if (!Number.isInteger(len)) {
return [];
}
return Array(len).fill(0, 0, len);
}
export default function Notation({ offsetX, notation }) {
const octaveInitialOffset =
notation.octave > 0 ? -(P.xHeight / 2 + 2) : P.xHeight / 2 - 2;
const octaveStepOffset = notation.octave > 0 ? -5 : 5;
return ( return (
<Row type="notation" {...props}> <Row type="notation" offsetX={offsetX}>
<Text y="100">1</Text> {notation.sharpFlat && (
<Text
dominantBaseline="middle"
textAnchor="middle"
fontSize="12"
y={-P.subXHeight / 2 + 4}
>
{notation.sharpFlat > 0 ? "#" : "♭"}
</Text>
)}
<Row offsetX={notation.sharpFlat ? P.subXWidth + 2 : 0}>
<Text dominantBaseline="middle" textAnchor="middle">
{notation.note}
</Text>
{notation.dotted && (
<circle type="dot" cx={P.xWidth + 4} cy="0" r="2"></circle>
)}
{composeArray(notation.octave).map((oc, i) => (
<circle
type="octave"
cx="0"
cy={octaveStepOffset * i + octaveInitialOffset}
r="2"
></circle>
))}
</Row>
</Row> </Row>
); );
} }
+26
View File
@@ -0,0 +1,26 @@
import P, { calcNotationWidth } from "../../util/placement";
import Notation from "../Notation";
import Row from "../Row";
function Paragraph({ paragraph, ...props }) {
const widthCache = [];
function accumulate(index) {
let width = 0;
for (let i = 0; i < index; i++) {
const n = paragraph.notations[i];
widthCache[i] = widthCache[i] || calcNotationWidth(n);
width += widthCache[i];
}
return width;
}
return (
<Row type="paragraph" {...props}>
{paragraph.notations.map((n, i) => (
<Notation key={n} notation={n} offsetX={accumulate(i)} />
))}
</Row>
);
}
export default Paragraph;