@tremolo-ui/react 0.4.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 +2272 -1650
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1461 -475
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1461 -475
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2258 -1648
- package/dist/index.js.map +1 -1
- package/package.json +32 -31
- package/src/components/AnimationCanvas/index.tsx +162 -194
- package/src/components/Knob/ActiveLine.tsx +9 -8
- package/src/components/Knob/InactiveLine.tsx +11 -14
- package/src/components/Knob/SVGRoot.tsx +10 -17
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +73 -89
- package/src/components/Knob/index.tsx +227 -105
- package/src/components/NumberInput/DecrementStepper.tsx +21 -50
- package/src/components/NumberInput/IncrementStepper.tsx +22 -50
- package/src/components/NumberInput/InputField.tsx +206 -0
- package/src/components/NumberInput/Stepper.tsx +150 -21
- package/src/components/NumberInput/StepperButton.tsx +95 -0
- package/src/components/NumberInput/context.tsx +104 -149
- package/src/components/NumberInput/index.tsx +461 -110
- package/src/components/Piano/index.tsx +413 -263
- package/src/components/Piano/keyboard-shortcuts.ts +61 -0
- package/src/components/PointsEditor/Background.tsx +19 -4
- package/src/components/PointsEditor/Container.tsx +92 -22
- package/src/components/PointsEditor/Point.tsx +293 -53
- package/src/components/PointsEditor/SelectionBox.tsx +54 -0
- package/src/components/PointsEditor/context.tsx +97 -75
- package/src/components/PointsEditor/index.tsx +497 -62
- package/src/components/Slider/Marks.tsx +84 -0
- package/src/components/Slider/MarksOption.tsx +115 -0
- package/src/components/Slider/Thumb.tsx +99 -54
- package/src/components/Slider/Track.tsx +73 -62
- package/src/components/Slider/context.tsx +40 -53
- package/src/components/Slider/index.tsx +237 -190
- package/src/components/Slider/type.ts +27 -9
- package/src/components/XYPad/Area.tsx +52 -33
- package/src/components/XYPad/Thumb.tsx +121 -60
- package/src/components/XYPad/context.tsx +49 -0
- package/src/components/XYPad/index.tsx +338 -246
- 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/{useCallbackRef.ts → _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 +39 -9
- package/src/hooks/useDragValue.ts +194 -0
- package/src/hooks/useEventListener.ts +11 -7
- 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/hooks/useWheel.ts +29 -9
- package/src/index.ts +50 -27
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -536
- package/src/Globals.d.ts +0 -3
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/Knob/index.css +0 -77
- package/src/components/NumberInput/InternalInput.tsx +0 -216
- package/src/components/NumberInput/index.css +0 -185
- package/src/components/NumberInput/type.ts +0 -58
- package/src/components/Piano/KeyLabel.tsx +0 -51
- package/src/components/Piano/context.tsx +0 -73
- package/src/components/Piano/index.css +0 -90
- package/src/components/Piano/key.tsx +0 -205
- package/src/components/Piano/keyboardShortcuts.ts +0 -31
- package/src/components/PointsEditor/index.css +0 -30
- package/src/components/Slider/Scale.tsx +0 -52
- package/src/components/Slider/ScaleOption.tsx +0 -103
- package/src/components/Slider/index.css +0 -112
- package/src/components/XYPad/index.css +0 -48
- package/src/components/_util/index.ts +0 -28
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -100
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- package/src/styles/global.css +0 -19
|
@@ -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%"
|
|
@@ -1,120 +1,104 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createContext,
|
|
3
|
-
PropsWithChildren,
|
|
4
|
-
useContext,
|
|
5
|
-
useEffect,
|
|
6
|
-
useRef,
|
|
7
|
-
} from 'react'
|
|
8
|
-
import { createStore, useStore } from 'zustand'
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
9
2
|
|
|
10
|
-
import {
|
|
3
|
+
import { radian, type Scale } from '@tremolo-ui/functions'
|
|
11
4
|
|
|
12
5
|
export const viewBoxSize = 100
|
|
13
6
|
export const center = viewBoxSize / 2
|
|
14
7
|
|
|
15
|
-
type
|
|
8
|
+
export type KnobConfig = {
|
|
16
9
|
value: number
|
|
17
10
|
min: number
|
|
18
11
|
max: number
|
|
19
12
|
step: number
|
|
20
|
-
|
|
13
|
+
scale: Scale
|
|
21
14
|
startValue: number
|
|
15
|
+
/** angle range [degree] */
|
|
22
16
|
angleRange: number
|
|
23
17
|
}
|
|
24
18
|
|
|
25
|
-
type
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
y3: number
|
|
37
|
-
x4: number
|
|
38
|
-
y4: number
|
|
19
|
+
export type KnobContextValue = KnobConfig & {
|
|
20
|
+
/** normalized value */
|
|
21
|
+
p: number
|
|
22
|
+
/** ロータリー開始位置 */
|
|
23
|
+
r1: number
|
|
24
|
+
/** activeLine の開始位置 */
|
|
25
|
+
r2: number
|
|
26
|
+
/** activeLine の終了位置 */
|
|
27
|
+
r3: number
|
|
28
|
+
/** ロータリー終了位置 */
|
|
29
|
+
r4: number
|
|
39
30
|
}
|
|
40
31
|
|
|
41
|
-
|
|
32
|
+
/**
|
|
33
|
+
* 角度から円弧上の点を求める。
|
|
34
|
+
*
|
|
35
|
+
* 半径は線の太さの分だけ内側に取る必要があるが、太さは ActiveLine / InactiveLine が
|
|
36
|
+
* それぞれ持つため、座標はストアで先に計算せず各コンポーネント側で求める。
|
|
37
|
+
*/
|
|
38
|
+
export function pointOnArc(angle: number, radius: number) {
|
|
39
|
+
return {
|
|
40
|
+
x: center + radius * Math.cos(radian(angle - 90)),
|
|
41
|
+
y: center + radius * Math.sin(radian(angle - 90)),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 線の太さが viewBox からはみ出さないようにした半径 */
|
|
46
|
+
export function arcRadius(strokeWidth: number | string | undefined) {
|
|
47
|
+
const width =
|
|
48
|
+
typeof strokeWidth === 'number'
|
|
49
|
+
? strokeWidth
|
|
50
|
+
: Number.parseFloat(String(strokeWidth))
|
|
51
|
+
return Number.isFinite(width) ? center - width / 2 : center
|
|
52
|
+
}
|
|
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
|
+
|
|
70
|
+
/** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
|
|
71
|
+
export function calcAngles({
|
|
42
72
|
value,
|
|
43
73
|
min,
|
|
44
74
|
max,
|
|
45
|
-
|
|
75
|
+
scale,
|
|
46
76
|
startValue,
|
|
47
77
|
angleRange,
|
|
48
|
-
}:
|
|
49
|
-
const p =
|
|
50
|
-
const s =
|
|
78
|
+
}: KnobConfig) {
|
|
79
|
+
const p = scale.normalize(value, min, max)
|
|
80
|
+
const s = scale.normalize(startValue, min, max)
|
|
51
81
|
|
|
52
82
|
const r1 = -angleRange / 2
|
|
53
83
|
const r2 = r1 + Math.min(p, s) * angleRange
|
|
54
84
|
const r3 = r1 + Math.max(p, s) * angleRange
|
|
55
85
|
const r4 = angleRange / 2
|
|
56
|
-
const x1 = center + center * Math.cos(radian(r1 - 90))
|
|
57
|
-
const y1 = center + center * Math.sin(radian(r1 - 90))
|
|
58
|
-
const x2 = center + center * Math.cos(radian(r2 - 90))
|
|
59
|
-
const y2 = center + center * Math.sin(radian(r2 - 90))
|
|
60
|
-
const x3 = center + center * Math.cos(radian(r3 - 90))
|
|
61
|
-
const y3 = center + center * Math.sin(radian(r3 - 90))
|
|
62
|
-
const x4 = center + center * Math.cos(radian(r4 - 90))
|
|
63
|
-
const y4 = center + center * Math.sin(radian(r4 - 90))
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
p,
|
|
67
|
-
r1,
|
|
68
|
-
r2,
|
|
69
|
-
r3,
|
|
70
|
-
r4,
|
|
71
|
-
x1,
|
|
72
|
-
y1,
|
|
73
|
-
x2,
|
|
74
|
-
y2,
|
|
75
|
-
x3,
|
|
76
|
-
y3,
|
|
77
|
-
x4,
|
|
78
|
-
y4,
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const createKnobStore = (initProps: State) => {
|
|
85
|
-
return createStore<State & DrawingState>()(() => ({
|
|
86
|
-
...initProps,
|
|
87
|
-
...calcDrawingState(initProps),
|
|
88
|
-
}))
|
|
87
|
+
return { p, r1, r2, r3, r4 }
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
const KnobContext = createContext<
|
|
92
|
-
|
|
93
|
-
export function KnobProvider({ children, ...props }: PropsWithChildren<State>) {
|
|
94
|
-
const storeRef = useRef<KnobStore>(null)
|
|
95
|
-
if (!storeRef.current) {
|
|
96
|
-
storeRef.current = createKnobStore(props)
|
|
97
|
-
}
|
|
90
|
+
const KnobContext = /* @__PURE__ */ createContext<KnobContextValue | null>(null)
|
|
98
91
|
|
|
99
|
-
|
|
100
|
-
if (storeRef.current) {
|
|
101
|
-
storeRef.current.setState({ ...props, ...calcDrawingState(props) })
|
|
102
|
-
} else {
|
|
103
|
-
storeRef.current = createKnobStore(props)
|
|
104
|
-
}
|
|
105
|
-
}, [props])
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<KnobContext.Provider value={storeRef.current}>
|
|
109
|
-
{children}
|
|
110
|
-
</KnobContext.Provider>
|
|
111
|
-
)
|
|
112
|
-
}
|
|
92
|
+
export const KnobProvider = KnobContext.Provider
|
|
113
93
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Everything here is derived during render, so there is no state to keep in
|
|
96
|
+
* sync: `value` comes from the props of `Root` and the rest follows from it.
|
|
97
|
+
*/
|
|
98
|
+
export function useKnobContext(): KnobContextValue
|
|
99
|
+
export function useKnobContext<T>(selector: (state: KnobContextValue) => T): T
|
|
100
|
+
export function useKnobContext<T>(selector?: (state: KnobContextValue) => T) {
|
|
101
|
+
const context = useContext(KnobContext)
|
|
102
|
+
if (!context) throw new Error('Missing KnobContext.Provider in the tree')
|
|
103
|
+
return selector ? selector(context) : context
|
|
120
104
|
}
|