solid-pianoroll 0.0.6 → 0.0.8

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.
package/README.md CHANGED
@@ -24,7 +24,7 @@ pnpm add solid-pianoroll
24
24
 
25
25
  Use it:
26
26
 
27
- ### Interactive waveform
27
+ ### Interactive PianoRoll
28
28
 
29
29
  ```tsx
30
30
  import { PianoRoll } from "solid-pianoroll";
package/dist/cjs/index.js CHANGED
@@ -61,10 +61,6 @@ function useBoundingClientRect(containerRef) {
61
61
  return boundingClientRect;
62
62
  }
63
63
 
64
- const clamp = (value, min, max) => {
65
- return Math.max(min, Math.min(max, value));
66
- };
67
-
68
64
  function useViewPortScaler(getState) {
69
65
  const state = solidJs.createMemo(() => getState());
70
66
  function getScaledValue(position) {
@@ -74,12 +70,20 @@ function useViewPortScaler(getState) {
74
70
  function getCoordinates(position) {
75
71
  return getScaledValue(position) - getScaledValue(state().virtualPosition);
76
72
  }
73
+ function getVisibleRange() {
74
+ return state().virtualRange / state().zoom;
75
+ }
77
76
  function getPosition(offset) {
78
- const visibleDuration = state().virtualRange / state().zoom;
79
77
  const percentX = (offset - state().viewPortOffset) / state().viewPortSize;
80
- const position = state().virtualPosition + percentX * visibleDuration;
78
+ const position = state().virtualPosition + percentX * getVisibleRange();
81
79
  return position;
82
80
  }
81
+ function getMaxPosition() {
82
+ return state().virtualRange - state().virtualRange / state().zoom;
83
+ }
84
+ function getScrollSize() {
85
+ return clamp(state().zoom * state().viewPortSize, state().viewPortSize, 100000);
86
+ }
83
87
  function getVirtualDimensions(position, length) {
84
88
  const virtualLeft = getCoordinates(position);
85
89
  const virtualWidth = getScaledValue(length);
@@ -94,9 +98,15 @@ function useViewPortScaler(getState) {
94
98
  getScaledValue,
95
99
  getCoordinates,
96
100
  getPosition,
97
- getVirtualDimensions
101
+ getVirtualDimensions,
102
+ getVisibleRange,
103
+ getMaxPosition,
104
+ getScrollSize
98
105
  };
99
106
  }
107
+ const clamp = (value, min, max) => {
108
+ return Math.max(min, Math.min(max, value));
109
+ };
100
110
 
101
111
  const PianoRollContext = solidJs.createContext(undefined);
102
112
  const PianoRollContextProvider = props => {
@@ -109,13 +119,13 @@ const PianoRollContextProvider = props => {
109
119
  viewPortSize: clientRect().width,
110
120
  virtualPosition: props.position,
111
121
  virtualRange: props.duration,
112
- zoom: props.zoom
122
+ zoom: props.zoom * (500 / clientRect().width)
113
123
  }));
114
124
  const verticalViewPort = useViewPortScaler(() => ({
115
125
  viewPortOffset: clientRect().top,
116
126
  viewPortSize: clientRect().height,
117
127
  virtualPosition: props.verticalPosition,
118
- zoom: props.verticalZoom,
128
+ zoom: props.verticalZoom * (500 / clientRect().height),
119
129
  virtualRange: 128
120
130
  }));
121
131
  const getContextValue = () => {
@@ -345,8 +355,8 @@ const ScrollContainer = props => {
345
355
  const context = usePianoRollContext();
346
356
  const gridDivisorTicks = solidJs.createMemo(() => context.ppq * 4 / context.gridDivision);
347
357
  const forwardEventToNote = event => {
348
- const x = "clientX" in event ? event.clientX : event.touches[0]?.clientX;
349
- const y = "clientY" in event ? event.clientY : event.touches[0]?.clientY;
358
+ const x = "clientX" in event ? event.clientX : event.touches[0]?.clientX ?? 0;
359
+ const y = "clientY" in event ? event.clientY : event.touches[0]?.clientY ?? 0;
350
360
  const elementUnderMouse = [...(context.notesContainer?.querySelectorAll?.("div") ?? [])].find(element => {
351
361
  const rect = element.getBoundingClientRect();
352
362
  return x >= rect.left && rect.left + rect.width > x && y >= rect.top && rect.top + rect.height > y;
@@ -361,8 +371,8 @@ const ScrollContainer = props => {
361
371
  didUpdateScroll = false;
362
372
  return;
363
373
  }
364
- const maxVerticalPosition = 128 - 128 / context.verticalZoom;
365
- const maxPosition = context.duration - context.duration / context.zoom;
374
+ const maxVerticalPosition = context.verticalViewPort.getMaxPosition();
375
+ const maxPosition = context.horizontalViewPort.getMaxPosition();
366
376
  const {
367
377
  width,
368
378
  height
@@ -380,8 +390,8 @@ const ScrollContainer = props => {
380
390
  };
381
391
  let didUpdateScroll = false;
382
392
  solidJs.createEffect(() => {
383
- const maxVerticalPosition = 128 - 128 / context.verticalZoom;
384
- const maxPosition = context.duration - context.duration / context.zoom;
393
+ const maxVerticalPosition = context.verticalViewPort.getMaxPosition();
394
+ const maxPosition = context.horizontalViewPort.getMaxPosition();
385
395
  const scrollTopAmount = maxVerticalPosition > 0 ? context.verticalPosition / maxVerticalPosition : 0;
386
396
  const scrollLeftAmount = maxPosition > 0 ? context.position / maxPosition : 0;
387
397
  const {
@@ -389,9 +399,9 @@ const ScrollContainer = props => {
389
399
  width
390
400
  } = context.clientRect;
391
401
  if (!scrollContentRef?.parentElement) return;
392
- const scrollDivHeight = clamp(context.verticalZoom * height, height, 10000);
402
+ const scrollDivHeight = context.verticalViewPort.getScrollSize();
393
403
  const scrollTop = scrollTopAmount * (scrollDivHeight - height);
394
- const scrollDivWidth = clamp(context.zoom * width, width, 10000);
404
+ const scrollDivWidth = context.horizontalViewPort.getScrollSize();
395
405
  const scrollLeft = scrollLeftAmount * (scrollDivWidth - width);
396
406
  didUpdateScroll = true;
397
407
  scrollContentRef.style.height = `${scrollDivHeight}px`;
@@ -401,6 +411,30 @@ const ScrollContainer = props => {
401
411
  top: scrollTop
402
412
  });
403
413
  });
414
+ const insertOrUpdateNote = event => {
415
+ const position = context.horizontalViewPort.getPosition(event.clientX);
416
+ const midi = 127 - Math.floor(context.verticalViewPort.getPosition(event.clientY));
417
+ const eventPositionTicks = Math.floor(position / gridDivisorTicks()) * gridDivisorTicks();
418
+ const velocity = 127;
419
+ const existingNote = newNote();
420
+ const ticks = existingNote?.ticks ?? eventPositionTicks;
421
+ const durationTicks = existingNote?.ticks ? eventPositionTicks - existingNote?.ticks : gridDivisorTicks();
422
+ const note = {
423
+ midi,
424
+ ticks,
425
+ durationTicks,
426
+ velocity
427
+ };
428
+ if (existingNote) {
429
+ context.onNoteChange?.(newNoteIndex(), note);
430
+ return newNoteIndex();
431
+ } else {
432
+ return context.onInsertNote?.(note) ?? -1;
433
+ }
434
+ };
435
+ const [newNoteIndex, setNewNoteIndex] = solidJs.createSignal(-1);
436
+ const newNote = solidJs.createMemo(() => context.notes[newNoteIndex()]);
437
+ const [isMouseDown, setIsMouseDown] = solidJs.createSignal(false);
404
438
  return (() => {
405
439
  const _el$ = _tmpl$$3.cloneNode(true),
406
440
  _el$2 = _el$.firstChild;
@@ -411,30 +445,43 @@ const ScrollContainer = props => {
411
445
  _el$.$$touchend = forwardEventToNote;
412
446
  _el$.$$touchmove = forwardEventToNote;
413
447
  _el$.$$touchstart = forwardEventToNote;
414
- _el$.$$mouseup = forwardEventToNote;
415
- _el$.$$dblclick = event => {
416
- if (!forwardEventToNote(event)) {
417
- const position = context.horizontalViewPort.getPosition(event.clientX);
418
- const midi = 127 - Math.floor(context.verticalViewPort.getPosition(event.clientY));
419
- const ticks = Math.floor(position / gridDivisorTicks()) * gridDivisorTicks();
420
- const durationTicks = gridDivisorTicks();
421
- const velocity = 127;
422
- const note = {
423
- midi,
424
- ticks,
425
- durationTicks,
426
- velocity
427
- };
428
- context.onInsertNote?.(note);
448
+ _el$.$$mouseup = event => {
449
+ setIsMouseDown(false);
450
+ if (newNote()) return;
451
+ if (forwardEventToNote(event)) return;
452
+ if (!event.altKey) return;
453
+ insertOrUpdateNote(event);
454
+ };
455
+ _el$.$$click = event => {
456
+ if (newNote()) {
457
+ setNewNoteIndex(-1);
458
+ return;
429
459
  }
460
+ if (forwardEventToNote(event)) return;
461
+ if (!event.altKey) return;
462
+ insertOrUpdateNote(event);
463
+ };
464
+ _el$.$$dblclick = event => {
465
+ if (newNote()) return;
466
+ if (context.isDragging) return;
467
+ if (forwardEventToNote(event)) return;
468
+ insertOrUpdateNote(event);
430
469
  };
431
- _el$.$$click = forwardEventToNote;
432
470
  _el$.$$mousemove = event => {
471
+ event.preventDefault();
433
472
  if (forwardEventToNote(event)) return;
434
473
  if (context.isDragging) return;
435
- context.onNoteDragModeChange(undefined);
474
+ if (!isMouseDown()) {
475
+ context.onNoteDragModeChange(undefined);
476
+ return;
477
+ }
478
+ const index = insertOrUpdateNote(event);
479
+ setNewNoteIndex(index);
480
+ };
481
+ _el$.$$mousedown = event => {
482
+ if (forwardEventToNote(event)) return;
483
+ setIsMouseDown(true);
436
484
  };
437
- _el$.$$mousedown = forwardEventToNote;
438
485
  _el$.addEventListener("scroll", handleScroll);
439
486
  const _ref$ = props.ref;
440
487
  typeof _ref$ === "function" ? web.use(_ref$, _el$) : props.ref = _el$;
@@ -452,7 +499,7 @@ const ScrollContainer = props => {
452
499
  return _el$;
453
500
  })();
454
501
  };
455
- web.delegateEvents(["mousedown", "mousemove", "click", "dblclick", "mouseup", "touchstart", "touchmove", "touchend"]);
502
+ web.delegateEvents(["mousedown", "mousemove", "dblclick", "click", "mouseup", "touchstart", "touchmove", "touchend"]);
456
503
 
457
504
  const _tmpl$$2 = /*#__PURE__*/web.template(`<input type="range" min="1" max="11" step="0.01">`, 1);
458
505
  const VerticalZoomControl = () => {
@@ -487,12 +534,27 @@ const _tmpl$$1 = /*#__PURE__*/web.template(`<div class="PianoRoll-Grid-Container
487
534
  _tmpl$3 = /*#__PURE__*/web.template(`<div class="PianoRoll-Grid-Key"></div>`, 2);
