daily: 2021-09-27
This commit is contained in:
@@ -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 PopoverOnSvg from "../PopoverOnSvg";
|
||||
import Styles from "./index.module.css";
|
||||
|
||||
function EditableContent({
|
||||
children,
|
||||
title,
|
||||
initialValue,
|
||||
inputType,
|
||||
options,
|
||||
onChange,
|
||||
}) {
|
||||
const EditableContent = function (
|
||||
{ children, title, initialValue, inputType, options, onChange, popoverProps },
|
||||
ref
|
||||
) {
|
||||
const [inputValue, setInputValue] = useState(initialValue);
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
showPopover: setPopoverVisible.bind(null, true),
|
||||
hidePopover: setPopoverVisible.bind(null, false),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const handleVisibilityChange = useCallback(
|
||||
function (value) {
|
||||
setPopoverVisible(value);
|
||||
@@ -68,6 +72,7 @@ function EditableContent({
|
||||
<Menu className={Styles.menu}>
|
||||
{(options || []).map((option) => (
|
||||
<Menu.Item
|
||||
icon={option.icon}
|
||||
key={option.key}
|
||||
className={
|
||||
Styles.menuItem + (option.key === initialValue ? " selected" : "")
|
||||
@@ -100,10 +105,11 @@ function EditableContent({
|
||||
visible={popoverVisible}
|
||||
placement="bottom"
|
||||
onVisibilityChange={handleVisibilityChange}
|
||||
{...popoverProps}
|
||||
>
|
||||
{children}
|
||||
</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 { createPortal } from "react-dom";
|
||||
import Styles from "./index.module.css";
|
||||
import { getContainerOffset, getPopoverContainer } from "./container";
|
||||
|
||||
// ENHANCE: smart placement
|
||||
function PopoverOnSvg({
|
||||
@@ -20,7 +21,6 @@ function PopoverOnSvg({
|
||||
showArrow,
|
||||
onVisibilityChange,
|
||||
}) {
|
||||
const containerRef = useRef();
|
||||
const triggerBoxRef = useRef({
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -38,6 +38,7 @@ function PopoverOnSvg({
|
||||
// true, we'll update it after dom mounted, because we cannot calculate
|
||||
// correct position of popover before react mounting it.
|
||||
const [innerVisible, setInnerVisible] = useState(false);
|
||||
const [lastContextCord, setLastContextCord] = useState({ x: 0, y: 0 });
|
||||
const [, forceUpdate] = useState();
|
||||
const popoverClassNames = [Styles.popover, globalClassName].filter(Boolean);
|
||||
const arrowClassNames = [Styles.arrow];
|
||||
@@ -74,8 +75,7 @@ function PopoverOnSvg({
|
||||
);
|
||||
const handleContextMenu = useCallback(
|
||||
(ev) => {
|
||||
// TODO: working
|
||||
console.log(ev.pageX, ev.pageY)
|
||||
setLastContextCord({ x: ev.pageX, y: ev.pageY });
|
||||
ev.preventDefault();
|
||||
changeVisibility(true);
|
||||
},
|
||||
@@ -128,84 +128,82 @@ function PopoverOnSvg({
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
if (!containerRef.current) {
|
||||
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);
|
||||
if (trigger !== "context") {
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
popoverClassNames.push("dark");
|
||||
arrowClassNames.push("dark");
|
||||
@@ -261,7 +259,7 @@ function PopoverOnSvg({
|
||||
</>
|
||||
)}
|
||||
</div>,
|
||||
containerRef.current
|
||||
getPopoverContainer()
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -270,6 +268,7 @@ function PopoverOnSvg({
|
||||
PopoverOnSvg.defaultProps = {
|
||||
trigger: "click",
|
||||
placement: "top",
|
||||
showArrow: true,
|
||||
};
|
||||
|
||||
export default React.memo(PopoverOnSvg);
|
||||
|
||||
+99
-2
@@ -6,11 +6,108 @@ let globalStore = observable({
|
||||
canvasHeight: 1448,
|
||||
title: "简谱",
|
||||
tone: "♭D",
|
||||
marginHorizontal: 32,
|
||||
marginHorizontal: 64,
|
||||
marginTop: 32,
|
||||
gapAfterTitle: 16,
|
||||
gapAfterHeader: 32,
|
||||
gapBetweenParagraph: 32,
|
||||
beat: [4, 4],
|
||||
speed: 75,
|
||||
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;
|
||||
|
||||
@@ -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 (
|
||||
<svg
|
||||
xmlns=" http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
className={Styles.svg}
|
||||
width={store.canvasWidth}
|
||||
height={store.canvasHeight}
|
||||
// TODO: do this
|
||||
onContextMenu={(ev) => ev.preventDefault()}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.svg {
|
||||
border: 1px solid ghostwhite;
|
||||
box-shadow: ghostwhite 0px 0px 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.svg :global(text) {
|
||||
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 Header from "../Header";
|
||||
import Paragraph from "../Paragraph";
|
||||
import Row from "../Row";
|
||||
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 (
|
||||
<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>
|
||||
<Header />
|
||||
<Row offsetX={store.marginHorizontal} offsetY={P.headerOffsetY}>
|
||||
{store.paragraphs.map((p, i) => {
|
||||
return <Paragraph key={p} paragraph={p} offsetY={accumulate(i)} />;
|
||||
})}
|
||||
</Row>
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Editor);
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
.container {
|
||||
background-color: white;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 8px;
|
||||
.headerWrapper {
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid whitesmoke;
|
||||
width: 100%;
|
||||
}
|
||||
.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 { observer } from "mobx-react-lite";
|
||||
import EditableContent from "../../../component/EditableContent";
|
||||
import P from "../../../util/placement";
|
||||
import store from "../../../store/global";
|
||||
import Row from "../../Row";
|
||||
import Text from "../../Text";
|
||||
@@ -22,7 +23,6 @@ const tones = [
|
||||
"♭F",
|
||||
"♭G",
|
||||
].map((t) => ({ key: t, text: t }));
|
||||
const beats = ["4/4"].map((t) => ({ key: t, text: t }));
|
||||
const handleChangeTone = action(function (value) {
|
||||
store.tone = value;
|
||||
});
|
||||
@@ -53,15 +53,20 @@ function LeftInfoBlock() {
|
||||
options={tones}
|
||||
onChange={handleChangeTone}
|
||||
>
|
||||
<Row type="tone" editable offsetX={store.marginHorizontal} offsetY="64">
|
||||
<Row
|
||||
type="tone"
|
||||
editable
|
||||
offsetX={store.marginHorizontal}
|
||||
offsetY={P.titleOffsetY}
|
||||
>
|
||||
<Text>1 = </Text>
|
||||
{store.tone.startsWith("♭") && (
|
||||
<Text x="25" y="-2" fontSize="12">
|
||||
<Text x="27" y="-2" fontSize="12">
|
||||
♭
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text editable x={store.tone.startsWith("♭") ? 34 : 28}>
|
||||
<Text editable x={store.tone.startsWith("♭") ? 36 : 29}>
|
||||
{store.tone.at(-1)}
|
||||
</Text>
|
||||
</Row>
|
||||
@@ -76,7 +81,7 @@ function LeftInfoBlock() {
|
||||
type="beat"
|
||||
editable
|
||||
offsetX={store.marginHorizontal + 64}
|
||||
offsetY="64"
|
||||
offsetY={P.titleOffsetY}
|
||||
>
|
||||
<Text x="0" y="-8" textAnchor="middle">
|
||||
{store.beat[0]}
|
||||
@@ -97,10 +102,10 @@ function LeftInfoBlock() {
|
||||
editable
|
||||
type="speed"
|
||||
offsetX={store.marginHorizontal}
|
||||
offsetY="86"
|
||||
offsetY={P.titleOffsetY + 22}
|
||||
>
|
||||
<Text x="-4">♩</Text>
|
||||
<Text x="14">= {store.speed}</Text>
|
||||
<Text x="16">= {store.speed}</Text>
|
||||
</Row>
|
||||
</EditableContent>
|
||||
</>
|
||||
|
||||
@@ -1,40 +1,103 @@
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { Modal } from "antd";
|
||||
import { action } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import EditableContent from "../../../component/EditableContent";
|
||||
import PopoverOnSvg from "../../../component/PopoverOnSvg";
|
||||
import escapeHtml from "../../../util/html-escape";
|
||||
import store from "../../../store/global";
|
||||
import P from "../../../util/placement";
|
||||
import Row from "../../Row";
|
||||
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) => {
|
||||
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;
|
||||
});
|
||||
|
||||
function RightInfoBlock() {
|
||||
return (
|
||||
// TODO: working
|
||||
<PopoverOnSvg
|
||||
trigger="context"
|
||||
placement="leftCenter"
|
||||
renderPopover="x"
|
||||
offset={{ x: -32 }}
|
||||
<Row
|
||||
type="authors"
|
||||
offsetX={store.canvasWidth - store.marginHorizontal}
|
||||
offsetY={P.titleOffsetY}
|
||||
>
|
||||
<Row
|
||||
type="authors"
|
||||
offsetX={store.canvasWidth - store.marginHorizontal}
|
||||
offsetY="64"
|
||||
<EditableContent
|
||||
inputType="select"
|
||||
options={blockContextMenu}
|
||||
popoverProps={{ trigger: "context" }}
|
||||
onChange={handleSelectBlockMenu}
|
||||
>
|
||||
// TODO: 添加新增指引
|
||||
<rect
|
||||
x="-100"
|
||||
y="-4"
|
||||
width="100"
|
||||
height={store.authors.length * 22}
|
||||
fill="transparent"
|
||||
x={store.marginHorizontal - 150}
|
||||
y="-64"
|
||||
width={150}
|
||||
height={150}
|
||||
fill={store.authors.length ? "none" : "transparent"}
|
||||
// className={Styles.hotCorner}
|
||||
></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
|
||||
key={author}
|
||||
title="作者信息:"
|
||||
initialValue={author}
|
||||
onChange={handleChangeAuthor.bind(null, i)}
|
||||
@@ -43,9 +106,9 @@ function RightInfoBlock() {
|
||||
{author}
|
||||
</Text>
|
||||
</EditableContent>
|
||||
))}
|
||||
</Row>
|
||||
</PopoverOnSvg>
|
||||
</EditableContent>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { action } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useRef } from "react";
|
||||
import EditableContent from "../../../component/EditableContent";
|
||||
import store from "../../../store/global";
|
||||
import Row from "../../Row";
|
||||
@@ -9,9 +10,17 @@ const handleChangeTitle = action((value) => {
|
||||
});
|
||||
|
||||
function Title() {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
store.popoverRefs.title = ref;
|
||||
return () => {
|
||||
store.popoverRefs.title = null;
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<Row type="title" offsetY="32">
|
||||
<Row type="title" offsetY={store.marginTop}>
|
||||
<EditableContent
|
||||
ref={ref}
|
||||
title="歌曲名称:"
|
||||
initialValue={store.title}
|
||||
onChange={handleChangeTitle}
|
||||
|
||||
@@ -1,10 +1,47 @@
|
||||
import P, { calcParagraphHeight } from "../../util/placement";
|
||||
import Row from "../Row";
|
||||
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 (
|
||||
<Row type="notation" {...props}>
|
||||
<Text y="100">1</Text>
|
||||
<Row type="notation" offsetX={offsetX}>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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