zds-pickers 4.0.7 → 4.0.9

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.
Files changed (55) hide show
  1. package/dist/index.cjs.js +11 -11
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.es.js +1194 -1818
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/types/index.d.ts +2 -0
  6. package/dist/types/midi/export.d.ts +8 -9
  7. package/dist/types/other/OctavePlayer.d.ts +7 -1
  8. package/dist/types/other/SVGText.d.ts +11 -0
  9. package/dist/types/other/SoundFontProvider.d.ts +1 -0
  10. package/dist/types/pickers/ChannelMappingPicker.d.ts +2 -1
  11. package/dist/types/pickers/StatusPicker.d.ts +2 -2
  12. package/lib/hooks/useComponentSize.js +40 -0
  13. package/lib/hooks/useStateWithDynamicDefault.js +9 -0
  14. package/lib/index.js +21 -0
  15. package/lib/index.ts +2 -0
  16. package/lib/midi/ccValues.js +134 -0
  17. package/lib/midi/export.js +27 -0
  18. package/lib/midi/export.ts +2 -3
  19. package/lib/other/DefaultTooltip.js +3 -0
  20. package/lib/other/OctavePlayer.js +48 -0
  21. package/lib/other/OctavePlayer.tsx +1 -1
  22. package/lib/other/SVGText.js +109 -0
  23. package/lib/other/SVGText.tsx +2 -0
  24. package/lib/other/SoundFontProvider.js +87 -0
  25. package/lib/other/SoundFontProvider.tsx +2 -0
  26. package/lib/pickers/CCPicker.js +6 -0
  27. package/lib/pickers/ChannelMappingPicker.js +50 -0
  28. package/lib/pickers/ChannelMappingPicker.tsx +5 -1
  29. package/lib/pickers/ChannelPicker.js +21 -0
  30. package/lib/pickers/Knob.js +49 -0
  31. package/lib/pickers/KnobPicker.js +29 -0
  32. package/lib/pickers/LatchPicker.js +9 -0
  33. package/lib/pickers/MappingPicker.js +37 -0
  34. package/lib/pickers/NotePicker.js +45 -0
  35. package/lib/pickers/PianoPicker.js +27 -0
  36. package/lib/pickers/PolarityPicker.js +11 -0
  37. package/lib/pickers/ResponseCurve.js +154 -0
  38. package/lib/pickers/ResponseCurvePicker.js +19 -0
  39. package/lib/pickers/Select.js +79 -0
  40. package/lib/pickers/StatusPicker.js +5 -0
  41. package/lib/pickers/ValuePicker.js +23 -0
  42. package/lib/pickers/knobSkin10.js +93 -0
  43. package/lib/rotaryKnob/InternalInput.js +22 -0
  44. package/lib/rotaryKnob/Types.js +1 -0
  45. package/lib/rotaryKnob/helpers/DrawCircle.js +9 -0
  46. package/lib/rotaryKnob/helpers/DrawLine.js +9 -0
  47. package/lib/rotaryKnob/helpers/HelpersOverlay.js +26 -0
  48. package/lib/rotaryKnob/helpers/KnobVisualHelpers.js +52 -0
  49. package/lib/rotaryKnob/index.js +244 -0
  50. package/lib/rotaryKnob/knobdefaultskin.js +56 -0
  51. package/lib/rotaryKnob/utils.js +106 -0
  52. package/lib/utils.js +21 -0
  53. package/package.json +2 -3
  54. package/tsconfig.json +1 -1
  55. package/vite.config.ts +3 -19
@@ -1,5 +1,6 @@
1
1
  export { default as ChannelMappingPicker } from './pickers/ChannelMappingPicker';
2
2
  export { default as ChannelPicker } from './pickers/ChannelPicker';
3
+ export { default as DefaultTooltip } from './other/DefaultTooltip';
3
4
  export { default as Knob } from './pickers/Knob';
4
5
  export { default as KnobPicker } from './pickers/KnobPicker';
5
6
  export { default as LatchPicker } from './pickers/LatchPicker';
@@ -15,6 +16,7 @@ export { default as SVGText } from './other/SVGText';
15
16
  export { default as ValuePicker } from './pickers/ValuePicker';