488
535
  const PianoRollGrid = () => {
489
536
  const context = usePianoRollContext();
490
- const gridDivisorTicks = solidJs.createMemo(() => context.ppq * 4 / context.gridDivision);
537
+ const measureTicks = solidJs.createMemo(() => context.ppq * 4);
538
+ const selectedGridDivisorTicks = solidJs.createMemo(() => measureTicks() / context.gridDivision);
539
+ function calculateVisibleGridDivisorTicks(value) {
540
+ if (context.horizontalViewPort.getVisibleRange() / value > 100) {
541
+ return calculateVisibleGridDivisorTicks(value * 2);
542
+ }
543
+ if (context.horizontalViewPort.getVisibleRange() / value < 30) {
544
+ return calculateVisibleGridDivisorTicks(value / 2);
545
+ }
546
+ return value;
547
+ }
548
+ const gridDivisorTicks = solidJs.createMemo(() => calculateVisibleGridDivisorTicks(selectedGridDivisorTicks()));
491
549
  const gridArray = solidJs.createMemo(() => {
492
- const numberOfLines = context.duration / gridDivisorTicks();
550
+ const numberOfLines = Math.ceil(context.horizontalViewPort.getVisibleRange() / gridDivisorTicks()) + 1;
551
+ const startIndex = Math.floor(context.position / gridDivisorTicks());
493
552
  return Array.from({
494
553
  length: numberOfLines
495
- }).map((_, index) => index * gridDivisorTicks());
554
+ }).map((_, index) => ({
555
+ index: index + startIndex,
556
+ ticks: (index + startIndex) * gridDivisorTicks()
557
+ }));
496
558
  });
