solid-pianoroll 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ import { createMemo, Index, Show } from "solid-js";
2
+ import { usePianoRollContext } from "./PianoRollContext";
3
+ import { blackKeys, keys } from "./PianoRollKeys";
4
+ const PianoRollGrid = () => {
5
+ const context = usePianoRollContext();
6
+ const gridDivisorTicks = createMemo(() => (context.ppq * 4) / context.gridDivision);
7
+ const gridArray = createMemo(() => {
8
+ const numberOfLines = context.duration / gridDivisorTicks();
9
+ return Array.from({ length: numberOfLines }).map((_, index) => index * gridDivisorTicks());
10
+ });
11
+ return (<div class="PianoRoll-Grid-Container" style={{
12
+ position: "absolute",
13
+ width: `${context.clientRect.width}px`,
14
+ height: `${context.clientRect.height}px`,
15
+ }}>
16
+ <div class="PianoRoll-Grid" style={{
17
+ position: "relative",
18
+ width: `${context.clientRect.width}px`,
19
+ height: "100%",
20
+ }}>
21
+ <Index each={gridArray()}>
22
+ {(tick, index) => {
23
+ const virtualDimensions = createMemo(() => context.horizontalViewPort.getVirtualDimensions(tick(), gridDivisorTicks()));
24
+ return (<Show when={virtualDimensions().size > 0}>
25
+ <div class="PianoRoll-Grid-Time" style={{
26
+ "z-index": 1,
27
+ position: "absolute",
28
+ "box-sizing": "border-box",
29
+ background: index % 4 === 0 ? "#ccc" : "#ddd",
30
+ top: `0px`,
31
+ height: `100%`,
32
+ left: `${virtualDimensions().offset}px`,
33
+ width: `${virtualDimensions().size}px`,
34
+ "border-left": "1px gray solid",
35
+ }}></div>
36
+ </Show>);
37
+ }}
38
+ </Index>
39
+ <Index each={keys}>
40
+ {(key) => {
41
+ const virtualDimensions = createMemo(() => context.verticalViewPort.getVirtualDimensions(127 - key().number, 1));
42
+ return (<Show when={virtualDimensions().size > 0}>
43
+ <div class="PianoRoll-Grid-Key" style={{
44
+ position: "absolute",
45
+ top: `${virtualDimensions().offset}px`,
46
+ height: `${virtualDimensions().size}px`,
47
+ width: "100%",
48
+ "background-color": key().isBlack ? "rgba(0,0,0,0.2)" : "none",
49
+ "border-style": "solid",
50
+ "border-color": "gray",
51
+ "border-width": `${!key().isBlack && !blackKeys.includes((key().number + 1) % 12) ? "0.1px" : 0} 1px ${!key().isBlack && !blackKeys.includes((key().number - 1) % 12) ? "0.1px" : 0} 0`,
52
+ "z-index": 1,
53
+ }}></div>
54
+ </Show>);
55
+ }}
56
+ </Index>
57
+ </div>
58
+ </div>);
59
+ };
60
+ export default PianoRollGrid;
@@ -0,0 +1,29 @@
1
+ import styles from "./PianoRollKeys.module.css";
2
+ import { createMemo, Index, Show } from "solid-js";
3
+ import { usePianoRollContext } from "./PianoRollContext";
4
+ const PianoRollKeys = () => {
5
+ const context = usePianoRollContext();
6
+ return (<div class={styles.PianoRollKeys}>
7
+ <Index each={keys}>
8
+ {(key) => {
9
+ const virtualDimensions = createMemo(() => context.verticalViewPort.getVirtualDimensions(127 - key().number, 1));
10
+ return (<Show when={virtualDimensions().size > 0}>
11
+ <div class={styles["PianoRollKeys-Key"]} title={key().name} data-index={key().number % 12} style={{
12
+ top: `${virtualDimensions().offset}px`,
13
+ height: `${virtualDimensions().size}px`,
14
+ "background-color": key().isBlack ? "#000" : "#fff",
15
+ "border-width": `${!key().isBlack && !blackKeys.includes((key().number + 1) % 12) ? "0.1px" : 0} 1px ${!key().isBlack && !blackKeys.includes((key().number - 1) % 12) ? "0.1px" : 0} 0`,
16
+ }}></div>
17
+ </Show>);
18
+ }}
19
+ </Index>
20
+ </div>);
21
+ };
22
+ export const blackKeys = [1, 3, 6, 8, 10];
23
+ export const keyNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
24
+ export const keys = Array.from({ length: 128 }).map((_, index) => ({
25
+ number: index,
26
+ name: `${keyNames[index % 12]} ${Math.floor(index / 12) - 2}`,
27
+ isBlack: blackKeys.includes(index % 12),
28
+ }));
29
+ export default PianoRollKeys;
@@ -0,0 +1,86 @@
1
+ import { createMemo, Index, Show } from "solid-js";
2
+ import { usePianoRollContext } from "./PianoRollContext";
3
+ const PianoRollNotes = (props) => {
4
+ const context = usePianoRollContext();
5
+ const gridDivisionTicks = createMemo(() => (context.ppq * 4) / context.gridDivision);
6
+ const snapValueToGridIfEnabled = (value, altKey) => context.snapToGrid && !altKey
7
+ ? Math.round(value / gridDivisionTicks()) * gridDivisionTicks()
8
+ : value;
9
+ return (<div class="PianoRoll-Notes-Container" style={{
10
+ position: "absolute",
11
+ width: `${context.clientRect.width}px`,
12
+ height: `${context.clientRect.height}px`,
13
+ }}>
14
+ <div class="PianoRoll-Notes" ref={props.ref} style={{
15
+ position: "relative",
16
+ width: `${context.clientRect.width}px`,
17
+ height: "100%",
18
+ }}>
19
+ <Index each={context.notes}>
20
+ {(note, index) => {
21
+ const horizontalVirtualDimensions = createMemo(() => context.verticalViewPort.getVirtualDimensions(127 - note().midi, 1));
22
+ const verticalVirtualDimensions = createMemo(() => context.horizontalViewPort.getVirtualDimensions(note().ticks, note().durationTicks));
23
+ return (<Show when={!!horizontalVirtualDimensions().size && !!verticalVirtualDimensions().size}>
24
+ <div class="PianoRoll-Note" onMouseMove={(event) => {
25
+ if (context.isDragging)
26
+ return;
27
+ const relativeX = context.horizontalViewPort.getScaledValue(context.horizontalViewPort.getPosition(event.clientX));
28
+ const noteStartX = context.horizontalViewPort.getScaledValue(note().ticks);
29
+ const noteEndX = context.horizontalViewPort.getScaledValue(note().ticks + note().durationTicks);
30
+ context.onNoteDragModeChange(relativeX - noteStartX < 3
31
+ ? "trimStart"
32
+ : noteEndX - relativeX < 3
33
+ ? "trimEnd"
34
+ : "move");
35
+ }} onMouseDown={(event) => {
36
+ context.onIsDraggingChange(true);
37
+ const initialPosition = context.horizontalViewPort.getPosition(event.clientX);
38
+ const diffPosition = initialPosition - note().ticks;
39
+ const handleMouseMove = (mouseMoveEvent) => {
40
+ const ticks = snapValueToGridIfEnabled(Math.max(context.horizontalViewPort.getPosition(mouseMoveEvent.clientX) -
41
+ diffPosition, 0), mouseMoveEvent.altKey);
42
+ context.onNoteChange?.(index, {
43
+ ...note(),
44
+ ...(context.noteDragMode === "move" && {
45
+ midi: Math.round(127 - context.verticalViewPort.getPosition(mouseMoveEvent.clientY)),
46
+ }),
47
+ ...((context.noteDragMode === "move" ||
48
+ context.noteDragMode === "trimStart") && {
49
+ ticks,
50
+ }),
51
+ ...(context.noteDragMode === "trimStart" && {
52
+ durationTicks: note().durationTicks + note().ticks - ticks,
53
+ }),
54
+ ...(context.noteDragMode === "trimEnd" && {
55
+ durationTicks: snapValueToGridIfEnabled(context.horizontalViewPort.getPosition(mouseMoveEvent.clientX) -
56
+ note().ticks, mouseMoveEvent.altKey),
57
+ }),
58
+ });
59
+ };
60
+ const handleMouseUp = () => {
61
+ context.onIsDraggingChange(false);
62
+ window.removeEventListener("mousemove", handleMouseMove);
63
+ window.removeEventListener("mouseup", handleMouseUp);
64
+ };
65
+ window.addEventListener("mousemove", handleMouseMove);
66
+ window.addEventListener("mouseup", handleMouseUp);
67
+ }} style={{
68
+ "z-index": 2,
69
+ position: "absolute",
70
+ "box-sizing": "border-box",
71
+ top: `${horizontalVirtualDimensions().offset}px`,
72
+ height: `${horizontalVirtualDimensions().size}px`,
73
+ left: `${verticalVirtualDimensions().offset}px`,
74
+ width: `${verticalVirtualDimensions().size}px`,
75
+ "border-width": "0.5px",
76
+ "border-style": "solid",
77
+ "background-color": `rgba(255,0,0, ${(128 + note().velocity) / 256})`,
78
+ "border-color": "#a00",
79
+ }}></div>
80
+ </Show>);
81
+ }}
82
+ </Index>
83
+ </div>
84
+ </div>);
85
+ };
86
+ export default PianoRollNotes;
@@ -0,0 +1,84 @@
1
+ import { createEffect } from "solid-js";
2
+ import { clamp } from "./helpers";
3
+ import { usePianoRollContext } from "./PianoRollContext";
4
+ const ScrollContainer = (props) => {
5
+ let scrollContentRef;
6
+ const context = usePianoRollContext();
7
+ const forwardEventToNote = (event) => {
8
+ const x = "clientX" in event ? event.clientX : event.touches[0]?.clientX;
9
+ const y = "clientY" in event ? event.clientY : event.touches[0]?.clientY;
10
+ const elementUnderMouse = [...(context.notesContainer?.querySelectorAll?.("div") ?? [])].find((element) => {
11
+ const rect = element.getBoundingClientRect();
12
+ return (x >= rect.left &&
13
+ rect.left + rect.width > x &&
14
+ y >= rect.top &&
15
+ rect.top + rect.height > y);
16
+ });
17
+ if (elementUnderMouse) {
18
+ return elementUnderMouse.dispatchEvent(new MouseEvent(event.type, event));
19
+ }
20
+ };
21
+ const handleScroll = (event) => {
22
+ event.preventDefault();
23
+ if (didUpdateScroll) {
24
+ didUpdateScroll = false;
25
+ return;
26
+ }
27
+ const maxVerticalPosition = 128 - 128 / context.verticalZoom;
28
+ const maxPosition = context.duration - context.duration / context.zoom;
29
+ const { width, height } = context.clientRect;
30
+ const { scrollTop, scrollLeft, scrollWidth, scrollHeight } = event.currentTarget;
31
+ const scrollTopAmount = scrollTop / (scrollHeight - height);
32
+ const scrollLeftAmount = scrollLeft / (scrollWidth - width);
33
+ context.onVerticalPositionChange?.(maxVerticalPosition * scrollTopAmount);
34
+ context.onPositionChange?.(maxPosition * scrollLeftAmount);
35
+ };
36
+ let didUpdateScroll = false;
37
+ createEffect(() => {
38
+ const maxVerticalPosition = 128 - 128 / context.verticalZoom;
39
+ const maxPosition = context.duration - context.duration / context.zoom;
40
+ const scrollTopAmount = maxVerticalPosition > 0 ? context.verticalPosition / maxVerticalPosition : 0;
41
+ const scrollLeftAmount = maxPosition > 0 ? context.position / maxPosition : 0;
42
+ const { height, width } = context.clientRect;
43
+ if (!scrollContentRef?.parentElement)
44
+ return;
45
+ const scrollDivHeight = clamp(context.verticalZoom * height, height, 10000);
46
+ const scrollTop = scrollTopAmount * (scrollDivHeight - height);
47
+ const scrollDivWidth = clamp(context.zoom * width, width, 10000);
48
+ const scrollLeft = scrollLeftAmount * (scrollDivWidth - width);
49
+ didUpdateScroll = true;
50
+ scrollContentRef.style.height = `${scrollDivHeight}px`;
51
+ scrollContentRef.style.width = `${scrollDivWidth}px`;
52
+ scrollContentRef.parentElement.scrollTo({
53
+ left: scrollLeft,
54
+ top: scrollTop,
55
+ });
56
+ });
57
+ return (<div ref={props.ref} class="PianoRoll-Vertical-Scroller" style={{
58
+ flex: 1,
59
+ ...(context.noteDragMode && {
60
+ cursor: context.noteDragMode === "trimStart"
61
+ ? "w-resize"
62
+ : context.noteDragMode === "trimEnd"
63
+ ? "e-resize"
64
+ : "pointer",
65
+ }),
66
+ //"box-sizing": "border-box",
67
+ "z-index": 4,
68
+ overflow: "scroll",
69
+ "pointer-events": "auto",
70
+ }} onScroll={handleScroll} onMouseDown={forwardEventToNote} onMouseMove={(event) => {
71
+ if (forwardEventToNote(event))
72
+ return;
73
+ if (context.isDragging)
74
+ return;
75
+ context.onNoteDragModeChange(undefined);
76
+ }} onClick={forwardEventToNote} onDblClick={(event) => {
77
+ if (!forwardEventToNote(event)) {
78
+ //TODO create new note with current gutter length
79
+ }
80
+ }} onMouseUp={forwardEventToNote} onTouchStart={forwardEventToNote} onTouchMove={forwardEventToNote} onTouchEnd={forwardEventToNote} onTouchCancel={forwardEventToNote} onDragStart={forwardEventToNote} onDrag={forwardEventToNote} onDragEnd={forwardEventToNote}>
81
+ <div ref={scrollContentRef}></div>
82
+ </div>);
83
+ };
84
+ export default ScrollContainer;
@@ -0,0 +1,12 @@
1
+ import { usePianoRollContext } from "./PianoRollContext";
2
+ const VerticalZoomControl = () => {
3
+ const context = usePianoRollContext();
4
+ return (<input value={context.verticalZoom} onInput={(event) => context.onVerticalZoomChange?.(event.currentTarget.valueAsNumber)} type="range" min="1" max="11" step={0.01} {...{ orient: "vertical" }} style={{
5
+ width: "16px",
6
+ ...{
7
+ "writing-mode": "bt-lr" /* IE */,
8
+ "-webkit-appearance": "slider-vertical" /* WebKit */,
9
+ },
10
+ }}/>);
11
+ };
12
+ export default VerticalZoomControl;
@@ -0,0 +1,29 @@
1
+ import { createEffect, createSignal } from "solid-js";
2
+ const defaultRect = {
3
+ left: 0,
4
+ width: 0,
5
+ top: 0,
6
+ height: 0,
7
+ bottom: 0,
8
+ right: 0,
9
+ x: 0,
10
+ y: 0,
11
+ };
12
+ export default function useBoundingClientRect(containerRef) {
13
+ const [boundingClientRect, setBoundingClientRect] = createSignal(defaultRect);
14
+ createEffect(() => {
15
+ const container = containerRef();
16
+ if (!container)
17
+ return;
18
+ const updateBoundingClientRect = () => {
19
+ setBoundingClientRect(container.getBoundingClientRect() ?? defaultRect);
20
+ };
21
+ const resizeObserver = new ResizeObserver(updateBoundingClientRect);
22
+ resizeObserver.observe(container);
23
+ updateBoundingClientRect();
24
+ return () => {
25
+ resizeObserver.disconnect();
26
+ };
27
+ });
28
+ return boundingClientRect;
29
+ }
@@ -0,0 +1,2 @@
1
+ declare const HorizontalZoomControl: () => import("solid-js").JSX.Element;
2
+ export default HorizontalZoomControl;
@@ -1,9 +1,12 @@
1
1
  import { Midi } from "@tonejs/midi";