16
17
  export type * from './pickers/ChannelMappingPicker';
17
18
  export type * from './pickers/ChannelPicker';
19
+ export type * from './other/DefaultTooltip';
18
20
  export type * from './pickers/Knob';
19
21
  export type * from './pickers/KnobPicker';
20
22
  export type * from './pickers/LatchPicker';
@@ -1,12 +1,12 @@
1
1
  import type { Option } from 'lib/pickers/Select';
2
2
  declare const Statuses: {
3
- noteOff: number;
4
- noteOn: number;
5
- aftertouch: number;
6
- controlChange: number;
7
- programChange: number;
8
- channelPressure: number;
9
- pitchWheel: number;
3
+ readonly noteOff: 8;
4
+ readonly noteOn: 9;
5
+ readonly aftertouch: 10;
6
+ readonly controlChange: 11;
7
+ readonly programChange: 12;
8
+ readonly channelPressure: 13;
9
+ readonly pitchWheel: 14;
10
10
  };
11
11
  type Status = (typeof Statuses)[keyof typeof Statuses];
12
12
  declare const MASK_CHANNEL = 15;
@@ -18,5 +18,4 @@ declare const extractStatusAndChannel: (status: number) => {
18
18
  status: Status;
19
19
  channel: number;
20
20
  };
21
- export { combineStatusWithChannel, extractStatusAndChannel, getStatusLabel, MASK_CHANNEL, MASK_STATUS, statusOptions, Statuses, };
22
- export type { Status };
21
+ export { combineStatusWithChannel, extractStatusAndChannel, getStatusLabel, MASK_CHANNEL, MASK_STATUS, statusOptions, Statuses, type Status, };
@@ -1,5 +1,11 @@
1
1
  import type Soundfont from 'soundfont-player';
2
2
  import type { TooltipProps } from './DefaultTooltip';