497
559
  return (() => {
498
560
  const _el$ = _tmpl$$1.cloneNode(true),
@@ -504,8 +566,8 @@ const PianoRollGrid = () => {
504
566
  get each() {
505
567
  return gridArray();
506
568
  },
507
- children: (tick, index) => {
508
- const virtualDimensions = solidJs.createMemo(() => context.horizontalViewPort.getVirtualDimensions(tick(), gridDivisorTicks()));
569
+ children: entry => {
570
+ const virtualDimensions = solidJs.createMemo(() => context.horizontalViewPort.getVirtualDimensions(entry().ticks, gridDivisorTicks()));
509
571
  return web.createComponent(solidJs.Show, {
510
572
  get when() {
511
573
  return virtualDimensions().size > 0;
@@ -515,19 +577,21 @@ const PianoRollGrid = () => {
515
577
  _el$3.style.setProperty("z-index", "1");
516
578
  _el$3.style.setProperty("position", "absolute");
517
579
  _el$3.style.setProperty("box-sizing", "border-box");
518
- _el$3.style.setProperty("background", index % 4 === 0 ? "#ccc" : "#ddd");
519
580
  _el$3.style.setProperty("top", "0px");
520
581
  _el$3.style.setProperty("height", "100%");
521
582
  _el$3.style.setProperty("border-left", "1px gray solid");
522
583
  web.effect(_p$ => {
523
- const _v$4 = `${virtualDimensions().offset}px`,
524
- _v$5 = `${virtualDimensions().size}px`;
525
- _v$4 !== _p$._v$4 && _el$3.style.setProperty("left", _p$._v$4 = _v$4);
526
- _v$5 !== _p$._v$5 && _el$3.style.setProperty("width", _p$._v$5 = _v$5);
584
+ const _v$4 = Math.ceil((entry().index + 1) * selectedGridDivisorTicks() / measureTicks()) % 2 === 0 ? "#ddd" : "#ccc",
585
+ _v$5 = `${virtualDimensions().offset}px`,
586
+ _v$6 = `${virtualDimensions().size}px`;
587
+ _v$4 !== _p$._v$4 && _el$3.style.setProperty("background", _p$._v$4 = _v$4);
588
+ _v$5 !== _p$._v$5 && _el$3.style.setProperty("left", _p$._v$5 = _v$5);
589
+ _v$6 !== _p$._v$6 && _el$3.style.setProperty("width", _p$._v$6 = _v$6);
527
590
  return _p$;
528
591
  }, {
529
592
  _v$4: undefined,
530
- _v$5: undefined
593
+ _v$5: undefined,
594
+ _v$6: undefined
531
595
  });
532
596
  return _el$3;
533
597
  }
@@ -550,20 +614,20 @@ const PianoRollGrid = () => {
550
614
  _el$4.style.setProperty("border-color", "gray");
551
615
  _el$4.style.setProperty("z-index", "1");
552
616
  web.effect(_p$ => {
553
- const _v$6 = `${virtualDimensions().offset}px`,
554
- _v$7 = `${virtualDimensions().size}px`,
555
- _v$8 = key().isBlack ? "rgba(0,0,0,0.2)" : "none",
556
- _v$9 = `${!key().isBlack && !blackKeys.includes((key().number + 1) % 12) ? "0.1px" : 0} 1px ${!key().isBlack && !blackKeys.includes((key().number - 1) % 12) ? "0.1px" : 0} 0`;
557
- _v$6 !== _p$._v$6 && _el$4.style.setProperty("top", _p$._v$6 = _v$6);
558
- _v$7 !== _p$._v$7 && _el$4.style.setProperty("height", _p$._v$7 = _v$7);
559
- _v$8 !== _p$._v$8 && _el$4.style.setProperty("background-color", _p$._v$8 = _v$8);
560
- _v$9 !== _p$._v$9 && _el$4.style.setProperty("border-width", _p$._v$9 = _v$9);
617
+ const _v$7 = `${virtualDimensions().offset}px`,
618
+ _v$8 = `${virtualDimensions().size}px`,
619
+ _v$9 = key().isBlack ? "rgba(0,0,0,0.2)" : "none",
620
+ _v$10 = `${!key().isBlack && !blackKeys.includes((key().number + 1) % 12) ? "0.1px" : 0} 1px ${!key().isBlack && !blackKeys.includes((key().number - 1) % 12) ? "0.1px" : 0} 0`;
621
+ _v$7 !== _p$._v$7 && _el$4.style.setProperty("top", _p$._v$7 = _v$7);
622
+ _v$8 !== _p$._v$8 && _el$4.style.setProperty("height", _p$._v$8 = _v$8);
623
+ _v$9 !== _p$._v$9 && _el$4.style.setProperty("background-color", _p$._v$9 = _v$9);
624
+ _v$10 !== _p$._v$10 && _el$4.style.setProperty("border-width", _p$._v$10 = _v$10);
561
625
  return _p$;
562
626
  }, {
563
- _v$6: undefined,
564
627
  _v$7: undefined,
565
628
  _v$8: undefined,
566
- _v$9: undefined
629
+ _v$9: undefined,
630
+ _v$10: undefined
567
631
  });
568
632
  return _el$4;
569
633
  }
@@ -633,5 +697,56 @@ const PianoRoll = allProps => {
633
697
  }));
634
698
  };
635
699
 
700
+ const usePianoRoll = () => {
701
+ const [ppq, onPpqChange] = solidJs.createSignal(120);
702
+ const [position, onPositionChange] = solidJs.createSignal(0);
703
+ const [zoom, onZoomChange] = solidJs.createSignal(10);
704
+ const [verticalZoom, onVerticalZoomChange] = solidJs.createSignal(5);
705
+ const [verticalPosition, onVerticalPositionChange] = solidJs.createSignal(64);
706
+ const [gridDivision, onGridDivisionChange] = solidJs.createSignal(16);
707
+ const [snapToGrid, onSnapToGridChange] = solidJs.createSignal(true);
708
+ const [notes, onNotesChange] = solidJs.createSignal([]);
709
+ const [duration, setDuration] = solidJs.createSignal(0);
710
+ const onNoteChange = (index, note) => {
711
+ onNotesChange([...notes().slice(0, index), note, ...notes().splice(index + 1)]);
712
+ };
713
+ const onInsertNote = note => {
714
+ const index = Math.max(notes().findIndex(({
715
+ ticks
716
+ }) => ticks > note.ticks), 0);
717
+ onNotesChange([...notes().slice(0, index), note, ...notes().splice(index)]);
718
+ return index;
719
+ };
720
+ const onRemoveNote = index => {
721
+ onNotesChange([...notes().slice(0, index), ...notes().splice(index + 1)]);
722
+ };
723
+ solidJs.createEffect(() => {
724
+ setDuration(Math.max((notes()[notes().length - 1]?.ticks ?? 0) + (notes()[notes().length - 1]?.durationTicks ?? 0), ppq() * 4 * 4));
725
+ });
726
+ return {
727
+ ppq,
728
+ onPpqChange,
729
+ position,
730
+ onPositionChange,
731
+ zoom,
732
+ onZoomChange,
733
+ verticalPosition,
734
+ onVerticalPositionChange,
735
+ verticalZoom,
736
+ onVerticalZoomChange,
737
+ gridDivision,
738
+ onGridDivisionChange,
739
+ snapToGrid,
740
+ onSnapToGridChange,
741
+ notes,
742
+ onNotesChange,
743
+ onNoteChange,
744
+ onInsertNote,
745
+ onRemoveNote,
746
+ duration
747
+ };
748
+ };
749
+
636
750
  exports.PianoRoll = PianoRoll;
751
+ exports.usePianoRoll = usePianoRoll;
637
752
  //# sourceMappingURL=index.js.map