2
2
  import { JSX } from "solid-js";
3
3
  type Note = Pick<ReturnType<Midi["tracks"][number]["notes"][number]["toJSON"]>, "durationTicks" | "midi" | "ticks" | "velocity">;
4
- type PianoRollProps = {
4
+ export type GridDivision = 1 | 2 | 4 | 8 | 16 | 32 | 64;
5
+ export type PianoRollProps = {
5
6
  ppq: number;
6
7
  notes: Note[];
8
+ gridDivision: GridDivision;
9
+ snapToGrid: boolean;
7
10
  verticalPosition: number;
8
11
  verticalZoom: number;
9
12
  position: number;
@@ -14,6 +17,6 @@ type PianoRollProps = {
14
17
  onZoomChange?: (zoom: number) => void;
15
18
  onPositionChange?: (zoom: number) => void;
16
19
  onNoteChange?: (index: number, note: Note) => void;
17
- } & JSX.IntrinsicElements["div"];
18
- declare const PianoRoll: (allProps: PianoRollProps) => JSX.Element;
20
+ };
21
+ declare const PianoRoll: (allProps: PianoRollProps & JSX.IntrinsicElements["div"]) => JSX.Element;
19
22
  export default PianoRoll;
@@ -0,0 +1,23 @@
1
+ import { JSX } from "solid-js";
2
+ import { PianoRollProps } from "./PianoRoll";
3
+ import { ClientRect } from "./useBoundingClientRect";
4
+ import { ViewPortScaler } from "./useViewPortScaler";
5
+ export type NoteDragMode = "trimStart" | "move" | "trimEnd" | undefined;
6
+ type PianoRollContext = {
7
+ horizontalViewPort: ViewPortScaler;
8
+ verticalViewPort: ViewPortScaler;
9
+ clientRect: ClientRect;
10
+ notesContainer?: HTMLDivElement;
11
+ isDragging: boolean;
12
+ onIsDraggingChange: (isDragging: boolean) => void;
13
+ noteDragMode: NoteDragMode;
14
+ onNoteDragModeChange: (isDragging: NoteDragMode) => void;
15
+ } & PianoRollProps;
16
+ declare const PianoRollContext: import("solid-js").Context<PianoRollContext>;
17
+ export declare const PianoRollContextProvider: (props: {
18
+ scrollContainer?: HTMLDivElement;
19
+ notesContainer?: HTMLDivElement;
20
+ children: JSX.Element;
21
+ } & PianoRollProps) => JSX.Element;
22
+ export declare const usePianoRollContext: () => PianoRollContext;
23
+ export {};
@@ -0,0 +1,2 @@
1
+ declare const PianoRollGrid: () => import("solid-js").JSX.Element;
2
+ export default PianoRollGrid;
@@ -0,0 +1,9 @@
1
+ declare const PianoRollKeys: () => import("solid-js").JSX.Element;
2
+ export declare const blackKeys: number[];
3
+ export declare const keyNames: string[];
4
+ export declare const keys: {
5
+ number: number;
6
+ name: string;
7
+ isBlack: boolean;
8
+ }[];
9
+ export default PianoRollKeys;
@@ -0,0 +1,5 @@
1
+ import { Ref } from "solid-js";
2
+ declare const PianoRollNotes: (props: {
3
+ ref: Ref<HTMLDivElement | undefined>;
4
+ }) => import("solid-js").JSX.Element;
5
+ export default PianoRollNotes;
@@ -0,0 +1,5 @@
1
+ import { Ref } from "solid-js";
2
+ declare const ScrollContainer: (props: {
3
+ ref: Ref<HTMLDivElement>;
4
+ }) => import("solid-js").JSX.Element;
5
+ export default ScrollContainer;
@@ -0,0 +1,2 @@
1
+ declare const VerticalZoomControl: () => import("solid-js").JSX.Element;
2
+ export default VerticalZoomControl;
@@ -0,0 +1,3 @@
1
+ import { Accessor } from "solid-js";
2
+ export type ClientRect = Omit<DOMRect, "toJSON">;
3
+ export default function useBoundingClientRect(containerRef: Accessor<HTMLElement | undefined>): Accessor<ClientRect>;
@@ -1,3 +1,4 @@
1
+ export type ViewPortScaler = ReturnType<typeof useViewPortScaler>;
1
2
  export default function useViewPortScaler(getState: () => {
2
3
  virtualPosition: number;
3
4
  virtualRange: number;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.2",
2
+ "version": "0.0.4",
3
3
  "name": "solid-pianoroll",
4
4
  "description": "Pianoroll UI Control for Solid JS apps",
5
5
  "license": "MIT",
@@ -50,12 +50,15 @@
50
50
  "solid-js": ">=1.0.0"
51
51
  },
52
52
  "devDependencies": {
53
+ "postcss": "^8.4.20",
53
54
  "prettier": "2.8.1",
54
55
  "rollup": "^3.7.2",
56
+ "rollup-plugin-postcss": "^4.0.2",
55
57
  "rollup-preset-solid": "^2.0.1",
56
58
  "solid-js": "^1.6.4",
57
59
  "taze": "^0.8.4",
58
60
  "typescript": "^4.9.4",
61
+ "typescript-plugin-css-modules": "^4.1.1",
59
62
  "vite": "^4.0.0",
60
63
  "vite-plugin-solid": "^2.5.0"
61
64
  },