3
+ type OctavePlayerProps = {
4
+ disabled?: boolean;
5
+ height: number;
6
+ octave: number;
7
+ width: number;
8
+ };
3
9
  type WithProviderProps = {
4
10
  format?: 'mp3' | 'ogg';
5
11
  height: number;
@@ -17,4 +23,4 @@ type WithProviderProps = {
17
23
  };
18
24
  declare const WithProvider: ({ format, height, hostname, instrumentName, octave, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, soundfont, Tooltip, width, }: WithProviderProps) => import("react/jsx-runtime").JSX.Element;
19
25
  export default WithProvider;
20
- export type { WithProviderProps };
26
+ export type { OctavePlayerProps, WithProviderProps };
@@ -1,4 +1,14 @@
1
1
  declare const MEASUREMENT_ELEMENT_ID = "__react_svg_text_measurement_id";
2
+ type WordsByLine = {
3
+ words: string[];
4
+ width: number;
5
+ showLine: boolean;
6
+ };
7
+ type WordWidths = {
8
+ wordsWithComputedWidth: Record<string, number>;
9
+ spaceWidth: number;
10
+ lineHeight: number;
11
+ };
2
12
  type TextProps = Partial<HTMLOrSVGElement> & {
3
13
  className?: string;
4
14
  dx?: number;
@@ -15,3 +25,4 @@ type TextProps = Partial<HTMLOrSVGElement> & {
15
25
  declare const Text: (props: TextProps) => import("react/jsx-runtime").JSX.Element;
16
26
  export default Text;
17
27
  export { MEASUREMENT_ELEMENT_ID };
28
+ export type { TextProps, WordWidths, WordsByLine };
@@ -15,3 +15,4 @@ type SoundfontProviderProps = {
15
15
  };
16
16
  declare const SoundfontProvider: (props: SoundfontProviderProps) => import("react").ReactNode;
17
17
  export default SoundfontProvider;
18
+ export type { SoundfontProviderProps };
@@ -1,6 +1,7 @@
1
1
  import type { GroupBase, SelectInstance } from 'react-select';
2
2
  import type { Option, SelectProps } from './Select';
3
3
  type InternalValue = number | 'separator';
4
+ type ChannelMappingOption = Option<InternalValue>;
4
5
  interface ChannelMappingPickerProps extends SelectProps<InternalValue> {
5
6
  channels: Option<number>[];
6
7
  className?: string;
@@ -11,4 +12,4 @@ interface ChannelMappingPickerProps extends SelectProps<InternalValue> {
11
12
  type ChannelMappingPickerRef = SelectInstance<Option<number>, false, GroupBase<Option<number>>>;
12
13
  declare const ChannelMappingPicker: import("react").ForwardRefExoticComponent<ChannelMappingPickerProps & import("react").RefAttributes<SelectInstance<Option<InternalValue>, false, GroupBase<Option<InternalValue>>>>>;
13
14
  export default ChannelMappingPicker;
14
- export type { ChannelMappingPickerProps, ChannelMappingPickerRef };
15
+ export type { ChannelMappingOption, ChannelMappingPickerProps, ChannelMappingPickerRef, };
@@ -5,8 +5,8 @@ type StatusPickerProps = SelectProps<Status> & {
5
5
  statuses: Option<Status>[];
6
6
  };
7
7
  type StatusPickerRef = SelectInstance<Option<Status>, false, GroupBase<Option<Status>>>;
8
- declare const StatusPicker: import("react").ForwardRefExoticComponent<SelectProps<number> & {
8
+ declare const StatusPicker: import("react").ForwardRefExoticComponent<SelectProps<Status> & {
9
9
  statuses: Option<Status>[];
10
- } & import("react").RefAttributes<SelectInstance<Option<number>, false, GroupBase<Option<number>>>>>;
10
+ } & import("react").RefAttributes<SelectInstance<Option<Status>, false, GroupBase<Option<Status>>>>>;
11
11
  export default StatusPicker;
12
12
  export type { StatusPickerProps, StatusPickerRef };
@@ -0,0 +1,40 @@
1
+ import { useCallback, useLayoutEffect, useState } from 'react';
2
+ const getSize = (el) => el
3
+ ? {
4
+ width: el.offsetWidth,
5
+ height: el.offsetHeight,
6
+ }
7
+ : {
8
+ width: 0,
9
+ height: 0,
10
+ };
11
+ const useComponentSize = (ref) => {
12
+ const [componentSize, setComponentSize] = useState(getSize(ref ? ref.current : null));
13
+ const handleResize = useCallback(() => {
14
+ if (ref.current) {
15
+ setComponentSize(getSize(ref.current));
16
+ }
17
+ }, [ref]);
18
+ // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
19
+ useLayoutEffect(() => {
20
+ if (!ref.current)
21
+ return;
22
+ handleResize();
23
+ if (typeof ResizeObserver === 'function') {
24
+ const resizeObserver = new ResizeObserver(() => {
25
+ handleResize();
26
+ });
27
+ resizeObserver.observe(ref.current);
28
+ return () => {
29
+ resizeObserver.disconnect();
30
+ // resizeObserver = null
31
+ };
32
+ }
33
+ window.addEventListener('resize', handleResize);
34
+ return () => {
35
+ window.removeEventListener('resize', handleResize);
36
+ };
37
+ }, [ref.current]);
38
+ return componentSize;
39
+ };
40
+ export default useComponentSize;
@@ -0,0 +1,9 @@
1
+ import { useEffect, useState } from 'react';
2
+ const useStateWithDynamicDefault = (defaultVal) => {
3
+ const [state, setState] = useState(defaultVal);
4
+ useEffect(() => {
5
+ setState(defaultVal);
6
+ }, [defaultVal]);
7
+ return [state, setState];
8
+ };
9
+ export default useStateWithDynamicDefault;
package/lib/index.js ADDED
@@ -0,0 +1,21 @@
1
+ // Components
2
+ export { default as ChannelMappingPicker } from './pickers/ChannelMappingPicker';
3
+ export { default as ChannelPicker } from './pickers/ChannelPicker';
4
+ export { default as DefaultTooltip } from './other/DefaultTooltip';
5
+ export { default as Knob } from './pickers/Knob';
6
+ export { default as KnobPicker } from './pickers/KnobPicker';
7
+ export { default as LatchPicker } from './pickers/LatchPicker';
8
+ export { default as MappingPicker } from './pickers/MappingPicker';
9
+ export { default as NotePicker } from './pickers/NotePicker';
10
+ export { default as OctavePlayer } from './other/OctavePlayer';
11
+ export { default as PianoPicker } from './pickers/PianoPicker';
12
+ export { default as PolarityPicker } from './pickers/PolarityPicker';
13
+ export { default as ResponseCurvePicker } from './pickers/ResponseCurvePicker';
14
+ export { default as Select } from './pickers/Select';
15
+ export { default as StatusPicker } from './pickers/StatusPicker';
16
+ export { default as SVGText } from './other/SVGText';
17
+ export { default as ValuePicker } from './pickers/ValuePicker';
18
+ // MIDI Values & Functions
19
+ export { combineStatusWithChannel, extractStatusAndChannel, getStatusLabel, MASK_CHANNEL, MASK_STATUS, Statuses, } from './midi/export';
20
+ export { default as ResponseCurve, RESPONSE_CURVES, } from './pickers/ResponseCurve';
21
+ export { default as ccValues } from './midi/ccValues';
package/lib/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Components
2
2
  export { default as ChannelMappingPicker } from './pickers/ChannelMappingPicker'
3
3
  export { default as ChannelPicker } from './pickers/ChannelPicker'
4
+ export { default as DefaultTooltip } from './other/DefaultTooltip'
4
5
  export { default as Knob } from './pickers/Knob'
5
6
  export { default as KnobPicker } from './pickers/KnobPicker'
6
7
  export { default as LatchPicker } from './pickers/LatchPicker'
@@ -18,6 +19,7 @@ export { default as ValuePicker } from './pickers/ValuePicker'
18
19
  // Types
19
20
  export type * from './pickers/ChannelMappingPicker'
20
21
  export type * from './pickers/ChannelPicker'
22
+ export type * from './other/DefaultTooltip'
21
23
  export type * from './pickers/Knob'
22
24
  export type * from './pickers/KnobPicker'
23
25
  export type * from './pickers/LatchPicker'
@@ -0,0 +1,134 @@
1
+ const CC_NAMES = [
2
+ 'Bank Select',
3
+ 'Modulation Wheel',
4
+ 'Breath Controller',
5
+ '',
6
+ 'Foot Control',
7
+ 'Portamento Time',
8
+ 'Data Entry Slider',
9
+ 'Volume',
10
+ 'Balance',
11
+ '',
12
+ 'Pan',
13
+ 'Expression',
14
+ 'Effect Control 1',
15
+ 'Effect Control 2',
16
+ '',
17
+ '',
18
+ '',
19
+ '',
20
+ '',
21
+ '',
22
+ '',
23
+ '',
24
+ '',
25
+ '',
26
+ '',
27
+ '',
28
+ '',
29
+ '',
30
+ '',
31
+ '',
32
+ '',
33
+ '',
34
+ 'Controller 0',
35
+ 'Controller 1',
36
+ 'Controller 2',
37
+ 'Controller 3',
38
+ 'Controller 4',
39
+ 'Controller 5',
40
+ 'Controller 6',
41
+ 'Controller 7',
42
+ 'Controller 8',
43
+ 'Controller 9',
44
+ 'Controller 10',
45
+ 'Controller 11',
46
+ 'Controller 12',
47
+ 'Controller 13',
48
+ 'Controller 14',
49
+ 'Controller 15',
50
+ 'Controller 16',
51
+ 'Controller 17',
52
+ 'Controller 18',
53
+ 'Controller 19',
54
+ 'Controller 20',
55
+ 'Controller 21',
56
+ 'Controller 22',
57
+ 'Controller 23',
58
+ 'Controller 24',
59
+ 'Controller 25',
60
+ 'Controller 26',
61
+ 'Controller 27',
62
+ 'Controller 28',
63
+ 'Controller 29',
64
+ 'Controller 30',
65
+ 'Controller 31',
66
+ 'Sustain',
67
+ 'Portamento',
68
+ 'Sostenuto',
69
+ 'Soft Pedal',
70
+ 'Legato Pedal',
71
+ 'Sustain 2',
72
+ 'Sound Variation',
73
+ 'Resonance/Timbre',
74
+ 'Sound Release Time',
75
+ 'Sound Attack Time',
76
+ 'Frequency Cutoff',
77
+ 'Sound Control 6',
78
+ 'Sound Control 7',
79
+ 'Sound Control 8',
80
+ 'Sound Control 9',
81
+ 'Sound Control 10',
82
+ 'Decay or General 1',
83
+ 'Hi-Pass or General 2',
84
+ 'General 3',
85
+ 'General 4',
86
+ 'Portamento Amount',
87
+ '',
88
+ '',
89
+ '',
90
+ '',
91
+ '',
92
+ '',
93
+ 'Reverb Level',
94
+ 'Tremolo Depth',
95
+ 'Chorus Level',
96
+ 'Detune',
97
+ 'Phaser Depth',
98
+ 'Data Entry +1',
99
+ 'Data Entry -1',
100
+ 'Non-registered LSB',
101
+ 'Non-registered MSB',
102
+ 'Registered msb',
103
+ 'Registered lsb',
104
+ '',
105
+ '',
106
+ '',
107
+ '',
108
+ '',
109
+ '',
110
+ '',
111
+ '',
112
+ 'Shift 1',
113
+ 'Shift 2',
114
+ 'Shift 3',
115
+ 'Shift 4',
116
+ 'Shift 5',
117
+ 'Shift 6',
118
+ '',
119
+ '',
120
+ '',
121
+ '',
122
+ 'All Sound Off',
123
+ 'All Controllers Off',
124
+ 'Local Mode',
125
+ 'All Notes Off',
126
+ 'Omni Mode Off',
127
+ 'Omni Mode On',
128
+ 'Mono Mode On',
129
+ 'Poly Mode',
130
+ ].map((label, value) => ({
131
+ label: String(label).length ? `${value} - ${label}` : String(value),
132
+ value,
133
+ }));
134
+ export default CC_NAMES;
@@ -0,0 +1,27 @@
1
+ const Statuses = {
2
+ noteOff: 8, // 1000
3
+ noteOn: 9, // 1001
4
+ aftertouch: 10, // 1010
5
+ controlChange: 11, // 1011
6
+ programChange: 12, // 1100
7
+ channelPressure: 13, // 1101
8
+ pitchWheel: 14, // 1110
9
+ };
10
+ const MASK_CHANNEL = 15; // 00001111
11
+ const MASK_STATUS = 240; // 01110000
12
+ const statusOptions = [
13
+ { value: Statuses.noteOn, label: 'Note On' },
14
+ { value: Statuses.noteOff, label: 'Note Stack' },
15
+ { value: Statuses.controlChange, label: 'Control Change' },
16
+ { value: Statuses.programChange, label: 'Program Change' },
17
+ { value: Statuses.aftertouch, label: 'Aftertouch' },
18
+ { value: Statuses.channelPressure, label: 'Channel Pressure' },
19
+ { value: Statuses.pitchWheel, label: 'Pitch Wheel' },
20
+ ];
21
+ const getStatusLabel = (val) => statusOptions.filter(({ value }) => value === val)[0]?.label || '???';
22
+ const combineStatusWithChannel = (channel, status) => channel | (status << 4);
23
+ const extractStatusAndChannel = (status) => ({
24
+ status: (((status & MASK_STATUS) >> 4) | 8),
25
+ channel: status & MASK_CHANNEL,
26
+ });
27
+ export { combineStatusWithChannel, extractStatusAndChannel, getStatusLabel, MASK_CHANNEL, MASK_STATUS, statusOptions, Statuses, };
@@ -8,7 +8,7 @@ const Statuses = {
8
8
  programChange: 12, // 1100
9
9
  channelPressure: 13, // 1101
10
10
  pitchWheel: 14, // 1110
11
- }
11
+ } as const
12
12
 
13
13
  type Status = (typeof Statuses)[keyof typeof Statuses]
14
14
 
@@ -49,6 +49,5 @@ export {
49
49
  MASK_STATUS,
50
50
  statusOptions,
51
51
  Statuses,
52
+ type Status,
52
53
  }
53
-
54
- export type { Status }
@@ -0,0 +1,3 @@
1
+ import { Fragment, createElement } from 'react';
2
+ const DefaultTooltip = ({ children }) => createElement(Fragment, null, children);
3
+ export default DefaultTooltip;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { /* Midi, */ Note } from 'tonal';
3
+ import { Piano as ReactPiano } from 'zds-react-piano';
4
+ import SoundfontProvider from './SoundFontProvider';
5
+ const baseNotes = [
6
+ 'C',
7
+ 'C#',
8
+ 'D',
9
+ 'D#',
10
+ 'E',
11
+ 'F',
12
+ 'F#',
13
+ 'G',
14
+ 'G#',
15
+ 'A',
16
+ 'A#',
17
+ 'B',
18
+ ];
19
+ // const { midiToFreq, midiToNoteName } = Midi
20
+ // console.log(Note.names(), Note.midi('C4'))
21
+ // const firstNote = 21
22
+ // const lastNote = 108
23
+ const numNotes = 12;
24
+ const whiteNotes = numNotes * 0.7; // approx
25
+ const audioContext = new window.AudioContext();
26
+ const OctavePlayer = ({ octave, height, width, ...rest }) => {
27
+ const keyWidth = width / whiteNotes + 2;
28
+ const keyWidthToHeight = keyWidth / height;
29
+ const firstNote = Note.midi(baseNotes[0] + String(octave)) || 0;
30
+ const lastNote = Note.midi(baseNotes[baseNotes.length - 1] + String(octave)) || 0;
31
+ return (_jsx("div", { children: _jsx(ReactPiano, { keyWidthToHeight: keyWidthToHeight, noteRange: { first: firstNote, last: lastNote }, width: width, ...rest }) }));
32
+ };
33
+ const WithProvider = ({ format, height, hostname, instrumentName, octave = 4, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, soundfont, Tooltip, width, }) => (_jsx(SoundfontProvider, { audioContext,
34
+ format,
35
+ hostname,
36
+ instrumentName,
37
+ onChange,
38
+ soundfont, render: ({ isLoading, playNote, stopNote, }) => (_jsxs(_Fragment, { children: [_jsx("div", { children: _jsxs("h2", { children: ["Octave: ", octave] }) }), _jsx(OctavePlayer, { disabled: isLoading, height,
39
+ octave,
40
+ onClick,
41
+ onDoubleClick,
42
+ onKeyMouseEnter,
43
+ onKeyMouseLeave,
44
+ playNote,
45
+ stopNote,
46
+ Tooltip,
47
+ width })] })) }));
48
+ export default WithProvider;
@@ -138,4 +138,4 @@ const WithProvider = ({
138
138
 
139
139
  export default WithProvider
140
140
 
141
- export type { WithProviderProps }
141
+ export type { OctavePlayerProps, WithProviderProps }
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ const MEASUREMENT_ELEMENT_ID = '__react_svg_text_measurement_id';
4
+ const calculateWordsByLines = (text, wordWidths, maxWidth, maxHeight) => {
5
+ const { lineHeight, spaceWidth, wordsWithComputedWidth } = wordWidths;
6
+ return text.split(/\s+/).reduce((result, word) => {
7
+ const wordWidth = wordsWithComputedWidth[word];
8
+ const currentLine = result[result.length - 1];
9
+ if (currentLine && currentLine.width + wordWidth + spaceWidth < maxWidth) {
10
+ // Word can be added to an existing line
11
+ currentLine.words.push(word);
12
+ currentLine.width += wordWidth + spaceWidth;
13
+ }
14
+ else {
15
+ // Add first word to line or word is too long to scaleToFit on existing line
16
+ const newLine = {
17
+ words: [word],
18
+ width: wordWidth,
19
+ showLine: maxHeight
20
+ ? lineHeight * (result.length + 1) < maxHeight
21
+ : true,
22
+ };
23
+ result.push(newLine);
24
+ }
25
+ return result;
26
+ }, []);
27
+ };
28
+ const calculateWordWidths = (style, textNode, text) => {
29
+ if (style && textNode) {
30
+ // biome-ignore lint/complexity/noForEach: <explanation>
31
+ Array.from(style).forEach(key => textNode.style.setProperty(key, style.getPropertyValue(key), style.getPropertyPriority(key)));
32
+ const wordArray = [...new Set(String(text).split(/\s+/))];
33
+ const wordsWithComputedWidth = wordArray.reduce((wordMap, word) => {
34
+ textNode.textContent = word;
35
+ // biome-ignore lint/performance/noAccumulatingSpread: <explanation>
36
+ return { ...wordMap, [word]: textNode.getBBox().width };
37
+ }, {});
38
+ textNode.textContent = '\u00A0';
39
+ const spaceWidth = textNode?.getComputedTextLength?.() || 8;
40
+ const lineHeight = textNode.getBBox().height;
41
+ textNode.setAttribute('style', '');
42
+ return { wordsWithComputedWidth, spaceWidth, lineHeight };
43
+ }
44
+ return undefined;
45
+ };
46
+ const Text = (props) => {
47
+ const { className = '', dx = 0, dy = 0, maxHeight = 1000, maxWidth = 1000,
48
+ // textAnchor = 'start',
49
+ text = '', verticalAnchor = 'start', x = 0, y = 0, transform = '', ...rest } = props;
50
+ const [wordWidths, setWordWidths] = useState();
51
+ const [textLines, setTextLines] = useState([]);
52
+ const [style, setComputedStyle] = useState();
53
+ const measureRef = useRef({});
54
+ const displayedLines = useMemo(() => {
55
+ const result = textLines.filter(({ showLine }) => showLine);
56
+ return result.length
57
+ ? result
58
+ : text
59
+ .split(/\n/)
60
+ .map(line => ({ words: [line], width: 0, showLine: true }));
61
+ }, [textLines, text]);
62
+ const ref = useCallback((node) => {
63
+ setComputedStyle(node !== null ? window.getComputedStyle(node) : undefined);
64
+ }, []);
65
+ useEffect(() => {
66
+ const el = document.getElementById(MEASUREMENT_ELEMENT_ID);
67
+ if (el === null) {
68
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
69
+ svg.setAttribute('id', MEASUREMENT_ELEMENT_ID);
70
+ document.body.appendChild(svg);
71
+ svg.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'text'));
72
+ measureRef.current = svg;
73
+ return () => {
74
+ document.body.removeChild(svg);
75
+ };
76
+ }
77
+ measureRef.current = el;
78
+ }, []);
79
+ useEffect(() => {
80
+ const wordWithWidths = calculateWordWidths(style, measureRef.current, text);
81
+ setWordWidths(wordWithWidths);
82
+ }, [text, style]);
83
+ useEffect(() => {
84
+ if (maxWidth && wordWidths) {
85
+ const lines = text
86
+ .split(/\n/)
87
+ .flatMap(line => calculateWordsByLines(line, wordWidths, maxWidth, maxHeight));
88
+ setTextLines(lines);
89
+ }
90
+ }, [maxHeight, maxWidth, wordWidths, text]);
91
+ const startDy = useMemo(() => {
92
+ if (wordWidths?.lineHeight) {
93
+ switch (verticalAnchor) {
94
+ case 'start':
95
+ return wordWidths.lineHeight;
96
+ case 'middle':
97
+ return -(((displayedLines.length - 1) * wordWidths.lineHeight) / 2);
98
+ default:
99
+ return -(displayedLines.length - 1) * wordWidths.lineHeight;
100
+ }
101
+ }
102
+ return 0;
103
+ }, [displayedLines.length, verticalAnchor, wordWidths?.lineHeight]);
104
+ return (_jsx("text", { className: className, ref: ref, dx: dx, dy: dy, x: x, y: y,
105
+ // textAnchor={textAnchor}
106
+ transform: transform, ...rest, children: _jsxs(_Fragment, { children: [displayedLines.map((line, idx) => (_jsx("tspan", { dx: x + dx, dy: idx === 0 ? startDy + dy : wordWidths?.lineHeight || 0, x: 0, children: line.words.join(' ') }, idx))), textLines.length && displayedLines.length !== textLines.length && (_jsxs(_Fragment, { children: [_jsx("tspan", { children: "..." }), _jsx("title", { children: text })] }))] }) }));
107
+ };
108
+ export default Text;
109
+ export { MEASUREMENT_ELEMENT_ID };
@@ -219,3 +219,5 @@ const Text = (props: TextProps) => {
219
219
  export default Text
220
220
 
221
221
  export { MEASUREMENT_ELEMENT_ID }
222
+
223
+ export type { TextProps, WordWidths, WordsByLine }
@@ -0,0 +1,87 @@
1
+ // See https://github.com/danigb/soundfont-player
2
+ // for more documentation on prop options.
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import Soundfont from 'soundfont-player';
5
+ // import '../temp/acoustic_grand_piano-mp3'
6
+ const isDefined = (item) => item !== undefined && item !== null && !Number.isNaN(item);
7
+ const SoundfontProvider = (props) => {
8
+ const { audioContext, format, hostname, instrumentName, onChange, render, soundfont, } = props;
9
+ const [isLoading, setIsLoading] = useState(true);
10
+ const [activeAudioNodes, setActiveAudioNodes] = useState({});
11
+ const [instrument, setInstrument] = useState(null);
12
+ const loadInstrument = useCallback((nm) => {
13
+ // Re-trigger loading state
14
+ setInstrument(null);
15
+ if (isDefined(hostname)) {
16
+ // pull from an url
17
+ Soundfont.instrument(audioContext, nm, {
18
+ format,
19
+ soundfont,
20
+ nameToUrl: (name, sf, fmt) => `${hostname}/${sf}/${name}-${fmt}.js`,
21
+ }).then(newInstrument => {
22
+ setInstrument(newInstrument);
23
+ });
24
+ }
25
+ else if (isDefined(instrumentName)) {
26
+ // the host app has already injected the soundfont into global space
27
+ Soundfont.instrument(audioContext, instrumentName).then(newInstrument => {
28
+ setInstrument(newInstrument);
29
+ });
30
+ }
31
+ else {
32
+ // sound must be disabled
33
+ setIsLoading(false);
34
+ }
35
+ }, [audioContext, format, hostname, instrumentName, soundfont]);
36
+ useEffect(() => {
37
+ loadInstrument(instrumentName);
38
+ }, [instrumentName, loadInstrument]);
39
+ useEffect(() => {
40
+ if (isDefined(instrumentName)) {
41
+ setIsLoading(!instrument);
42
+ }
43
+ }, [instrument, instrumentName]);
44
+ const playNote = (midiNumber) => {
45
+ if (instrument) {
46
+ audioContext.resume().then(() => {
47
+ const audioNode = instrument.play(String(midiNumber));
48
+ setActiveAudioNodes(prev => ({ ...prev, [midiNumber]: audioNode }));
49
+ });
50
+ }
51
+ };
52
+ const stopNote = (midiNumber) => {
53
+ audioContext.resume().then(() => {
54
+ if (!activeAudioNodes[midiNumber]) {
55
+ return;
56
+ }
57
+ const audioNode = activeAudioNodes[midiNumber];
58
+ audioNode.stop();
59
+ setActiveAudioNodes(prev => {
60
+ const { [midiNumber]: _, ...rest } = prev;
61
+ return rest;
62
+ });
63
+ });
64
+ onChange?.(midiNumber);
65
+ };
66
+ // Clear any residual notes that don't get called with stopNote
67
+ const stopAllNotes = () => {
68
+ audioContext.resume().then(() => {
69
+ const nodes = Object.values(activeAudioNodes);
70
+ for (const node of nodes) {
71
+ if (node) {
72
+ node.stop();
73
+ }
74
+ }
75
+ setActiveAudioNodes(nodes);
76
+ });
77
+ };
78
+ return render({
79
+ isLoading,
80
+ playNote,
81
+ stopNote,
82
+ stopAllNotes,
83
+ });
84
+ };
85
+ // https://gleitz.github.io/midi-js-soundfonts/MusyngKite/acoustic_grand_piano-mp3.js
86
+ // https://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/.js
87
+ export default SoundfontProvider;
@@ -128,3 +128,5 @@ const SoundfontProvider = (props: SoundfontProviderProps) => {
128
128
  // https://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/.js
129
129
 
130
130
  export default SoundfontProvider
131
+
132
+ export type { SoundfontProviderProps }
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import ccValues from '../midi/ccValues';
4
+ import Select from './Select';
5
+ const CCPicker = forwardRef((props, ref) => (_jsx(Select, { ...props, options: ccValues, ref: ref })));
6
+ export default CCPicker;