daily: 2021-09-27
This commit is contained in:
@@ -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 can’t go back!**
|
|
||||||
|
|
||||||
If you aren’t 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 you’re on your own.
|
|
||||||
|
|
||||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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)
|
|
||||||
Generated
+15
-14
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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,16 +128,7 @@ function PopoverOnSvg({
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!containerRef.current) {
|
if (trigger !== "context") {
|
||||||
containerRef.current = document.body.querySelector(
|
|
||||||
"body > div#svg-popover-container"
|
|
||||||
);
|
|
||||||
if (!containerRef.current) {
|
|
||||||
containerRef.current = document.createElement("div");
|
|
||||||
containerRef.current.id = "svg-popover-container";
|
|
||||||
document.body.appendChild(containerRef.current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const box = triggerBoxRef.current;
|
const box = triggerBoxRef.current;
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case /^(left|right)(top|center|bottom)?$/i.test(placement):
|
case /^(left|right)(top|center|bottom)?$/i.test(placement):
|
||||||
@@ -205,7 +196,14 @@ function PopoverOnSvg({
|
|||||||
arrowClassNames.push("top");
|
arrowClassNames.push("top");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
makeOffset(getContainerOffset().x, getContainerOffset().y);
|
||||||
mergedStyles.transform += ` translate(${offsetX}px, ${offsetY}px)`;
|
mergedStyles.transform += ` translate(${offsetX}px, ${offsetY}px)`;
|
||||||
|
} else {
|
||||||
|
//TODO:
|
||||||
|
mergedStyles.left = lastContextCord.x;
|
||||||
|
mergedStyles.top = lastContextCord.y;
|
||||||
|
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
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = </Text>
|
<Text>1 = </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">= {store.speed}</Text>
|
<Text x="16">= {store.speed}</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</EditableContent>
|
</EditableContent>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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
|
|
||||||
trigger="context"
|
|
||||||
placement="leftCenter"
|
|
||||||
renderPopover="x"
|
|
||||||
offset={{ x: -32 }}
|
|
||||||
>
|
|
||||||
<Row
|
<Row
|
||||||
type="authors"
|
type="authors"
|
||||||
offsetX={store.canvasWidth - store.marginHorizontal}
|
offsetX={store.canvasWidth - store.marginHorizontal}
|
||||||
offsetY="64"
|
offsetY={P.titleOffsetY}
|
||||||
>
|
>
|
||||||
|
<EditableContent
|
||||||
|
inputType="select"
|
||||||
|
options={blockContextMenu}
|
||||||
|
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>
|
||||||
|
</EditableContent>
|
||||||
{store.authors.map((author, i) => (
|
{store.authors.map((author, i) => (
|
||||||
<EditableContent
|
<EditableContent
|
||||||
key={author}
|
key={i + author}
|
||||||
|
inputType="select"
|
||||||
|
options={authorContextMenu}
|
||||||
|
popoverProps={{ trigger: "context" }}
|
||||||
|
onChange={handleSelectMenu.bind(null, i)}
|
||||||
|
>
|
||||||
|
<EditableContent
|
||||||
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>
|
</Row>
|
||||||
</PopoverOnSvg>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user