@tremolo-ui/react 0.5.0 → 0.6.0
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/compose-refs.cjs +72 -0
- package/dist/compose-refs.cjs.map +1 -0
- package/dist/compose-refs.d.cts +43 -0
- package/dist/compose-refs.d.cts.map +1 -0
- package/dist/compose-refs.d.ts +43 -0
- package/dist/compose-refs.d.ts.map +1 -0
- package/dist/compose-refs.js +70 -0
- package/dist/compose-refs.js.map +1 -0
- package/dist/index.cjs +1468 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1103 -435
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1103 -435
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1457 -534
- package/dist/index.js.map +1 -1
- package/package.json +28 -26
- package/src/components/AnimationCanvas/index.tsx +115 -76
- package/src/components/Knob/ActiveLine.tsx +7 -6
- package/src/components/Knob/InactiveLine.tsx +9 -10
- package/src/components/Knob/SVGRoot.tsx +5 -3
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +17 -1
- package/src/components/Knob/index.tsx +148 -55
- package/src/components/NumberInput/DecrementStepper.tsx +8 -4
- package/src/components/NumberInput/IncrementStepper.tsx +9 -4
- package/src/components/NumberInput/InputField.tsx +188 -105
- package/src/components/NumberInput/Stepper.tsx +123 -72
- package/src/components/NumberInput/{stepperButton.tsx → StepperButton.tsx} +27 -22
- package/src/components/NumberInput/context.tsx +35 -8
- package/src/components/NumberInput/index.tsx +272 -75
- package/src/components/Piano/index.tsx +354 -188
- package/src/components/PointsEditor/Background.tsx +11 -2
- package/src/components/PointsEditor/Container.tsx +87 -27
- package/src/components/PointsEditor/Point.tsx +239 -79
- package/src/components/PointsEditor/SelectionBox.tsx +54 -0
- package/src/components/PointsEditor/context.tsx +78 -10
- package/src/components/PointsEditor/index.tsx +440 -39
- package/src/components/Slider/Marks.tsx +48 -16
- package/src/components/Slider/MarksOption.tsx +57 -43
- package/src/components/Slider/Thumb.tsx +108 -50
- package/src/components/Slider/Track.tsx +69 -58
- package/src/components/Slider/context.tsx +4 -2
- package/src/components/Slider/index.tsx +131 -65
- package/src/components/Slider/type.ts +26 -8
- package/src/components/XYPad/Area.tsx +45 -35
- package/src/components/XYPad/Thumb.tsx +135 -62
- package/src/components/XYPad/context.tsx +4 -1
- package/src/components/XYPad/index.tsx +180 -48
- package/src/components/_util/Placement.tsx +68 -0
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
- package/src/components/_util/css-length.ts +13 -0
- package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
- package/src/hooks/_internal/useCallbackRef.ts +3 -1
- package/src/hooks/_internal/useCheckSteps.ts +170 -0
- package/src/hooks/useAnimationFrame.ts +17 -7
- package/src/hooks/useDrag.ts +38 -8
- package/src/hooks/useDragValue.ts +54 -9
- package/src/hooks/useEventListener.ts +10 -6
- package/src/hooks/useLongPress.ts +42 -8
- package/src/hooks/useMIDIAccess.ts +82 -11
- package/src/hooks/useMIDIInput.ts +45 -13
- package/src/hooks/useMIDIMessage.ts +23 -5
- package/src/index.ts +38 -21
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -501
- package/src/Globals.d.ts +0 -3
- package/src/components/Knob/index.css +0 -81
- package/src/components/NumberInput/index.css +0 -121
- package/src/components/Piano/index.css +0 -90
- package/src/components/PointsEditor/index.css +0 -35
- package/src/components/Slider/index.css +0 -125
- package/src/components/XYPad/index.css +0 -60
- package/src/components/_util/index.ts +0 -28
- package/src/styles/global.css +0 -19
- /package/src/components/Piano/{keyboardShortcuts.ts → keyboard-shortcuts.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tremolo-ui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "React component library for audio app",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"sideEffects":
|
|
7
|
-
"*.css"
|
|
8
|
-
],
|
|
6
|
+
"sideEffects": false,
|
|
9
7
|
"main": "dist/index.cjs",
|
|
10
8
|
"module": "dist/index.js",
|
|
11
9
|
"types": "dist/index.d.cts",
|
|
12
10
|
"scripts": {
|
|
13
|
-
"sb": "storybook dev
|
|
11
|
+
"sb": "storybook dev",
|
|
14
12
|
"typecheck": "tsc",
|
|
15
|
-
"build:sb": "storybook build",
|
|
13
|
+
"build:sb": "storybook build -o storybook-static/i/storybook-react && cp .storybook/_headers storybook-static/_headers",
|
|
16
14
|
"serve": "http-server ./storybook-static",
|
|
17
15
|
"lint": "eslint --fix --ext .ts,.tsx .",
|
|
18
16
|
"format": "prettier --write .",
|
|
19
|
-
"test": "
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
20
19
|
"build:package": "tsc && tsdown",
|
|
21
20
|
"clean": "npm run clean:build && npm run clean:sb",
|
|
22
21
|
"clean:build": "rimraf ./dist",
|
|
@@ -24,31 +23,32 @@
|
|
|
24
23
|
},
|
|
25
24
|
"devDependencies": {
|
|
26
25
|
"@storybook/addon-a11y": "10.6.0",
|
|
26
|
+
"@storybook/addon-docs": "10.6.0",
|
|
27
27
|
"@storybook/addon-links": "10.6.0",
|
|
28
28
|
"@storybook/react-vite": "10.6.0",
|
|
29
|
+
"@testing-library/jest-dom": "7.0.1",
|
|
29
30
|
"@testing-library/react": "16.3.3",
|
|
30
|
-
"@types/react": "19.
|
|
31
|
-
"@types/react-dom": "19.
|
|
32
|
-
"jest-environment-jsdom": "29.7.0",
|
|
31
|
+
"@types/react": "19.3.0",
|
|
32
|
+
"@types/react-dom": "19.3.0",
|
|
33
33
|
"jotai": "2.20.3",
|
|
34
|
-
"react": "19.
|
|
35
|
-
"react-dom": "19.
|
|
34
|
+
"react": "19.3.0",
|
|
35
|
+
"react-dom": "19.3.0",
|
|
36
36
|
"storybook": "10.6.0",
|
|
37
|
-
"tone": "15.1.22"
|
|
38
|
-
"@storybook/addon-docs": "10.6.0"
|
|
37
|
+
"tone": "15.1.22"
|
|
39
38
|
},
|
|
40
39
|
"peerDependencies": {
|
|
41
40
|
"react": "^18 ||^19",
|
|
42
41
|
"react-dom": "^18 || ^19"
|
|
43
42
|
},
|
|
44
43
|
"dependencies": {
|
|
45
|
-
"@tremolo-ui/dom": "^0.
|
|
46
|
-
"@tremolo-ui/functions": "^0.
|
|
47
|
-
"clsx": "^2.1.1"
|
|
44
|
+
"@tremolo-ui/dom": "^0.6.0",
|
|
45
|
+
"@tremolo-ui/functions": "^0.6.0"
|
|
48
46
|
},
|
|
49
47
|
"files": [
|
|
50
48
|
"dist",
|
|
51
|
-
"src"
|
|
49
|
+
"src",
|
|
50
|
+
"!src/**/*.test.{ts,tsx}",
|
|
51
|
+
"!src/**/*.stories.{ts,tsx}"
|
|
52
52
|
],
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public"
|
|
@@ -82,13 +82,15 @@
|
|
|
82
82
|
"default": "./dist/index.js"
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
|
-
"./
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
"./compose-refs": {
|
|
86
|
+
"require": {
|
|
87
|
+
"types": "./dist/compose-refs.d.cts",
|
|
88
|
+
"default": "./dist/compose-refs.cjs"
|
|
89
|
+
},
|
|
90
|
+
"import": {
|
|
91
|
+
"types": "./dist/compose-refs.d.ts",
|
|
92
|
+
"default": "./dist/compose-refs.js"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
93
95
|
}
|
|
94
96
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import {
|
|
3
2
|
ComponentPropsWithoutRef,
|
|
4
|
-
|
|
3
|
+
forwardRef,
|
|
5
4
|
useEffect,
|
|
6
5
|
useRef,
|
|
7
6
|
useState,
|
|
@@ -13,6 +12,8 @@ import {
|
|
|
13
12
|
type AnimationFrame,
|
|
14
13
|
} from '@tremolo-ui/dom'
|
|
15
14
|
|
|
15
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
16
|
+
|
|
16
17
|
export type InitFunction = (
|
|
17
18
|
context: CanvasRenderingContext2D,
|
|
18
19
|
option: {
|
|
@@ -28,9 +29,27 @@ export type DrawFunction = (
|
|
|
28
29
|
option: AnimationFrame,
|
|
29
30
|
) => void
|
|
30
31
|
|
|
31
|
-
export interface
|
|
32
|
+
export interface AnimationCanvasCommonProps {
|
|
33
|
+
/**
|
|
34
|
+
* Draw one frame. It is given the 2D context and the frame: the size in CSS
|
|
35
|
+
* pixels, `count`, `deltaTime`, `elapsedTime` and `fps`.
|
|
36
|
+
*
|
|
37
|
+
* A new function replaces the old one in place, so writing it inline does
|
|
38
|
+
* not restart anything.
|
|
39
|
+
*/
|
|
32
40
|
draw: DrawFunction
|
|
41
|
+
/**
|
|
42
|
+
* Called once before the first frame, with the context and the size in CSS
|
|
43
|
+
* pixels. Set up what every frame shares here.
|
|
44
|
+
*/
|
|
33
45
|
init?: InitFunction
|
|
46
|
+
/**
|
|
47
|
+
* Redraw on every animation frame. Turn it off to draw only when there is a
|
|
48
|
+
* reason to: when the canvas mounts, when it is resized, and when the
|
|
49
|
+
* component re-renders.
|
|
50
|
+
*
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
34
53
|
animate?: boolean
|
|
35
54
|
/**
|
|
36
55
|
* Read once, when the 2D context is created, so changing it later has no
|
|
@@ -39,59 +58,88 @@ export interface CommonProps {
|
|
|
39
58
|
* @see https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/getContext#contextattributes
|
|
40
59
|
*/
|
|
41
60
|
options?: CanvasRenderingContext2DSettings
|
|
61
|
+
/**
|
|
62
|
+
* Carry the drawing across a resize, so that the canvas does not blank for a
|
|
63
|
+
* frame while the new size is drawn. A fixed canvas is resized too, when its
|
|
64
|
+
* `width` or `height` changes.
|
|
65
|
+
*
|
|
66
|
+
* @default true
|
|
67
|
+
*/
|
|
68
|
+
reduceFlickering?: boolean
|
|
42
69
|
}
|
|
43
70
|
|
|
44
|
-
|
|
71
|
+
/** A canvas of the size given in CSS pixels. This is the default. */
|
|
72
|
+
export interface AnimationCanvasFixedProps {
|
|
73
|
+
/** Leave it out, or pass `false`, for a canvas of the size below. */
|
|
74
|
+
resizable?: false
|
|
75
|
+
/**
|
|
76
|
+
* Width of the canvas in CSS pixels.
|
|
77
|
+
* @default 100
|
|
78
|
+
*/
|
|
45
79
|
width?: number
|
|
80
|
+
/**
|
|
81
|
+
* Height of the canvas in CSS pixels.
|
|
82
|
+
* @default 100
|
|
83
|
+
*/
|
|
46
84
|
height?: number
|
|
47
85
|
}
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
87
|
+
/**
|
|
88
|
+
* A canvas that follows the size of its parent element, so the parent needs a
|
|
89
|
+
* size of its own.
|
|
90
|
+
*/
|
|
91
|
+
export interface AnimationCanvasResizableProps {
|
|
92
|
+
/**
|
|
93
|
+
* Follow the size of the parent element instead of `width` and `height`.
|
|
94
|
+
* Switching it rebuilds the canvas.
|
|
95
|
+
*/
|
|
96
|
+
resizable: true
|
|
97
|
+
/** Not accepted: the size comes from the parent element. */
|
|
98
|
+
width?: never
|
|
99
|
+
/** Not accepted: the size comes from the parent element. */
|
|
100
|
+
height?: never
|
|
52
101
|
}
|
|
53
102
|
|
|
54
|
-
export type AnimationCanvasProps =
|
|
55
|
-
|
|
56
|
-
|
|
103
|
+
export type AnimationCanvasProps = AnimationCanvasCommonProps &
|
|
104
|
+
(AnimationCanvasFixedProps | AnimationCanvasResizableProps)
|
|
105
|
+
|
|
106
|
+
type Props = AnimationCanvasProps &
|
|
107
|
+
Omit<
|
|
108
|
+
ComponentPropsWithoutRef<'canvas'>,
|
|
109
|
+
keyof AnimationCanvasCommonProps | 'resizable' | 'width' | 'height'
|
|
110
|
+
>
|
|
57
111
|
|
|
58
112
|
/**
|
|
59
113
|
* A simple animatable canvas with requestAnimationFrame()
|
|
60
114
|
*/
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
className,
|
|
85
|
-
onContextMenu = (event) => event.preventDefault(),
|
|
86
|
-
...props
|
|
87
|
-
}: AnimationCanvasProps &
|
|
88
|
-
Omit<
|
|
89
|
-
ComponentPropsWithoutRef<'canvas'>,
|
|
90
|
-
keyof AnimationCanvasProps
|
|
91
|
-
>): ReactElement {
|
|
115
|
+
export const AnimationCanvas = /* @__PURE__ */ forwardRef<
|
|
116
|
+
HTMLCanvasElement,
|
|
117
|
+
Props
|
|
118
|
+
>(function AnimationCanvas(
|
|
119
|
+
{
|
|
120
|
+
// common
|
|
121
|
+
draw,
|
|
122
|
+
init,
|
|
123
|
+
animate = true,
|
|
124
|
+
options,
|
|
125
|
+
reduceFlickering = true,
|
|
126
|
+
// fixed
|
|
127
|
+
width = 100,
|
|
128
|
+
height = 100,
|
|
129
|
+
// resizable
|
|
130
|
+
resizable = false,
|
|
131
|
+
// canvas props
|
|
132
|
+
className,
|
|
133
|
+
onContextMenu = (event) => event.preventDefault(),
|
|
134
|
+
...props
|
|
135
|
+
},
|
|
136
|
+
forwardedRef,
|
|
137
|
+
) {
|
|
92
138
|
// See useDrag for why the node is held in state rather than a ref: an inline
|
|
93
139
|
// ref would be re-attached on every render and tear the instance down.
|
|
94
140
|
const [node, setNode] = useState<HTMLCanvasElement | null>(null)
|
|
141
|
+
// The caller's ref gets the same element.
|
|
142
|
+
const composedRef = useComposedRefs<HTMLCanvasElement>(forwardedRef, setNode)
|
|
95
143
|
|
|
96
144
|
// Read when the instance is created. The effect below keeps it current, and
|
|
97
145
|
// runs right after, so a stale handler is replaced within the same commit.
|
|
@@ -105,8 +153,26 @@ export function AnimationCanvas({
|
|
|
105
153
|
options,
|
|
106
154
|
})
|
|
107
155
|
const instanceRef = useRef<AnimationCanvasInstance | null>(null)
|
|
108
|
-
|
|
109
|
-
|
|
156
|
+
|
|
157
|
+
// Runs before the creation effect below. This keeps a rebuilt instance on
|
|
158
|
+
// the current render's size and handlers while updating an existing one in
|
|
159
|
+
// place on ordinary renders.
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
latest.current = {
|
|
162
|
+
draw,
|
|
163
|
+
init,
|
|
164
|
+
animate,
|
|
165
|
+
width,
|
|
166
|
+
height,
|
|
167
|
+
reduceFlickering,
|
|
168
|
+
options,
|
|
169
|
+
}
|
|
170
|
+
instanceRef.current?.update({
|
|
171
|
+
animate,
|
|
172
|
+
size: { width, height },
|
|
173
|
+
reduceFlickering,
|
|
174
|
+
})
|
|
175
|
+
})
|
|
110
176
|
|
|
111
177
|
useEffect(() => {
|
|
112
178
|
if (!node) return
|
|
@@ -118,54 +184,27 @@ export function AnimationCanvas({
|
|
|
118
184
|
animate: current.animate,
|
|
119
185
|
size: { width: current.width, height: current.height },
|
|
120
186
|
reduceFlickering: current.reduceFlickering,
|
|
121
|
-
|
|
187
|
+
resizable,
|
|
122
188
|
contextAttributes: current.options,
|
|
123
189
|
})
|
|
124
190
|
instanceRef.current = instance
|
|
125
|
-
justCreated.current = true
|
|
126
191
|
|
|
127
192
|
return () => {
|
|
128
193
|
instanceRef.current = null
|
|
129
194
|
instance.destroy()
|
|
130
195
|
}
|
|
131
|
-
// Only `
|
|
196
|
+
// Only `resizable` decides how the instance is wired, so it is the one
|
|
132
197
|
// setting that rebuilds it. `options` is read from the ref above rather
|
|
133
198
|
// than depended on: it is almost always written inline, and depending on
|
|
134
199
|
// it would tear the canvas down on every render.
|
|
135
|
-
}, [node,
|
|
136
|
-
|
|
137
|
-
// Runs after every render: the handlers come from props and are cheap to
|
|
138
|
-
// push, and updating in place keeps the frame count and elapsed time going.
|
|
139
|
-
useEffect(() => {
|
|
140
|
-
latest.current = {
|
|
141
|
-
draw,
|
|
142
|
-
init,
|
|
143
|
-
animate,
|
|
144
|
-
width,
|
|
145
|
-
height,
|
|
146
|
-
reduceFlickering,
|
|
147
|
-
options,
|
|
148
|
-
}
|
|
149
|
-
// The instance was built from `latest` a moment ago, so there is nothing
|
|
150
|
-
// to push yet. Skipping matters with `animate` off, where `update()` draws
|
|
151
|
-
// a frame and would otherwise paint the same one twice on mount.
|
|
152
|
-
if (justCreated.current) {
|
|
153
|
-
justCreated.current = false
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
instanceRef.current?.update({
|
|
157
|
-
animate,
|
|
158
|
-
size: { width, height },
|
|
159
|
-
reduceFlickering,
|
|
160
|
-
})
|
|
161
|
-
})
|
|
200
|
+
}, [node, resizable])
|
|
162
201
|
|
|
163
202
|
return (
|
|
164
203
|
<canvas
|
|
165
|
-
className={
|
|
166
|
-
ref={
|
|
204
|
+
className={className}
|
|
205
|
+
ref={composedRef}
|
|
167
206
|
onContextMenu={onContextMenu}
|
|
168
207
|
{...props}
|
|
169
208
|
/>
|
|
170
209
|
)
|
|
171
|
-
}
|
|
210
|
+
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import { SVGProps } from 'react'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { useCheckPlacement } from '../_util/Placement'
|
|
4
|
+
|
|
5
|
+
import { arcPath, arcRadius, useKnobContext } from './context'
|
|
5
6
|
|
|
6
7
|
export function ActiveLine({
|
|
7
8
|
stroke = 'currentColor',
|
|
@@ -9,17 +10,17 @@ export function ActiveLine({
|
|
|
9
10
|
className,
|
|
10
11
|
...props
|
|
11
12
|
}: Omit<SVGProps<SVGPathElement>, 'd'>) {
|
|
13
|
+
useCheckPlacement('Knob.ActiveLine', 'Knob.SVGRoot')
|
|
14
|
+
|
|
12
15
|
const r2 = useKnobContext((s) => s.r2)
|
|
13
16
|
const r3 = useKnobContext((s) => s.r3)
|
|
14
17
|
|
|
15
18
|
const radius = arcRadius(strokeWidth)
|
|
16
|
-
const start = pointOnArc(r2, radius)
|
|
17
|
-
const end = pointOnArc(r3, radius)
|
|
18
19
|
|
|
19
20
|
return (
|
|
20
21
|
<path
|
|
21
|
-
className={
|
|
22
|
-
d={
|
|
22
|
+
className={className}
|
|
23
|
+
d={arcPath(r2, r3, radius)}
|
|
23
24
|
fill="none"
|
|
24
25
|
stroke={stroke}
|
|
25
26
|
strokeWidth={strokeWidth}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import { SVGProps } from 'react'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { useCheckPlacement } from '../_util/Placement'
|
|
4
|
+
|
|
5
|
+
import { arcPath, arcRadius, useKnobContext } from './context'
|
|
5
6
|
|
|
6
7
|
export function InactiveLine({
|
|
7
8
|
stroke = 'currentColor',
|
|
@@ -9,6 +10,8 @@ export function InactiveLine({
|
|
|
9
10
|
className,
|
|
10
11
|
...props
|
|
11
12
|
}: Omit<SVGProps<SVGPathElement>, 'd'>) {
|
|
13
|
+
useCheckPlacement('Knob.InactiveLine', 'Knob.SVGRoot')
|
|
14
|
+
|
|
12
15
|
const min = useKnobContext((s) => s.min)
|
|
13
16
|
const max = useKnobContext((s) => s.max)
|
|
14
17
|
const startValue = useKnobContext((s) => s.startValue)
|
|
@@ -19,17 +22,13 @@ export function InactiveLine({
|
|
|
19
22
|
const r4 = useKnobContext((s) => s.r4)
|
|
20
23
|
|
|
21
24
|
const radius = arcRadius(strokeWidth)
|
|
22
|
-
const p1 = pointOnArc(r1, radius)
|
|
23
|
-
const p2 = pointOnArc(r2, radius)
|
|
24
|
-
const p3 = pointOnArc(r3, radius)
|
|
25
|
-
const p4 = pointOnArc(r4, radius)
|
|
26
25
|
|
|
27
26
|
return (
|
|
28
27
|
<>
|
|
29
28
|
{startValue > min && (
|
|
30
29
|
<path
|
|
31
|
-
className={
|
|
32
|
-
d={
|
|
30
|
+
className={className}
|
|
31
|
+
d={arcPath(r1, r2, radius)}
|
|
33
32
|
fill="none"
|
|
34
33
|
stroke={stroke}
|
|
35
34
|
strokeWidth={strokeWidth}
|
|
@@ -38,8 +37,8 @@ export function InactiveLine({
|
|
|
38
37
|
)}
|
|
39
38
|
{startValue < max && (
|
|
40
39
|
<path
|
|
41
|
-
className={
|
|
42
|
-
d={
|
|
40
|
+
className={className}
|
|
41
|
+
d={arcPath(r3, r4, radius)}
|
|
43
42
|
fill="none"
|
|
44
43
|
stroke={stroke}
|
|
45
44
|
strokeWidth={strokeWidth}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ReactNode, SVGProps } from 'react'
|
|
2
2
|
|
|
3
|
+
import { Placement } from '../_util/Placement'
|
|
4
|
+
|
|
3
5
|
import { viewBoxSize } from './context'
|
|
4
6
|
|
|
5
|
-
export interface
|
|
7
|
+
export interface KnobSVGRootProps {
|
|
6
8
|
/**
|
|
7
9
|
* `<Knob.InactiveLine />`, `<Knob.ActiveLine />` and `<Knob.Thumb />` go
|
|
8
10
|
* here, in the order you want them painted.
|
|
@@ -14,7 +16,7 @@ export function SVGRoot({
|
|
|
14
16
|
children,
|
|
15
17
|
style,
|
|
16
18
|
...props
|
|
17
|
-
}:
|
|
19
|
+
}: KnobSVGRootProps & Omit<SVGProps<SVGSVGElement>, keyof KnobSVGRootProps>) {
|
|
18
20
|
return (
|
|
19
21
|
<svg
|
|
20
22
|
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
|
@@ -24,7 +26,7 @@ export function SVGRoot({
|
|
|
24
26
|
}}
|
|
25
27
|
{...props}
|
|
26
28
|
>
|
|
27
|
-
{children}
|
|
29
|
+
<Placement name="Knob.SVGRoot">{children}</Placement>
|
|
28
30
|
</svg>
|
|
29
31
|
)
|
|
30
32
|
}
|
|
@@ -1,24 +1,44 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import { SVGProps } from 'react'
|
|
3
2
|
|
|
4
3
|
import { clamp } from '@tremolo-ui/functions'
|
|
5
4
|
|
|
5
|
+
import { useCheckPlacement } from '../_util/Placement'
|
|
6
|
+
|
|
6
7
|
import { useKnobContext, viewBoxSize } from './context'
|
|
7
8
|
|
|
8
|
-
interface
|
|
9
|
-
/**
|
|
9
|
+
export interface KnobThumbProps {
|
|
10
|
+
/**
|
|
11
|
+
* Fill colour of the circle.
|
|
12
|
+
* @default 'currentColor'
|
|
13
|
+
*/
|
|
10
14
|
thumb?: string
|
|
11
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Colour of the line that points at the value.
|
|
17
|
+
* @default 'currentColor'
|
|
18
|
+
*/
|
|
12
19
|
thumbLine?: string
|
|
13
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Diameter of the circle, as a percentage of the knob.
|
|
22
|
+
* @default 84
|
|
23
|
+
*/
|
|
14
24
|
thumbSize?: number
|
|
15
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Thickness of the line, as a percentage of the knob.
|
|
27
|
+
* @default 6
|
|
28
|
+
*/
|
|
16
29
|
thumbLineWeight?: number
|
|
17
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* How far down the line reaches, as a percentage of the knob from its top.
|
|
32
|
+
* The line starts at the edge of the circle.
|
|
33
|
+
* @default 35
|
|
34
|
+
*/
|
|
18
35
|
thumbLineLength?: number
|
|
19
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Classes for what the thumb draws inside itself: `thumbLine` is the line
|
|
39
|
+
* that points at the value.
|
|
40
|
+
*/
|
|
20
41
|
classes?: {
|
|
21
|
-
thumb?: string
|
|
22
42
|
thumbLine?: string
|
|
23
43
|
}
|
|
24
44
|
}
|
|
@@ -34,17 +54,19 @@ export function Thumb({
|
|
|
34
54
|
classes,
|
|
35
55
|
|
|
36
56
|
...props
|
|
37
|
-
}:
|
|
57
|
+
}: KnobThumbProps & Omit<SVGProps<SVGSVGElement>, 'd' | keyof KnobThumbProps>) {
|
|
58
|
+
useCheckPlacement('Knob.Thumb', 'Knob.SVGRoot')
|
|
59
|
+
|
|
38
60
|
const angleRange = useKnobContext((s) => s.angleRange)
|
|
39
61
|
|
|
40
62
|
const p = useKnobContext((s) => s.p)
|
|
41
63
|
const r1 = useKnobContext((s) => s.r1)
|
|
42
64
|
|
|
43
65
|
return (
|
|
44
|
-
<svg className={
|
|
66
|
+
<svg className={className} {...props}>
|
|
45
67
|
<circle cx="50%" cy="50%" r={`${thumbSize / 2}%`} fill={thumb} />
|
|
46
68
|
<line
|
|
47
|
-
className={
|
|
69
|
+
className={classes?.thumbLine}
|
|
48
70
|
x1="50%"
|
|
49
71
|
y1={`${(viewBoxSize - clamp(thumbSize, 0, 100)) / 2}%`}
|
|
50
72
|
x2="50%"
|
|
@@ -51,6 +51,22 @@ export function arcRadius(strokeWidth: number | string | undefined) {
|
|
|
51
51
|
return Number.isFinite(width) ? center - width / 2 : center
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Build an SVG path for an arc, splitting full turns into drawable segments. */
|
|
55
|
+
export function arcPath(startAngle: number, endAngle: number, radius: number) {
|
|
56
|
+
const start = pointOnArc(startAngle, radius)
|
|
57
|
+
const sweep = endAngle - startAngle
|
|
58
|
+
const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180))
|
|
59
|
+
const segmentSweep = sweep / segmentCount
|
|
60
|
+
let path = `M ${start.x} ${start.y}`
|
|
61
|
+
|
|
62
|
+
for (let i = 1; i <= segmentCount; i += 1) {
|
|
63
|
+
const end = pointOnArc(startAngle + segmentSweep * i, radius)
|
|
64
|
+
path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return path
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
/** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
|
|
55
71
|
export function calcAngles({
|
|
56
72
|
value,
|
|
@@ -71,7 +87,7 @@ export function calcAngles({
|
|
|
71
87
|
return { p, r1, r2, r3, r4 }
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
const KnobContext = createContext<KnobContextValue | null>(null)
|
|
90
|
+
const KnobContext = /* @__PURE__ */ createContext<KnobContextValue | null>(null)
|
|
75
91
|
|
|
76
92
|
export const KnobProvider = KnobContext.Provider
|
|
77
93
|
|