@tremolo-ui/react 0.1.4 → 0.1.6
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/index.cjs +285 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +80 -32
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +95 -60
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +97 -62
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +262 -139
- package/dist/index.js.map +1 -1
- package/package.json +14 -9
- package/src/components/AnimationCanvas/canvas.ts +16 -0
- package/src/components/AnimationCanvas/index.tsx +54 -71
- package/src/components/Knob/index.css +54 -23
- package/src/components/Knob/index.tsx +30 -25
- package/src/components/NumberInput/InternalInput.tsx +1 -1
- package/src/components/NumberInput/context.tsx +2 -1
- package/src/components/NumberInput/index.css +1 -0
- package/src/components/NumberInput/index.tsx +5 -5
- package/src/components/NumberInput/type.ts +58 -0
- package/src/components/Piano/context.tsx +1 -1
- package/src/components/Piano/index.css +24 -8
- package/src/components/Piano/index.tsx +254 -219
- package/src/components/Piano/key.tsx +3 -3
- package/src/components/Slider/Scale.tsx +1 -1
- package/src/components/Slider/ScaleOption.tsx +1 -1
- package/src/components/Slider/Track.tsx +2 -2
- package/src/components/Slider/index.tsx +3 -2
- package/src/components/Slider/type.ts +26 -0
- package/src/components/XYPad/index.tsx +2 -2
- package/src/hooks/useAnimationFrame.ts +3 -0
- package/src/hooks/useCallbackRef.ts +2 -2
- package/src/hooks/useDrag.ts +2 -1
- package/src/hooks/useDragWithElement.ts +2 -1
- package/src/hooks/useEventListener.ts +3 -0
- package/src/hooks/useInterval.ts +3 -0
- package/src/hooks/useLongPress.ts +3 -0
- package/src/hooks/useMIDIAccess.ts +40 -0
- package/src/hooks/useMIDIInput.ts +50 -0
- package/src/hooks/useMIDIMessage.ts +24 -0
- package/src/hooks/useRefCallbackEvent.ts +4 -0
- package/src/index.ts +21 -13
- package/src/styles/{index.css → global.css} +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tremolo-ui/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "React component library for audio app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"serve": "http-server ./storybook-static",
|
|
17
17
|
"lint": "eslint --fix --ext .ts,.tsx .",
|
|
18
18
|
"format": "prettier --write .",
|
|
19
|
-
"test": "
|
|
19
|
+
"test": "jest",
|
|
20
20
|
"build:package": "tsc && tsdown",
|
|
21
21
|
"clean": "npm run clean:build && npm run clean:sb",
|
|
22
22
|
"clean:build": "rimraf ./dist",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"react": "^19",
|
|
40
40
|
"react-dom": "^19",
|
|
41
41
|
"storybook": "^8.6.7",
|
|
42
|
-
"tone": "^15
|
|
42
|
+
"tone": "^15"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"react": "^18 ||^19",
|
|
46
46
|
"react-dom": "^18 || ^19"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@tremolo-ui/functions": "^0.1.
|
|
49
|
+
"@tremolo-ui/functions": "^0.1.6",
|
|
50
50
|
"clsx": "^2.1.1",
|
|
51
51
|
"zustand": "^5.0.3"
|
|
52
52
|
},
|
|
@@ -78,14 +78,19 @@
|
|
|
78
78
|
"exports": {
|
|
79
79
|
".": {
|
|
80
80
|
"require": {
|
|
81
|
-
"
|
|
82
|
-
"
|
|
81
|
+
"types": "./dist/index.d.cts",
|
|
82
|
+
"default": "./dist/index.cjs"
|
|
83
83
|
},
|
|
84
84
|
"import": {
|
|
85
|
-
"
|
|
86
|
-
"
|
|
85
|
+
"types": "./dist/index.d.ts",
|
|
86
|
+
"default": "./dist/index.js"
|
|
87
87
|
}
|
|
88
88
|
},
|
|
89
|
-
"./index.css": "./dist/index.css"
|
|
89
|
+
"./styles/index.css": "./dist/index.css",
|
|
90
|
+
"./styles/global.css": "./src/styles/global.css",
|
|
91
|
+
"./styles/Knob.css": "./src/components/Knob/index.css",
|
|
92
|
+
"./styles/NumberInput.css": "./src/components/NumberInput/index.css",
|
|
93
|
+
"./styles/Piano.css": "./src/components/Piano/index.css",
|
|
94
|
+
"./styles/XYPad.css": "./src/components/XYPad/index.css"
|
|
90
95
|
}
|
|
91
96
|
}
|
|
@@ -28,3 +28,19 @@ export function isDrawingState(obj: unknown): obj is DrawingState {
|
|
|
28
28
|
const d: string[] = [...drawingState]
|
|
29
29
|
return typeof obj == 'string' && d.includes(obj)
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
export function setDprConfig(
|
|
33
|
+
canvas: HTMLCanvasElement,
|
|
34
|
+
context: CanvasRenderingContext2D,
|
|
35
|
+
width: number,
|
|
36
|
+
height: number,
|
|
37
|
+
dpr: number,
|
|
38
|
+
) {
|
|
39
|
+
canvas.width = width * dpr
|
|
40
|
+
canvas.height = height * dpr
|
|
41
|
+
// Reset current transformation matrix to the identity matrix
|
|
42
|
+
context.setTransform(1, 0, 0, 1, 0, 0)
|
|
43
|
+
context.scale(dpr, dpr)
|
|
44
|
+
canvas.style.width = `${width}px`
|
|
45
|
+
canvas.style.height = `${height}px`
|
|
46
|
+
}
|
|
@@ -8,30 +8,12 @@ import {
|
|
|
8
8
|
useRef,
|
|
9
9
|
} from 'react'
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
RelativeSizingPropsのの場合、relativeSizeは必須、reduceFlickeringはオプショナル
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
function setDprConfig(
|
|
21
|
-
canvas: HTMLCanvasElement,
|
|
22
|
-
context: CanvasRenderingContext2D,
|
|
23
|
-
width: number,
|
|
24
|
-
height: number,
|
|
25
|
-
dpr: number,
|
|
26
|
-
) {
|
|
27
|
-
canvas.width = width * dpr
|
|
28
|
-
canvas.height = height * dpr
|
|
29
|
-
// Reset current transformation matrix to the identity matrix
|
|
30
|
-
context.setTransform(1, 0, 0, 1, 0, 0)
|
|
31
|
-
context.scale(dpr, dpr)
|
|
32
|
-
canvas.style.width = `${width}px`
|
|
33
|
-
canvas.style.height = `${height}px`
|
|
34
|
-
}
|
|
11
|
+
import {
|
|
12
|
+
DrawingContext,
|
|
13
|
+
drawingState,
|
|
14
|
+
DrawingStateValue,
|
|
15
|
+
setDprConfig,
|
|
16
|
+
} from './canvas'
|
|
35
17
|
|
|
36
18
|
/** @category AnimationCanvas */
|
|
37
19
|
export type InitFunction = (
|
|
@@ -46,8 +28,6 @@ export type InitFunction = (
|
|
|
46
28
|
|
|
47
29
|
/** @category AnimationCanvas */
|
|
48
30
|
export type DrawFunction = (
|
|
49
|
-
// TODO: saving scale and translate
|
|
50
|
-
// context: Omit<CanvasRenderingContext2D, 'save' | 'restore' | 'scale'>,
|
|
51
31
|
context: CanvasRenderingContext2D,
|
|
52
32
|
option: {
|
|
53
33
|
/** current canvas width */
|
|
@@ -67,12 +47,13 @@ export type DrawFunction = (
|
|
|
67
47
|
|
|
68
48
|
/** @category AnimationCanvas */
|
|
69
49
|
export interface CommonProps {
|
|
50
|
+
draw: DrawFunction
|
|
51
|
+
init?: InitFunction
|
|
52
|
+
animate?: boolean
|
|
70
53
|
/**
|
|
71
54
|
* @see https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/getContext#contextattributes
|
|
72
55
|
*/
|
|
73
56
|
options?: CanvasRenderingContext2DSettings
|
|
74
|
-
init?: InitFunction
|
|
75
|
-
draw: DrawFunction
|
|
76
57
|
}
|
|
77
58
|
|
|
78
59
|
/** @category AnimationCanvas */
|
|
@@ -108,12 +89,13 @@ export function AnimationCanvas(
|
|
|
108
89
|
): ReactElement
|
|
109
90
|
export function AnimationCanvas({
|
|
110
91
|
// common
|
|
111
|
-
options,
|
|
112
|
-
init,
|
|
113
92
|
draw,
|
|
93
|
+
init,
|
|
94
|
+
animate = true,
|
|
95
|
+
options,
|
|
114
96
|
// absolute
|
|
115
|
-
width = 100,
|
|
116
|
-
height = 100,
|
|
97
|
+
width: _width = 100,
|
|
98
|
+
height: _height = 100,
|
|
117
99
|
// relative
|
|
118
100
|
relativeSize,
|
|
119
101
|
reduceFlickering = true,
|
|
@@ -145,9 +127,11 @@ export function AnimationCanvas({
|
|
|
145
127
|
const deltaTime = now - deltaMemoRef.current
|
|
146
128
|
const elapsedTime = now - startTimeRef.current
|
|
147
129
|
deltaMemoRef.current = now
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
130
|
+
if (animate) {
|
|
131
|
+
reqIdRef.current = requestAnimationFrame(() =>
|
|
132
|
+
loop(context, width, height, count + 1),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
151
135
|
draw(context, {
|
|
152
136
|
width: width.current,
|
|
153
137
|
height: height.current,
|
|
@@ -157,7 +141,7 @@ export function AnimationCanvas({
|
|
|
157
141
|
fps: 1000 / deltaTime,
|
|
158
142
|
})
|
|
159
143
|
},
|
|
160
|
-
[draw],
|
|
144
|
+
[draw, animate],
|
|
161
145
|
)
|
|
162
146
|
|
|
163
147
|
useEffect(() => {
|
|
@@ -167,6 +151,18 @@ export function AnimationCanvas({
|
|
|
167
151
|
if (!context) throw new Error('Cannot get canvas context.')
|
|
168
152
|
const dpr = globalThis.devicePixelRatio
|
|
169
153
|
|
|
154
|
+
const firstRendering = (width: number, height: number) => {
|
|
155
|
+
setDprConfig(canvas, context, width, height, dpr)
|
|
156
|
+
widthRef.current = width
|
|
157
|
+
heightRef.current = height
|
|
158
|
+
|
|
159
|
+
if (init) init(context, { width, height })
|
|
160
|
+
const now = performance.now()
|
|
161
|
+
deltaMemoRef.current = now
|
|
162
|
+
startTimeRef.current = now
|
|
163
|
+
loop(context, widthRef, heightRef, -1)
|
|
164
|
+
}
|
|
165
|
+
|
|
170
166
|
if (relativeSize) {
|
|
171
167
|
const parent = canvas.parentElement
|
|
172
168
|
if (!parent) throw new Error("Canvas doesn't have a parent element.")
|
|
@@ -175,16 +171,14 @@ export function AnimationCanvas({
|
|
|
175
171
|
|
|
176
172
|
const ro = new ResizeObserver((entries) => {
|
|
177
173
|
for (const entry of entries) {
|
|
178
|
-
// Prevents loss of some context when the canvas is resized
|
|
179
|
-
const contextMemo = {} as DrawingContext
|
|
180
|
-
for (const prop of drawingState) {
|
|
181
|
-
;(contextMemo[prop] as DrawingStateValue) = context[prop]
|
|
182
|
-
}
|
|
183
|
-
|
|
184
174
|
const w = entry.contentRect.width
|
|
185
175
|
const h = entry.contentRect.height
|
|
186
|
-
|
|
176
|
+
const contextMemo = {} as DrawingContext
|
|
187
177
|
if (reduceFlickering && memoCanvas && memoContext) {
|
|
178
|
+
// Prevents loss of some context when the canvas is resized
|
|
179
|
+
for (const prop of drawingState) {
|
|
180
|
+
;(contextMemo[prop] as DrawingStateValue) = context[prop]
|
|
181
|
+
}
|
|
188
182
|
memoCanvas.width = w * dpr
|
|
189
183
|
memoCanvas.height = h * dpr
|
|
190
184
|
memoContext.scale(1 / dpr, 1 / dpr)
|
|
@@ -197,10 +191,6 @@ export function AnimationCanvas({
|
|
|
197
191
|
widthRef.current = w
|
|
198
192
|
heightRef.current = h
|
|
199
193
|
|
|
200
|
-
for (const prop of drawingState) {
|
|
201
|
-
;(context[prop] as DrawingStateValue) = contextMemo[prop]
|
|
202
|
-
}
|
|
203
|
-
|
|
204
194
|
if (
|
|
205
195
|
reduceFlickering &&
|
|
206
196
|
memoContext &&
|
|
@@ -208,52 +198,45 @@ export function AnimationCanvas({
|
|
|
208
198
|
memoCanvas.width > 0 &&
|
|
209
199
|
memoCanvas.height > 0
|
|
210
200
|
) {
|
|
201
|
+
for (const prop of drawingState) {
|
|
202
|
+
;(context[prop] as DrawingStateValue) = contextMemo[prop]
|
|
203
|
+
}
|
|
204
|
+
|
|
211
205
|
context.drawImage(memoContext.canvas, 0, 0)
|
|
212
206
|
}
|
|
207
|
+
|
|
208
|
+
if (!animate) {
|
|
209
|
+
// re rendering
|
|
210
|
+
loop(context, widthRef, heightRef, -1)
|
|
211
|
+
}
|
|
213
212
|
}
|
|
214
213
|
})
|
|
215
214
|
ro.observe(parent)
|
|
216
215
|
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (init)
|
|
222
|
-
init(context, { width: widthRef.current, height: heightRef.current })
|
|
223
|
-
const now = performance.now()
|
|
224
|
-
deltaMemoRef.current = now
|
|
225
|
-
startTimeRef.current = now
|
|
226
|
-
loop(context, widthRef, heightRef, -1)
|
|
216
|
+
const width = parent.clientWidth
|
|
217
|
+
const height = parent.clientHeight
|
|
218
|
+
firstRendering(width, height)
|
|
227
219
|
|
|
228
220
|
return () => {
|
|
229
221
|
if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current)
|
|
230
222
|
ro.disconnect()
|
|
231
223
|
}
|
|
232
224
|
} else {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
widthRef.current = rect.width
|
|
236
|
-
heightRef.current = rect.height
|
|
237
|
-
|
|
238
|
-
if (init)
|
|
239
|
-
init(context, { width: widthRef.current, height: heightRef.current })
|
|
240
|
-
const now = performance.now()
|
|
241
|
-
deltaMemoRef.current = now
|
|
242
|
-
startTimeRef.current = now
|
|
243
|
-
loop(context, widthRef, heightRef, -1)
|
|
225
|
+
const { width, height } = canvas.getBoundingClientRect()
|
|
226
|
+
firstRendering(width, height)
|
|
244
227
|
|
|
245
228
|
return () => {
|
|
246
229
|
if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current)
|
|
247
230
|
}
|
|
248
231
|
}
|
|
249
|
-
}, [loop, init, options, reduceFlickering, relativeSize])
|
|
232
|
+
}, [loop, init, options, reduceFlickering, relativeSize, animate])
|
|
250
233
|
|
|
251
234
|
return (
|
|
252
235
|
<>
|
|
253
236
|
<canvas
|
|
254
237
|
className={clsx('tremolo-animation-canvas', className)}
|
|
255
|
-
width={relativeSize ? 0 :
|
|
256
|
-
height={relativeSize ? 0 :
|
|
238
|
+
width={relativeSize ? 0 : _width}
|
|
239
|
+
height={relativeSize ? 0 : _height}
|
|
257
240
|
ref={canvasRef}
|
|
258
241
|
onContextMenu={onContextMenu}
|
|
259
242
|
{...props}
|
|
@@ -4,45 +4,76 @@
|
|
|
4
4
|
overflow: visible;
|
|
5
5
|
outline: 0;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
7
|
+
&[aria-readonly='true'] {
|
|
8
|
+
cursor: not-allowed;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
.tremolo-knob-active-line {
|
|
13
|
+
overflow: visible;
|
|
14
|
+
color: oklch(69.2% 0.17548 266.373);
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
16
|
+
:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
|
|
17
|
+
color: var(--tremolo-theme-color);
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
:where(.tremolo-knob[aria-disabled='true']) & {
|
|
21
|
+
color: oklch(50.5% 0.03307 270.021);
|
|
24
22
|
}
|
|
23
|
+
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
:where(.dark, [data-theme='dark']) {
|
|
26
|
+
.tremolo-knob-active-line {
|
|
27
|
+
color: var(--tremolo-theme-color);
|
|
28
|
+
|
|
29
|
+
:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
|
|
30
|
+
color: oklch(65.2% 0.17548 266.373);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
.tremolo-knob-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
.tremolo-knob-inactive-line {
|
|
36
|
+
color: oklch(95% 0 0);
|
|
37
|
+
|
|
38
|
+
:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
|
|
39
|
+
color: oklch(90% 0 0);
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
.
|
|
39
|
-
|
|
43
|
+
:where(.dark, [data-theme='dark']) {
|
|
44
|
+
.tremolo-knob-inactive-line {
|
|
45
|
+
color: oklch(41.7% 0 0);
|
|
46
|
+
|
|
47
|
+
:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
|
|
48
|
+
color: oklch(44.59% 0 0);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
.tremolo-knob-thumb {
|
|
43
|
-
color:
|
|
54
|
+
color: oklch(85.7% 0 0);
|
|
55
|
+
|
|
56
|
+
:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
|
|
57
|
+
color: oklch(84.5% 0 0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
:where(.dark, [data-theme='dark']) {
|
|
62
|
+
.tremolo-knob-thumb {
|
|
63
|
+
color: oklch(48.6% 0 0);
|
|
64
|
+
|
|
65
|
+
:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
|
|
66
|
+
color: oklch(51.7% 0 0);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
44
69
|
}
|
|
45
70
|
|
|
46
71
|
.tremolo-knob-thumb-line {
|
|
47
|
-
color:
|
|
72
|
+
color: oklch(94.2% 0 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
:where(.dark, [data-theme='dark']) {
|
|
76
|
+
.tremolo-knob-thumb-line {
|
|
77
|
+
color: oklch(84.2% 0 0);
|
|
78
|
+
}
|
|
48
79
|
}
|
|
@@ -43,8 +43,8 @@ export interface KnobProps {
|
|
|
43
43
|
*/
|
|
44
44
|
startValue?: number
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
size?: number
|
|
46
|
+
/** width and height */
|
|
47
|
+
size?: number | string
|
|
48
48
|
|
|
49
49
|
/** Whether to apply `{use-select: none}` when dragging */
|
|
50
50
|
bodyNoSelect?: boolean
|
|
@@ -69,15 +69,16 @@ export interface KnobProps {
|
|
|
69
69
|
thumb?: string
|
|
70
70
|
/** color */
|
|
71
71
|
thumbLine?: string
|
|
72
|
-
|
|
73
72
|
/** percent (0-100) */
|
|
74
73
|
thumbSize?: number
|
|
75
74
|
/** percent (0-100) */
|
|
76
75
|
thumbLineWeight?: number
|
|
77
76
|
/** percent (0-100) */
|
|
78
77
|
thumbLineLength?: number
|
|
79
|
-
/**
|
|
78
|
+
/** line weight [px] */
|
|
80
79
|
lineWeight?: number
|
|
80
|
+
/** angle range [degree] */
|
|
81
|
+
angleRange?: number
|
|
81
82
|
|
|
82
83
|
classes?: {
|
|
83
84
|
activeLine?: string
|
|
@@ -125,6 +126,11 @@ export const Knob = forwardRef<KnobMethods, Props>(
|
|
|
125
126
|
inactiveLine,
|
|
126
127
|
thumb,
|
|
127
128
|
thumbLine,
|
|
129
|
+
thumbSize = 84,
|
|
130
|
+
thumbLineWeight = 6,
|
|
131
|
+
thumbLineLength = 35,
|
|
132
|
+
lineWeight = 6,
|
|
133
|
+
angleRange = 270,
|
|
128
134
|
onChange,
|
|
129
135
|
onKeyDown,
|
|
130
136
|
onPointerDown,
|
|
@@ -133,26 +139,18 @@ export const Knob = forwardRef<KnobMethods, Props>(
|
|
|
133
139
|
classes,
|
|
134
140
|
...props
|
|
135
141
|
}: Props,
|
|
136
|
-
|
|
142
|
+
forwardedRef,
|
|
137
143
|
) => {
|
|
138
144
|
const valueRef = useRef(0)
|
|
139
145
|
const elmRef = useRef<HTMLOrSVGElement>(null)
|
|
140
146
|
|
|
141
|
-
// --- interpret props ---
|
|
142
|
-
const padding = 8 // %
|
|
143
|
-
const thumbLineWeight = 6 // %
|
|
144
|
-
const thumbLineLength = 35 // %
|
|
145
|
-
const lineWeight = size * 0.06 // px
|
|
146
|
-
const p = normalizeValue(value, min, max, skew)
|
|
147
|
-
const s = normalizeValue(startValue, min, max, skew)
|
|
148
|
-
|
|
149
147
|
// --- internal functions ---
|
|
150
148
|
const onDrag = useCallback(
|
|
151
149
|
(_x: number, y: number) => {
|
|
152
150
|
if (!onChange || readonly) return
|
|
153
151
|
const v = rawValue(valueRef.current - y / 100, min, max, skew)
|
|
154
|
-
const
|
|
155
|
-
onChange(
|
|
152
|
+
const clamped = clamp(stepValue(v, step), min, max)
|
|
153
|
+
onChange(clamped)
|
|
156
154
|
},
|
|
157
155
|
[max, min, onChange, readonly, skew, step],
|
|
158
156
|
)
|
|
@@ -210,7 +208,7 @@ export const Knob = forwardRef<KnobMethods, Props>(
|
|
|
210
208
|
[wheel, onChange, readonly, updateValueByEvent],
|
|
211
209
|
)
|
|
212
210
|
|
|
213
|
-
useImperativeHandle(
|
|
211
|
+
useImperativeHandle(forwardedRef, () => {
|
|
214
212
|
return {
|
|
215
213
|
focus() {
|
|
216
214
|
elmRef.current?.focus()
|
|
@@ -221,11 +219,17 @@ export const Knob = forwardRef<KnobMethods, Props>(
|
|
|
221
219
|
}
|
|
222
220
|
}, [])
|
|
223
221
|
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
const
|
|
222
|
+
const viewBoxSize = 100
|
|
223
|
+
const center = viewBoxSize / 2
|
|
224
|
+
const padding = (viewBoxSize - clamp(thumbSize, 0, 100)) / 2 // %
|
|
225
|
+
const p = normalizeValue(value, min, max, skew)
|
|
226
|
+
const s = normalizeValue(startValue, min, max, skew)
|
|
227
|
+
|
|
228
|
+
// 時計回りで描画していく
|
|
229
|
+
const r1 = -angleRange / 2 // ロータリー開始位置
|
|
230
|
+
const r2 = r1 + Math.min(p, s) * angleRange // activeLineの開始位置
|
|
231
|
+
const r3 = r1 + Math.max(p, s) * angleRange // activeLineの終了位置
|
|
232
|
+
const r4 = angleRange / 2 // ロータリー終了位置
|
|
229
233
|
const x1 = center + center * Math.cos(radian(r1 - 90))
|
|
230
234
|
const y1 = center + center * Math.sin(radian(r1 - 90))
|
|
231
235
|
const x2 = center + center * Math.cos(radian(r2 - 90))
|
|
@@ -237,12 +241,13 @@ export const Knob = forwardRef<KnobMethods, Props>(
|
|
|
237
241
|
|
|
238
242
|
return (
|
|
239
243
|
<svg
|
|
240
|
-
className={clsx('tremolo-knob', className)}
|
|
241
244
|
ref={(div) => {
|
|
242
245
|
elmRef.current = div
|
|
243
246
|
wheelRefCallback(div)
|
|
244
247
|
touchMoveRefCallback(div)
|
|
245
248
|
}}
|
|
249
|
+
className={clsx('tremolo-knob', className)}
|
|
250
|
+
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
|
246
251
|
width={size}
|
|
247
252
|
height={size}
|
|
248
253
|
tabIndex={0}
|
|
@@ -268,7 +273,7 @@ export const Knob = forwardRef<KnobMethods, Props>(
|
|
|
268
273
|
}}
|
|
269
274
|
{...props}
|
|
270
275
|
>
|
|
271
|
-
{/*
|
|
276
|
+
{/* lines */}
|
|
272
277
|
{startValue > min && (
|
|
273
278
|
<path
|
|
274
279
|
className={clsx(
|
|
@@ -305,7 +310,7 @@ export const Knob = forwardRef<KnobMethods, Props>(
|
|
|
305
310
|
<circle
|
|
306
311
|
cx="50%"
|
|
307
312
|
cy="50%"
|
|
308
|
-
r={`${
|
|
313
|
+
r={`${thumbSize / 2}%`}
|
|
309
314
|
fill={thumb || 'currentColor'}
|
|
310
315
|
/>
|
|
311
316
|
<line
|
|
@@ -320,7 +325,7 @@ export const Knob = forwardRef<KnobMethods, Props>(
|
|
|
320
325
|
// https://bugs.webkit.org/show_bug.cgi?id=201854
|
|
321
326
|
// https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/transform-origin#browser_compatibility
|
|
322
327
|
style={{
|
|
323
|
-
transform: `rotate(${
|
|
328
|
+
transform: `rotate(${r1 + p * angleRange}deg)`,
|
|
324
329
|
transformOrigin: '50% 50%',
|
|
325
330
|
}}
|
|
326
331
|
/>
|
|
@@ -16,11 +16,11 @@ import {
|
|
|
16
16
|
rawValue,
|
|
17
17
|
stepValue,
|
|
18
18
|
} from '@tremolo-ui/functions'
|
|
19
|
-
import { parseValue } from '@tremolo-ui/functions/NumberInput'
|
|
20
19
|
|
|
21
20
|
import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
|
|
22
21
|
|
|
23
22
|
import { safeClamp, useNumberInputContext } from './context'
|
|
23
|
+
import { parseValue } from './type'
|
|
24
24
|
|
|
25
25
|
import { NumberInputMethods } from '.'
|
|
26
26
|
|
|
@@ -2,7 +2,8 @@ import { createContext, useContext, useEffect, useRef } from 'react'
|
|
|
2
2
|
import { createStore, useStore } from 'zustand'
|
|
3
3
|
|
|
4
4
|
import { clamp } from '@tremolo-ui/functions'
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
import { parseValue, Units } from './type'
|
|
6
7
|
|
|
7
8
|
type State = {
|
|
8
9
|
value: string
|
|
@@ -2,10 +2,10 @@ import clsx from 'clsx'
|
|
|
2
2
|
import { ComponentPropsWithoutRef, forwardRef, ReactNode } from 'react'
|
|
3
3
|
|
|
4
4
|
import { InputEventOption } from '@tremolo-ui/functions'
|
|
5
|
-
import { Units } from '@tremolo-ui/functions/NumberInput'
|
|
6
5
|
|
|
7
6
|
import { NumberInputProvider } from './context'
|
|
8
7
|
import { InternalInput } from './InternalInput'
|
|
8
|
+
import { Units } from './type'
|
|
9
9
|
|
|
10
10
|
/*
|
|
11
11
|
TODO
|
|
@@ -55,7 +55,6 @@ export interface NumberInputProps {
|
|
|
55
55
|
keepWithinRange?: boolean
|
|
56
56
|
clampValueOnBlur?: boolean
|
|
57
57
|
|
|
58
|
-
// TODO
|
|
59
58
|
/**
|
|
60
59
|
* wheel control option
|
|
61
60
|
*/
|
|
@@ -126,9 +125,9 @@ export const NumberInput = forwardRef<NumberInputMethods, Props>(
|
|
|
126
125
|
children,
|
|
127
126
|
...props
|
|
128
127
|
}: Props,
|
|
129
|
-
|
|
128
|
+
forwardedRef,
|
|
130
129
|
) => {
|
|
131
|
-
const colors:
|
|
130
|
+
const colors: Record<string, string | undefined> = {
|
|
132
131
|
'--active-color': activeColor,
|
|
133
132
|
}
|
|
134
133
|
return (
|
|
@@ -154,7 +153,7 @@ export const NumberInput = forwardRef<NumberInputMethods, Props>(
|
|
|
154
153
|
data-variant={variant}
|
|
155
154
|
>
|
|
156
155
|
<InternalInput
|
|
157
|
-
ref={
|
|
156
|
+
ref={forwardedRef}
|
|
158
157
|
readonly={readonly}
|
|
159
158
|
disabled={disabled}
|
|
160
159
|
selectWithFocus={selectWithFocus}
|
|
@@ -178,3 +177,4 @@ export const NumberInput = forwardRef<NumberInputMethods, Props>(
|
|
|
178
177
|
export * from './Stepper'
|
|
179
178
|
export * from './IncrementStepper'
|
|
180
179
|
export * from './DecrementStepper'
|
|
180
|
+
export { type Units } from './type'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [unit, scale][]
|
|
3
|
+
* @example
|
|
4
|
+
* [['Hz', 1], ['kHz', 1000]]
|
|
5
|
+
*/
|
|
6
|
+
export type Units = [string, number][]
|
|
7
|
+
|
|
8
|
+
export function selectUnit(units: Units, value: number): [string, number] {
|
|
9
|
+
let i = 0
|
|
10
|
+
for (; i < units.length; i++) {
|
|
11
|
+
if (Math.abs(units[i][1]) > Math.abs(value)) break
|
|
12
|
+
}
|
|
13
|
+
return units[Math.max(0, i - 1)]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseValue(
|
|
17
|
+
inputString: string,
|
|
18
|
+
units?: string | Units,
|
|
19
|
+
digit?: number,
|
|
20
|
+
): {
|
|
21
|
+
rawValue: number
|
|
22
|
+
formatValue: string
|
|
23
|
+
unit: string
|
|
24
|
+
} {
|
|
25
|
+
const str = inputString.trim()
|
|
26
|
+
if (!units || typeof units == 'string') {
|
|
27
|
+
const m = str.match(/-?\d+(\.\d+)?/)
|
|
28
|
+
let v = Number(m?.[0] ?? '0')
|
|
29
|
+
v = isNaN(v) ? 0 : v
|
|
30
|
+
return {
|
|
31
|
+
rawValue: v,
|
|
32
|
+
formatValue: (digit != undefined ? v.toFixed(digit) : v) + (units ?? ''),
|
|
33
|
+
unit: units ?? '',
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const unitList = units.map(([u, _s]) => u)
|
|
38
|
+
const scaleList = units.map(([_u, s]) => s)
|
|
39
|
+
let rawValue = 0
|
|
40
|
+
let unit = unitList[0]
|
|
41
|
+
let formatValue =
|
|
42
|
+
(digit != undefined ? rawValue.toFixed(digit) : rawValue) + unit
|
|
43
|
+
const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/)
|
|
44
|
+
if (m) {
|
|
45
|
+
rawValue = Number(m[1]) || 0
|
|
46
|
+
const uIndex = unitList.indexOf(m[3])
|
|
47
|
+
rawValue *= uIndex != -1 ? scaleList[uIndex] : 1
|
|
48
|
+
const [u, scale] = selectUnit(units, rawValue)
|
|
49
|
+
const v = rawValue / scale
|
|
50
|
+
formatValue = (digit != undefined ? v.toFixed(digit) : v) + u
|
|
51
|
+
unit = u
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
rawValue: rawValue,
|
|
55
|
+
formatValue: formatValue,
|
|
56
|
+
unit: unit,
|
|
57
|
+
}
|
|
58
|
+
}
|