@tremolo-ui/react 0.0.6 → 0.0.7

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/dist/cjs/index.js CHANGED
@@ -242,6 +242,62 @@ function styleHelper(value, op, influencer) {
242
242
  }
243
243
  }
244
244
  }
245
+ /**
246
+ * @category Utility
247
+ */
248
+ function mod(n, m) {
249
+ return (((n % m) + m) % m);
250
+ }
251
+ /**
252
+ * @category MIDI
253
+ */
254
+ const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
255
+ /**
256
+ * @category MIDI
257
+ */
258
+ function parseNoteName(noteName) {
259
+ const m = noteName.match(/^([a-g])(#{0,2}|b{0,2})(-?\d+)$/i);
260
+ if (!m)
261
+ throw new Error('Invalid note name');
262
+ const [, letter, accidental, octave] = m;
263
+ return {
264
+ letter: letter.toLocaleUpperCase(),
265
+ accidental: accidental,
266
+ octave: Number(octave),
267
+ };
268
+ }
269
+ /**
270
+ *
271
+ * @category MIDI
272
+ */
273
+ function noteNumber(noteName) {
274
+ const { letter, accidental, octave, } = parseNoteName(noteName);
275
+ const noteIndex = noteNames.indexOf(letter.toLocaleUpperCase());
276
+ const accidentalValue = (accidental[0] == 'b' ? -1 : 1) * accidental.length;
277
+ return noteIndex + 12 * (Number(octave) + 1) + accidentalValue;
278
+ }
279
+ /**
280
+ *
281
+ * @category MIDI
282
+ */
283
+ function noteName(noteNumber) {
284
+ const noteIndex = mod(noteNumber, 12);
285
+ const octave = Math.floor(noteNumber / 12) - 1;
286
+ return noteNames[noteIndex] + octave;
287
+ }
288
+ /**
289
+ * @category MIDI
290
+ */
291
+ function isWhiteKey(note) {
292
+ const n = typeof note == 'string' ? noteNumber(note) : note;
293
+ return (mod(n, 12) == 0 ||
294
+ mod(n, 12) == 2 ||
295
+ mod(n, 12) == 4 ||
296
+ mod(n, 12) == 5 ||
297
+ mod(n, 12) == 7 ||
298
+ mod(n, 12) == 9 ||
299
+ mod(n, 12) == 11);
300
+ }
245
301
 
246
302
  /**
247
303
  * This hook is user-land implementation of the experimental `useEffectEvent` hook.
@@ -537,6 +593,261 @@ function NumberInput(_a) {
537
593
  } }, rest)));
538
594
  }
539
595
 
596
+ function KeyLabel({ label, className, style, wrapperStyle, __note, __index, __label, }) {
597
+ const content = label ? label(__note, __index) : __label && __label(__note, __index);
598
+ return ((content != undefined || content != null) &&
599
+ jsxRuntime.jsx("div", { className: 'tremolo-piano-key-label-wrapper', css: react.css(Object.assign({ display: 'flex', justifyContent: 'center', alignItems: 'end', height: '100%' }, wrapperStyle)), children: jsxRuntime.jsx("div", { className: clsx('tremolo-piano-key-label', className), css: react.css(Object.assign({ display: 'flex', justifyContent: 'center', alignItems: 'center', fontSize: '0.6rem', height: 10, marginBottom: 10, padding: 4, borderRadius: 4, borderStyle: 'solid', borderWidth: 1, borderColor: '#888', aspectRatio: 1, maxWidth: 'calc(100% - 16px)' }, style)), children: content }) }));
600
+ }
601
+
602
+ const WhiteKey = React.forwardRef(({ width = 40, height = '100%', bg = 'white', activeBg = '#ccc', style, children, __glissando, __position, __note, __disabled, __index, __fill, __width, __playNote, __stopNote, __label, }, ref) => {
603
+ const [played, setPlayed] = React.useState(false);
604
+ React.useImperativeHandle(ref, () => {
605
+ return {
606
+ play() {
607
+ if (__disabled)
608
+ return;
609
+ setPlayed(true);
610
+ if (__playNote)
611
+ __playNote(__note);
612
+ },
613
+ stop() {
614
+ setPlayed(false);
615
+ if (__stopNote)
616
+ __stopNote(__note);
617
+ },
618
+ played() {
619
+ return played;
620
+ }
621
+ };
622
+ }, []);
623
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
624
+ let keyLabelProps;
625
+ if (children != undefined) {
626
+ React.Children.forEach(children, (child) => {
627
+ if (React.isValidElement(child)) {
628
+ if (child.type == KeyLabel) {
629
+ keyLabelProps = child.props;
630
+ }
631
+ else {
632
+ throw new Error('only <KeyLabel>');
633
+ }
634
+ }
635
+ else {
636
+ throw new Error('children is an invalid element.');
637
+ }
638
+ });
639
+ }
640
+ return (jsxRuntime.jsx("div", { className: `tremolo-piano-white-key`, "aria-disabled": __disabled, css: react.css(Object.assign({ position: 'absolute', backgroundColor: played ? activeBg : bg, color: 'black', left: __position, width: __fill ? __width : (width !== null && width !== undefined ? width : __width), height: height, border: '1px solid #555', borderRadius: '0 0 8px 8px', cursor: __disabled ? 'not-allowed' : 'pointer', zIndex: 1 }, style)), onPointerDown: () => {
641
+ if (__disabled)
642
+ return;
643
+ setPlayed(true);
644
+ if (__playNote)
645
+ __playNote(__note);
646
+ }, onPointerEnter: () => {
647
+ if (__disabled)
648
+ return;
649
+ if (__glissando) {
650
+ setPlayed(true);
651
+ if (__playNote)
652
+ __playNote(__note);
653
+ }
654
+ }, onPointerUp: () => {
655
+ if (__disabled)
656
+ return;
657
+ setPlayed(false);
658
+ if (__stopNote)
659
+ __stopNote(__note);
660
+ }, onPointerLeave: () => {
661
+ if (__disabled)
662
+ return;
663
+ setPlayed(false);
664
+ if (__stopNote)
665
+ __stopNote(__note);
666
+ }, children: jsxRuntime.jsx(KeyLabel, Object.assign({ __note: __note, __label: __label, __index: __index }, keyLabelProps)) }));
667
+ });
668
+
669
+ const BlackKey = React.forwardRef(({ width = 40 * 0.65, height = '60%', bg = '#333', activeBg = '#666', style, children, __glissando, __position, __note, __disabled, __index, __fill, __width, __playNote, __stopNote, __label, }, ref) => {
670
+ const [played, setPlayed] = React.useState(false);
671
+ React.useImperativeHandle(ref, () => {
672
+ return {
673
+ play() {
674
+ if (__disabled)
675
+ return;
676
+ setPlayed(true);
677
+ if (__playNote)
678
+ __playNote(__note);
679
+ },
680
+ stop() {
681
+ setPlayed(false);
682
+ if (__stopNote)
683
+ __stopNote(__note);
684
+ },
685
+ played() {
686
+ return played;
687
+ }
688
+ };
689
+ }, []);
690
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
691
+ let keyLabelProps;
692
+ if (children != undefined) {
693
+ React.Children.forEach(children, (child) => {
694
+ if (React.isValidElement(child)) {
695
+ if (child.type == KeyLabel) {
696
+ keyLabelProps = child.props;
697
+ }
698
+ else {
699
+ throw new Error('only <KeyLabel>');
700
+ }
701
+ }
702
+ else {
703
+ throw new Error('children is an invalid element.');
704
+ }
705
+ });
706
+ }
707
+ return (jsxRuntime.jsx("div", { className: `tremolo-piano-black-key`, "aria-disabled": __disabled, css: react.css(Object.assign({ position: 'absolute', backgroundColor: played ? activeBg : bg, color: 'white', left: __position, width: __fill ? __width : (width !== null && width !== undefined ? width : __width), height: height, border: '1px solid #555', borderRadius: '0 0 8px 8px', cursor: __disabled ? 'not-allowed' : 'pointer', zIndex: 2 }, style)), onPointerDown: () => {
708
+ if (__disabled)
709
+ return;
710
+ setPlayed(true);
711
+ if (__playNote)
712
+ __playNote(__note);
713
+ }, onPointerEnter: () => {
714
+ if (__disabled)
715
+ return;
716
+ if (__glissando) {
717
+ setPlayed(true);
718
+ if (__playNote)
719
+ __playNote(__note);
720
+ }
721
+ }, onPointerUp: () => {
722
+ if (__disabled)
723
+ return;
724
+ setPlayed(false);
725
+ if (__stopNote)
726
+ __stopNote(__note);
727
+ }, onPointerLeave: () => {
728
+ if (__disabled)
729
+ return;
730
+ setPlayed(false);
731
+ if (__stopNote)
732
+ __stopNote(__note);
733
+ }, children: jsxRuntime.jsx(KeyLabel, Object.assign({ __note: __note, __label: __label, __index: __index }, keyLabelProps)) }));
734
+ });
735
+
736
+ const Shortcuts = {
737
+ HOME_ROW: {
738
+ keys: ['a', 'w', 's', 'e', 'd', 'f', 't', 'g', 'y', 'h', 'u', 'j', 'k', 'o', 'l', 'p', ';']
739
+ }
740
+ };
741
+
742
+ function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? '100%' : 160, style, playNote, stopNote, label, children, }) {
743
+ // -- state and ref ---
744
+ const [pressed, setPressed] = React.useState(false);
745
+ const [whiteNoteWidth, setWhiteNoteWidth] = React.useState(-1);
746
+ const keyRefs = React.useRef([]);
747
+ for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) {
748
+ keyRefs.current[i] = React.createRef();
749
+ }
750
+ const pianoRef = React.useRef(null);
751
+ // --- interpret props ---
752
+ const noteRangeArray = Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
753
+ const whiteNoteCount = noteRangeArray.filter((v) => isWhiteKey(v)).length;
754
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
755
+ let whiteKeyProps, blackKeyProps;
756
+ if (children != undefined) {
757
+ React.Children.forEach(children, (child) => {
758
+ if (React.isValidElement(child)) {
759
+ if (child.type == WhiteKey) {
760
+ whiteKeyProps = child.props;
761
+ }
762
+ else if (child.type == BlackKey) {
763
+ blackKeyProps = child.props;
764
+ }
765
+ else {
766
+ throw new Error('only <WhiteKey> or <BlackKey>');
767
+ }
768
+ }
769
+ else {
770
+ throw new Error('children is an invalid element.');
771
+ }
772
+ });
773
+ }
774
+ // --- internal functions ---
775
+ function notePosition(note) {
776
+ const pitchPositions = {
777
+ C: 0,
778
+ 'C#': 0.55,
779
+ D: 1,
780
+ 'D#': 1.8,
781
+ E: 2,
782
+ F: 3,
783
+ 'F#': 3.5,
784
+ G: 4,
785
+ 'G#': 4.7,
786
+ A: 5,
787
+ 'A#': 5.85,
788
+ B: 6,
789
+ };
790
+ const toNoteName = (n) => {
791
+ const { letter, accidental } = parseNoteName(noteName(n));
792
+ return (letter + accidental);
793
+ };
794
+ const _noteName = toNoteName(note);
795
+ const firstNoteName = toNoteName(noteRange.first);
796
+ const pos = pitchPositions[_noteName] - pitchPositions[firstNoteName];
797
+ const octave = Math.floor((note - noteRange.first) / 12);
798
+ const octaveOffset = noteNames.indexOf(firstNoteName) > noteNames.indexOf(_noteName) ? 1 : 0;
799
+ const length = whiteNoteWidth + 3;
800
+ return pos * length + (octave + octaveOffset) * 7 * length;
801
+ }
802
+ // --- hooks ---
803
+ React.useEffect(() => {
804
+ if (fill && pianoRef.current) {
805
+ const parent = pianoRef.current.parentElement;
806
+ if (!parent)
807
+ throw new Error("doesn't have a parent element.");
808
+ const resizeObserver = new ResizeObserver(() => {
809
+ const w = pianoRef.current.clientWidth;
810
+ setWhiteNoteWidth(w / whiteNoteCount - 3);
811
+ });
812
+ resizeObserver.observe(parent);
813
+ }
814
+ else {
815
+ setWhiteNoteWidth((whiteKeyProps === null || whiteKeyProps === undefined ? undefined : whiteKeyProps.width) || 40);
816
+ }
817
+ }, []);
818
+ useEventListener(window, 'keydown', (e) => {
819
+ var _a, _b, _c, _d;
820
+ if (e.repeat)
821
+ return;
822
+ if (!keyboardShortcuts)
823
+ return;
824
+ const index = keyboardShortcuts.keys.indexOf(e.key);
825
+ if (index != -1) {
826
+ if (!((_b = (_a = keyRefs.current[index]) === null || _a === undefined ? undefined : _a.current) === null || _b === undefined ? undefined : _b.played())) {
827
+ (_d = (_c = keyRefs.current[index]) === null || _c === undefined ? undefined : _c.current) === null || _d === undefined ? undefined : _d.play();
828
+ }
829
+ }
830
+ });
831
+ useEventListener(window, 'keyup', (e) => {
832
+ var _a, _b;
833
+ if (e.repeat)
834
+ return;
835
+ if (!keyboardShortcuts)
836
+ return;
837
+ const index = keyboardShortcuts.keys.indexOf(e.key);
838
+ if (index != -1) {
839
+ (_b = (_a = keyRefs.current[index]) === null || _a === undefined ? undefined : _a.current) === null || _b === undefined ? undefined : _b.stop();
840
+ }
841
+ });
842
+ return (jsxRuntime.jsx("div", { ref: pianoRef, className: "tremolo-piano", css: react.css(Object.assign({ display: 'inline-block', boxSizing: 'border-box', userSelect: 'none', width: fill ? '100%' : undefined, height: height, position: 'relative' }, style)), onPointerDown: () => {
843
+ setPressed(true);
844
+ }, onPointerUp: () => {
845
+ setPressed(false);
846
+ }, children: noteRangeArray.map((note, index) => {
847
+ return (isWhiteKey(note) ? (jsxRuntime.jsx(WhiteKey, Object.assign({ ref: keyRefs.current[index], __glissando: glissando && pressed, __position: notePosition(note), __note: note, __disabled: note > midiMax, __index: index, __fill: fill, __width: whiteNoteWidth, __playNote: playNote, __stopNote: stopNote, __label: label }, whiteKeyProps), note)) : (jsxRuntime.jsx(BlackKey, Object.assign({ ref: keyRefs.current[index], __glissando: glissando && pressed, __position: notePosition(note), __note: note, __disabled: note > midiMax, __index: index, __fill: fill, __width: whiteNoteWidth * 0.65, __playNote: playNote, __stopNote: stopNote, __label: label }, blackKeyProps), note)));
848
+ }) }));
849
+ }
850
+
540
851
  /**
541
852
  * clamp value between min and max
542
853
  * @category Math
@@ -807,10 +1118,15 @@ const Slider = React.forwardRef((_a, ref) => {
807
1118
  });
808
1119
 
809
1120
  exports.AnimationCanvas = AnimationCanvas;
1121
+ exports.BlackKey = BlackKey;
1122
+ exports.KeyLabel = KeyLabel;
810
1123
  exports.Knob = Knob;
811
1124
  exports.NumberInput = NumberInput;
1125
+ exports.Piano = Piano;
1126
+ exports.Shortcuts = Shortcuts;
812
1127
  exports.Slider = Slider;
813
1128
  exports.SliderThumb = SliderThumb;
814
1129
  exports.SliderTrack = SliderTrack;
1130
+ exports.WhiteKey = WhiteKey;
815
1131
  exports.gradientDirection = gradientDirection;
816
1132
  //# sourceMappingURL=index.js.map