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