@tremolo-ui/react 0.1.6 → 0.2.1
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 +1329 -1152
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +238 -183
- package/dist/index.d.cts +367 -366
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +367 -366
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1313 -1125
- package/dist/index.js.map +1 -1
- package/package.json +17 -19
- package/src/components/AnimationCanvas/index.tsx +0 -7
- package/src/components/DragObserver/index.tsx +9 -7
- package/src/components/Knob/ActiveLine.tsx +29 -0
- package/src/components/Knob/InactiveLine.tsx +53 -0
- package/src/components/Knob/SVGRoot.tsx +39 -0
- package/src/components/Knob/Thumb.tsx +64 -0
- package/src/components/Knob/context.tsx +120 -0
- package/src/components/Knob/index.css +25 -27
- package/src/components/Knob/index.tsx +110 -164
- package/src/components/NumberInput/DecrementStepper.tsx +0 -2
- package/src/components/NumberInput/IncrementStepper.tsx +0 -2
- package/src/components/NumberInput/InternalInput.tsx +3 -1
- package/src/components/NumberInput/Stepper.tsx +0 -2
- package/src/components/NumberInput/context.tsx +0 -1
- package/src/components/NumberInput/index.css +31 -15
- package/src/components/NumberInput/index.tsx +21 -14
- package/src/components/Piano/KeyLabel.tsx +0 -2
- package/src/components/Piano/context.tsx +0 -2
- package/src/components/Piano/index.css +7 -14
- package/src/components/Piano/index.tsx +14 -15
- package/src/components/Piano/key.tsx +0 -10
- package/src/components/Piano/keyboardShortcuts.ts +0 -2
- package/src/components/PointsEditor/Background.tsx +17 -0
- package/src/components/PointsEditor/Container.tsx +26 -0
- package/src/components/PointsEditor/Point.tsx +137 -0
- package/src/components/PointsEditor/context.tsx +91 -0
- package/src/components/PointsEditor/index.css +30 -0
- package/src/components/PointsEditor/index.tsx +129 -0
- package/src/components/Slider/Scale.tsx +0 -2
- package/src/components/Slider/ScaleOption.tsx +0 -2
- package/src/components/Slider/Thumb.tsx +1 -5
- package/src/components/Slider/Track.tsx +1 -3
- package/src/components/Slider/context.tsx +0 -1
- package/src/components/Slider/index.css +33 -8
- package/src/components/Slider/index.tsx +61 -33
- package/src/components/WheelObserver/index.tsx +17 -19
- package/src/components/XYPad/Area.tsx +4 -5
- package/src/components/XYPad/Thumb.tsx +8 -7
- package/src/components/XYPad/index.css +18 -4
- package/src/components/XYPad/index.tsx +71 -38
- package/src/components/_util/composeRefs.tsx +63 -0
- package/src/components/_util/index.ts +23 -6
- package/src/components/_util/type.ts +1 -0
- package/src/hooks/useAnimationFrame.ts +0 -3
- package/src/hooks/useCallbackRef.ts +2 -2
- package/src/hooks/useDrag.ts +12 -11
- package/src/hooks/useDragWithElement.ts +12 -17
- package/src/hooks/useEventListener.ts +6 -9
- package/src/hooks/useInterval.ts +0 -3
- package/src/hooks/useLongPress.ts +0 -3
- package/src/hooks/useMIDIAccess.ts +0 -1
- package/src/hooks/useMIDIInput.ts +0 -1
- package/src/hooks/useMIDIMessage.ts +0 -1
- package/src/hooks/usePianoDrag.ts +75 -0
- package/src/index.ts +21 -30
- package/src/styles/global.css +16 -4
- package/dist/index.css.map +0 -1
- package/src/components/AnimationKnob/index.tsx +0 -314
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
.tremolo-piano-white-key {
|
|
10
10
|
--bg: oklch(100% 0 0);
|
|
11
11
|
--color: oklch(0% 0 0);
|
|
12
|
-
--active-bg: oklch(
|
|
12
|
+
--active-bg: oklch(92% 0 0);
|
|
13
13
|
--active-color: oklch(0% 0 0);
|
|
14
14
|
|
|
15
15
|
box-sizing: border-box;
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
|
|
35
35
|
.tremolo-piano-black-key {
|
|
36
36
|
--color: oklch(100% 0 0);
|
|
37
|
-
--bg: oklch(32
|
|
37
|
+
--bg: oklch(32% 0 0);
|
|
38
38
|
--active-color: oklch(100% 0 0);
|
|
39
|
-
--active-bg: oklch(
|
|
39
|
+
--active-bg: oklch(44% 0 0);
|
|
40
40
|
|
|
41
41
|
box-sizing: border-box;
|
|
42
42
|
position: absolute;
|
|
@@ -82,16 +82,9 @@
|
|
|
82
82
|
|
|
83
83
|
:where(.dark, [data-theme='dark']) {
|
|
84
84
|
.tremolo-piano-white-key {
|
|
85
|
-
--bg: oklch(
|
|
86
|
-
--color: oklch(
|
|
87
|
-
--active-bg: oklch(
|
|
88
|
-
--active-color: oklch(
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.tremolo-piano-black-key {
|
|
92
|
-
--color: oklch(100% 0 0);
|
|
93
|
-
--bg: oklch(32.109% 0 0);
|
|
94
|
-
--active-color: oklch(100% 0 0);
|
|
95
|
-
--active-bg: oklch(38.666% 0.00004 271.152);
|
|
85
|
+
--bg: oklch(80% 0 0);
|
|
86
|
+
--color: oklch(0% 0 0);
|
|
87
|
+
--active-bg: oklch(72% 0 0);
|
|
88
|
+
--active-color: oklch(0% 0 0);
|
|
96
89
|
}
|
|
97
90
|
}
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
noteKeys,
|
|
24
24
|
} from '@tremolo-ui/functions'
|
|
25
25
|
|
|
26
|
-
import { useDragWithElement } from '../../hooks/useDragWithElement'
|
|
27
26
|
import { useEventListener } from '../../hooks/useEventListener'
|
|
27
|
+
import { usePianoDrag } from '../../hooks/usePianoDrag'
|
|
28
28
|
|
|
29
29
|
import { NoteRange, PianoProvider } from './context'
|
|
30
30
|
import {
|
|
@@ -35,10 +35,10 @@ import {
|
|
|
35
35
|
WhiteKey,
|
|
36
36
|
} from './key'
|
|
37
37
|
import { KeyboardShortcuts } from './keyboardShortcuts'
|
|
38
|
+
import { KeyLabel } from './KeyLabel'
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* [noteRange.first, noteRange.first + 1 ..., noteRange.last]
|
|
41
|
-
* @category Piano
|
|
42
42
|
*/
|
|
43
43
|
export function getNoteRangeArray(noteRange: NoteRange) {
|
|
44
44
|
return Array.from(
|
|
@@ -47,7 +47,6 @@ export function getNoteRangeArray(noteRange: NoteRange) {
|
|
|
47
47
|
)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/** @category Piano */
|
|
51
50
|
export interface PianoProps {
|
|
52
51
|
noteRange: NoteRange
|
|
53
52
|
|
|
@@ -73,9 +72,6 @@ export interface PianoProps {
|
|
|
73
72
|
|
|
74
73
|
const blackPerWhiteWidth = defaultBlackKeyWidth / defaultWhiteKeyWidth // 0.65
|
|
75
74
|
|
|
76
|
-
/**
|
|
77
|
-
* @category Piano
|
|
78
|
-
*/
|
|
79
75
|
export interface PianoMethods {
|
|
80
76
|
playNote: (note: number, velocity?: number) => void
|
|
81
77
|
stopNote: (note: number) => void
|
|
@@ -84,11 +80,7 @@ export interface PianoMethods {
|
|
|
84
80
|
type Props = PianoProps &
|
|
85
81
|
Omit<ComponentPropsWithoutRef<'div'>, keyof PianoProps>
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
* Customizable piano component.
|
|
89
|
-
* @category Piano
|
|
90
|
-
*/
|
|
91
|
-
export const Piano = forwardRef<PianoMethods, Props>(
|
|
83
|
+
const Root = forwardRef<PianoMethods, Props>(
|
|
92
84
|
(
|
|
93
85
|
{
|
|
94
86
|
noteRange,
|
|
@@ -243,10 +235,12 @@ export const Piano = forwardRef<PianoMethods, Props>(
|
|
|
243
235
|
}
|
|
244
236
|
}, [fill, _whiteNoteWidth, whiteNoteCount])
|
|
245
237
|
|
|
246
|
-
|
|
247
|
-
|
|
238
|
+
// FIXME
|
|
239
|
+
const { refHandler: touchMoveRefCallback, pointerDownHandler } =
|
|
240
|
+
usePianoDrag<HTMLDivElement>({
|
|
248
241
|
baseElementRef: pianoRef,
|
|
249
242
|
onDrag: onDrag,
|
|
243
|
+
// onDragStart: onDrag,
|
|
250
244
|
onDragEnd: onDragEnd,
|
|
251
245
|
})
|
|
252
246
|
|
|
@@ -345,6 +339,11 @@ export const Piano = forwardRef<PianoMethods, Props>(
|
|
|
345
339
|
},
|
|
346
340
|
)
|
|
347
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Customizable piano component.
|
|
344
|
+
*/
|
|
345
|
+
export const Piano = { Root, WhiteKey, BlackKey, KeyLabel }
|
|
346
|
+
|
|
348
347
|
export { type KeyboardShortcuts, SHORTCUTS } from './keyboardShortcuts'
|
|
349
|
-
export {
|
|
350
|
-
export {
|
|
348
|
+
export { type KeyProps, type KeyMethods } from './key'
|
|
349
|
+
export { type KeyLabelProps } from './KeyLabel'
|
|
@@ -11,9 +11,6 @@ import React, {
|
|
|
11
11
|
import { usePianoContext } from './context'
|
|
12
12
|
import { KeyLabel, KeyLabelProps } from './KeyLabel'
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* @category Piano
|
|
16
|
-
*/
|
|
17
14
|
export interface KeyProps {
|
|
18
15
|
noteNumber: number
|
|
19
16
|
|
|
@@ -36,9 +33,6 @@ export interface KeyProps {
|
|
|
36
33
|
__width?: number
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
/**
|
|
40
|
-
* @category Piano
|
|
41
|
-
*/
|
|
42
36
|
export interface KeyMethods {
|
|
43
37
|
play: (velocity?: number) => void
|
|
44
38
|
stop: () => void
|
|
@@ -48,9 +42,7 @@ export interface KeyMethods {
|
|
|
48
42
|
type Props = KeyProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KeyProps>
|
|
49
43
|
type ImplProps = Props & { keyType: 'black' | 'white' }
|
|
50
44
|
|
|
51
|
-
/** @category Piano */
|
|
52
45
|
export const defaultWhiteKeyWidth = 40
|
|
53
|
-
/** @category Piano */
|
|
54
46
|
export const defaultBlackKeyWidth = 26
|
|
55
47
|
|
|
56
48
|
const KeyImpl = forwardRef<KeyMethods, ImplProps>(
|
|
@@ -166,7 +158,6 @@ const KeyImpl = forwardRef<KeyMethods, ImplProps>(
|
|
|
166
158
|
},
|
|
167
159
|
)
|
|
168
160
|
|
|
169
|
-
/** @category Piano */
|
|
170
161
|
export const WhiteKey = forwardRef<KeyMethods, Props>(
|
|
171
162
|
(
|
|
172
163
|
{
|
|
@@ -190,7 +181,6 @@ export const WhiteKey = forwardRef<KeyMethods, Props>(
|
|
|
190
181
|
},
|
|
191
182
|
)
|
|
192
183
|
|
|
193
|
-
/** @category Piano */
|
|
194
184
|
export const BlackKey = forwardRef<KeyMethods, Props>(
|
|
195
185
|
(
|
|
196
186
|
{
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { ComponentPropsWithoutRef } from 'react'
|
|
3
|
+
|
|
4
|
+
export function Background({
|
|
5
|
+
className,
|
|
6
|
+
children,
|
|
7
|
+
...props
|
|
8
|
+
}: ComponentPropsWithoutRef<'div'>) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={clsx('tremolo-points-editor-background', className)}
|
|
12
|
+
{...props}
|
|
13
|
+
>
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { ComponentPropsWithoutRef, useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { usePointsEditorContext } from './context'
|
|
5
|
+
|
|
6
|
+
export function Container({
|
|
7
|
+
className,
|
|
8
|
+
children,
|
|
9
|
+
...props
|
|
10
|
+
}: ComponentPropsWithoutRef<'div'>) {
|
|
11
|
+
const containerElementRef = useRef<HTMLDivElement>(null)
|
|
12
|
+
const setContainerElementRef = usePointsEditorContext(
|
|
13
|
+
(s) => s.setContainerElementRef,
|
|
14
|
+
)
|
|
15
|
+
setContainerElementRef(containerElementRef)
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
ref={containerElementRef}
|
|
20
|
+
className={clsx('tremolo-points-editor-container', className)}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { ComponentPropsWithoutRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { clamp } from '@tremolo-ui/functions'
|
|
5
|
+
|
|
6
|
+
import { useDragWithElement } from '../../hooks/useDragWithElement'
|
|
7
|
+
import {
|
|
8
|
+
addUserSelectNone,
|
|
9
|
+
removeUserSelectNone,
|
|
10
|
+
resetCursorStyle,
|
|
11
|
+
setCursorStyle,
|
|
12
|
+
} from '../_util'
|
|
13
|
+
|
|
14
|
+
import { usePointsEditorContext } from './context'
|
|
15
|
+
|
|
16
|
+
export type PointBaseType = { x: number; y: number }
|
|
17
|
+
|
|
18
|
+
export function clampPoint(
|
|
19
|
+
point: PointBaseType,
|
|
20
|
+
min?: Partial<PointBaseType>,
|
|
21
|
+
max?: Partial<PointBaseType>,
|
|
22
|
+
) {
|
|
23
|
+
const { x, y } = point
|
|
24
|
+
const newX = clamp(x, min?.x ?? 0, max?.x ?? 1)
|
|
25
|
+
const newY = clamp(y, min?.y ?? 0, max?.y ?? 1)
|
|
26
|
+
return { x: newX, y: newY }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PointProps<T extends PointBaseType> {
|
|
30
|
+
value: T
|
|
31
|
+
min?: Partial<PointBaseType>
|
|
32
|
+
max?: Partial<PointBaseType>
|
|
33
|
+
|
|
34
|
+
size?: number | string
|
|
35
|
+
width?: number | string
|
|
36
|
+
height?: number | string
|
|
37
|
+
color?: string
|
|
38
|
+
|
|
39
|
+
disabled?: boolean
|
|
40
|
+
readonly?: boolean
|
|
41
|
+
|
|
42
|
+
onChange?: (value: PointBaseType) => void
|
|
43
|
+
onDragStart?: (value: PointBaseType) => void
|
|
44
|
+
onDragEnd?: (value: PointBaseType) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Point<T extends PointBaseType>({
|
|
48
|
+
value,
|
|
49
|
+
min,
|
|
50
|
+
max,
|
|
51
|
+
size,
|
|
52
|
+
width = 16,
|
|
53
|
+
height = 16,
|
|
54
|
+
color,
|
|
55
|
+
|
|
56
|
+
disabled,
|
|
57
|
+
readonly,
|
|
58
|
+
|
|
59
|
+
onChange,
|
|
60
|
+
onDragStart,
|
|
61
|
+
onDragEnd,
|
|
62
|
+
|
|
63
|
+
className,
|
|
64
|
+
style,
|
|
65
|
+
onPointerDown,
|
|
66
|
+
...props
|
|
67
|
+
}: PointProps<T> & Omit<ComponentPropsWithoutRef<'div'>, keyof PointProps<T>>) {
|
|
68
|
+
const containerElementRef = usePointsEditorContext(
|
|
69
|
+
(s) => s.containerElementRef,
|
|
70
|
+
)
|
|
71
|
+
const externalStyles = usePointsEditorContext((s) => s.externalStyles)
|
|
72
|
+
const __disabled = usePointsEditorContext((s) => s.disabled)
|
|
73
|
+
const __readonly = usePointsEditorContext((s) => s.readonly)
|
|
74
|
+
|
|
75
|
+
// console.log(value.x, value.y)
|
|
76
|
+
|
|
77
|
+
// Piano: 値が更新されることで、イベントハンドラがリセットされる
|
|
78
|
+
|
|
79
|
+
const {
|
|
80
|
+
refHandler: touchMoveRefCallback,
|
|
81
|
+
pointerDownHandler,
|
|
82
|
+
dragging,
|
|
83
|
+
} = useDragWithElement<HTMLDivElement>({
|
|
84
|
+
baseElementRef: containerElementRef,
|
|
85
|
+
onDrag: (x, y) => {
|
|
86
|
+
if (readonly) return
|
|
87
|
+
|
|
88
|
+
onChange?.(clampPoint({ x, y }, min, max))
|
|
89
|
+
},
|
|
90
|
+
onDragStart: (x, y) => {
|
|
91
|
+
if (readonly) return
|
|
92
|
+
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
93
|
+
if (externalStyles.cursor) setCursorStyle(externalStyles.cursor)
|
|
94
|
+
|
|
95
|
+
onDragStart?.(clampPoint({ x, y }, min, max))
|
|
96
|
+
},
|
|
97
|
+
onDragEnd: (x, y) => {
|
|
98
|
+
if (readonly) return
|
|
99
|
+
if (externalStyles.userSelectNone) removeUserSelectNone()
|
|
100
|
+
if (externalStyles.cursor) resetCursorStyle()
|
|
101
|
+
|
|
102
|
+
onDragEnd?.(clampPoint({ x, y }, min, max))
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const colors: Record<string, string | undefined> = {
|
|
107
|
+
'--color': color,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
ref={(div) => {
|
|
113
|
+
// wheelRefCallback(div)
|
|
114
|
+
touchMoveRefCallback(div)
|
|
115
|
+
}}
|
|
116
|
+
className={clsx('tremolo-points-editor-point', className)}
|
|
117
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
118
|
+
tabIndex={0}
|
|
119
|
+
aria-disabled={disabled != undefined ? disabled : __disabled}
|
|
120
|
+
aria-readonly={readonly != undefined ? readonly : __readonly}
|
|
121
|
+
data-dragging={dragging}
|
|
122
|
+
style={{
|
|
123
|
+
...colors,
|
|
124
|
+
width: size ?? width,
|
|
125
|
+
height: size ?? height,
|
|
126
|
+
left: `${value.x * 100}%`,
|
|
127
|
+
top: `${value.y * 100}%`,
|
|
128
|
+
...style,
|
|
129
|
+
}}
|
|
130
|
+
onPointerDown={(event) => {
|
|
131
|
+
pointerDownHandler(event)
|
|
132
|
+
onPointerDown?.(event)
|
|
133
|
+
}}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createRef,
|
|
4
|
+
RefObject,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import { createStore, useStore } from 'zustand'
|
|
10
|
+
|
|
11
|
+
import { Cursor } from '../_util'
|
|
12
|
+
|
|
13
|
+
type State = {
|
|
14
|
+
containerElementRef: RefObject<HTMLDivElement | null>
|
|
15
|
+
disabled: boolean
|
|
16
|
+
readonly: boolean
|
|
17
|
+
externalStyles: {
|
|
18
|
+
userSelectNone?: boolean
|
|
19
|
+
cursor?: Cursor
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Action = {
|
|
24
|
+
setContainerElementRef: (
|
|
25
|
+
containerElementRef: RefObject<HTMLDivElement | null>,
|
|
26
|
+
) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type PointsEditorStore = ReturnType<typeof createPointsEditorStore>
|
|
30
|
+
|
|
31
|
+
const createPointsEditorStore = (initProps: Partial<State & Action>) => {
|
|
32
|
+
// const noteRange = initProps.noteRange || { first: 0, last: 127 }
|
|
33
|
+
const DEFAULT_PROPS: State = {
|
|
34
|
+
containerElementRef: createRef(),
|
|
35
|
+
disabled: false,
|
|
36
|
+
readonly: false,
|
|
37
|
+
externalStyles: {
|
|
38
|
+
userSelectNone: true,
|
|
39
|
+
cursor: 'grabbing',
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return createStore<State & Action>()((set) => ({
|
|
44
|
+
...DEFAULT_PROPS,
|
|
45
|
+
...initProps,
|
|
46
|
+
setContainerElementRef: (containerElementRef) => {
|
|
47
|
+
set(() => ({
|
|
48
|
+
containerElementRef,
|
|
49
|
+
}))
|
|
50
|
+
},
|
|
51
|
+
}))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const PointsEditorContext = createContext<PointsEditorStore | null>(null)
|
|
55
|
+
|
|
56
|
+
type PointsEditorProviderProps = React.PropsWithChildren<
|
|
57
|
+
Partial<State & Action>
|
|
58
|
+
>
|
|
59
|
+
|
|
60
|
+
export function PointsEditorProvider({
|
|
61
|
+
children,
|
|
62
|
+
...props
|
|
63
|
+
}: PointsEditorProviderProps) {
|
|
64
|
+
const storeRef = useRef<PointsEditorStore>(null)
|
|
65
|
+
if (!storeRef.current) {
|
|
66
|
+
storeRef.current = createPointsEditorStore(props)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (storeRef.current) {
|
|
71
|
+
storeRef.current.setState(props)
|
|
72
|
+
} else {
|
|
73
|
+
storeRef.current = createPointsEditorStore(props)
|
|
74
|
+
}
|
|
75
|
+
}, [props])
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<PointsEditorContext.Provider value={storeRef.current}>
|
|
79
|
+
{children}
|
|
80
|
+
</PointsEditorContext.Provider>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function usePointsEditorContext<T>(
|
|
85
|
+
selector: (state: State & Action) => T,
|
|
86
|
+
): T {
|
|
87
|
+
const store = useContext(PointsEditorContext)
|
|
88
|
+
if (!store)
|
|
89
|
+
throw new Error('Missing PointsEditorContext.Provider in the tree')
|
|
90
|
+
return useStore(store, selector)
|
|
91
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.tremolo-points-editor {
|
|
2
|
+
position: relative;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.tremolo-points-editor-background {
|
|
6
|
+
position: absolute;
|
|
7
|
+
inset: 0;
|
|
8
|
+
z-index: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.tremolo-points-editor-container {
|
|
12
|
+
position: absolute;
|
|
13
|
+
inset: 0;
|
|
14
|
+
z-index: 10;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.tremolo-points-editor-point {
|
|
18
|
+
--color: oklch(59.5% 0.17548 266.373);
|
|
19
|
+
|
|
20
|
+
position: absolute;
|
|
21
|
+
border-radius: 50%;
|
|
22
|
+
outline: none;
|
|
23
|
+
cursor: grab;
|
|
24
|
+
background-color: var(--color);
|
|
25
|
+
translate: -50% -50%;
|
|
26
|
+
|
|
27
|
+
&[data-dragging='true'] {
|
|
28
|
+
cursor: inherit;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { ComponentPropsWithoutRef, forwardRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { InputEventOption } from '@tremolo-ui/functions'
|
|
5
|
+
|
|
6
|
+
import { Cursor } from '../_util'
|
|
7
|
+
|
|
8
|
+
import { Background } from './Background'
|
|
9
|
+
import { Container } from './Container'
|
|
10
|
+
import { PointsEditorProvider } from './context'
|
|
11
|
+
import { Point, PointBaseType } from './Point'
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
TODO:
|
|
15
|
+
|
|
16
|
+
- 複数選択
|
|
17
|
+
- modifier
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
import { useState } from 'react'
|
|
22
|
+
|
|
23
|
+
export default function App() {
|
|
24
|
+
const [points, setPoints] = useState(initialPoints)
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<PointsEditor.Root
|
|
28
|
+
onDoubleClick={({ x, y }, _e) => {
|
|
29
|
+
const newPoints = [...points, { x, y }]
|
|
30
|
+
setPoints(newPoints)
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
{points.map((point) => (
|
|
34
|
+
<PointsEditor.Point key={`${point.x}, ${point.y}`} value={point} />
|
|
35
|
+
))}
|
|
36
|
+
</PointsEditor.Root>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const defaultExternalStyles: PointsEditorProps['externalStyles'] = {
|
|
42
|
+
userSelectNone: true,
|
|
43
|
+
cursor: 'grabbing',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PointsEditorProps {
|
|
47
|
+
width?: number | string
|
|
48
|
+
height?: number | string
|
|
49
|
+
|
|
50
|
+
// TODO
|
|
51
|
+
grid?: number | PointBaseType
|
|
52
|
+
// modifier?:
|
|
53
|
+
|
|
54
|
+
disabled?: boolean
|
|
55
|
+
readonly?: boolean
|
|
56
|
+
|
|
57
|
+
externalStyles?: {
|
|
58
|
+
userSelectNone?: boolean
|
|
59
|
+
cursor?: Cursor
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// TODO
|
|
63
|
+
/**
|
|
64
|
+
* wheel control option
|
|
65
|
+
* If null, no event will be triggered
|
|
66
|
+
*/
|
|
67
|
+
wheel?: InputEventOption | null
|
|
68
|
+
/**
|
|
69
|
+
* keyboard control option
|
|
70
|
+
* If null, no event will be triggered
|
|
71
|
+
*/
|
|
72
|
+
keyboard?: InputEventOption | null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type Props = PointsEditorProps &
|
|
76
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorProps>
|
|
77
|
+
|
|
78
|
+
const Root = forwardRef<HTMLDivElement, Props>(
|
|
79
|
+
(
|
|
80
|
+
{
|
|
81
|
+
width = 200,
|
|
82
|
+
height = 100,
|
|
83
|
+
disabled = false,
|
|
84
|
+
readonly = false,
|
|
85
|
+
externalStyles: _externalStyles,
|
|
86
|
+
style,
|
|
87
|
+
className,
|
|
88
|
+
children,
|
|
89
|
+
...props
|
|
90
|
+
},
|
|
91
|
+
forwardedRef,
|
|
92
|
+
) => {
|
|
93
|
+
const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<PointsEditorProvider
|
|
97
|
+
disabled={disabled}
|
|
98
|
+
readonly={readonly}
|
|
99
|
+
externalStyles={externalStyles}
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
ref={forwardedRef}
|
|
103
|
+
className={clsx('tremolo-points-editor', className)}
|
|
104
|
+
style={{
|
|
105
|
+
width,
|
|
106
|
+
height,
|
|
107
|
+
...style,
|
|
108
|
+
}}
|
|
109
|
+
{...props}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</div>
|
|
113
|
+
</PointsEditorProvider>
|
|
114
|
+
)
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Multiple Point Controller
|
|
120
|
+
*/
|
|
121
|
+
export const PointsEditor = {
|
|
122
|
+
Root,
|
|
123
|
+
Background,
|
|
124
|
+
Container,
|
|
125
|
+
Point,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { usePointsEditorContext } from './context'
|
|
129
|
+
export { clampPoint, type PointBaseType, type PointProps } from './Point'
|
|
@@ -7,14 +7,12 @@ import { useSliderContext } from './context'
|
|
|
7
7
|
import { ScaleOption } from './ScaleOption'
|
|
8
8
|
import { generateOptionsList, ScaleOptions } from './type'
|
|
9
9
|
|
|
10
|
-
/** @category Slider */
|
|
11
10
|
export interface ScaleProps {
|
|
12
11
|
gap?: number | string
|
|
13
12
|
options?: ScaleOptions
|
|
14
13
|
children?: ReactNode
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
/** @category Slider */
|
|
18
16
|
export function Scale({
|
|
19
17
|
gap = 6,
|
|
20
18
|
options,
|
|
@@ -6,7 +6,6 @@ import { toFixed, xor, normalizeValue } from '@tremolo-ui/functions'
|
|
|
6
6
|
import { useSliderContext } from './context'
|
|
7
7
|
import { ScaleType } from './type'
|
|
8
8
|
|
|
9
|
-
/** @category Slider */
|
|
10
9
|
export interface ScaleOptionProps {
|
|
11
10
|
// required
|
|
12
11
|
value: number
|
|
@@ -32,7 +31,6 @@ export interface ScaleOptionProps {
|
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
/** @category Slider */
|
|
36
34
|
export function ScaleOption({
|
|
37
35
|
value,
|
|
38
36
|
type = 'mark-number',
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
|
|
10
10
|
import { useSliderContext } from './context'
|
|
11
11
|
|
|
12
|
-
/** @category Slider */
|
|
13
12
|
export interface SliderThumbProps {
|
|
14
13
|
size?: number | string
|
|
15
14
|
width?: number | string
|
|
@@ -25,17 +24,14 @@ export interface SliderThumbProps {
|
|
|
25
24
|
__percent?: number
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
/** @category Slider */
|
|
29
27
|
export interface SliderThumbMethods {
|
|
30
28
|
focus: () => void
|
|
31
29
|
blur: () => void
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
/** @category Slider */
|
|
35
32
|
export const defaultThumbSize = 22
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
export const SliderThumb = forwardRef<SliderThumbMethods, SliderThumbProps>(
|
|
34
|
+
export const Thumb = forwardRef<SliderThumbMethods, SliderThumbProps>(
|
|
39
35
|
(
|
|
40
36
|
{
|
|
41
37
|
size,
|
|
@@ -8,7 +8,6 @@ import { useSliderContext } from './context'
|
|
|
8
8
|
export const defaultLength = 140
|
|
9
9
|
export const defaultThickness = 10
|
|
10
10
|
|
|
11
|
-
/** @category Slider */
|
|
12
11
|
export interface SliderTrackProps {
|
|
13
12
|
length?: number | string
|
|
14
13
|
thickness?: number | string
|
|
@@ -28,8 +27,7 @@ export interface SliderTrackProps {
|
|
|
28
27
|
__percent?: number
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
export function SliderTrack({
|
|
30
|
+
export function Track({
|
|
33
31
|
length = defaultLength,
|
|
34
32
|
thickness = defaultThickness,
|
|
35
33
|
active,
|
|
@@ -57,7 +57,6 @@ export function SliderProvider({ children, ...props }: SliderProviderProps) {
|
|
|
57
57
|
)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** @category Slider */
|
|
61
60
|
export function useSliderContext<T>(selector: (state: State) => T): T {
|
|
62
61
|
const store = useContext(SliderContext)
|
|
63
62
|
if (!store) throw new Error('Missing SliderContext.Provider in the tree')
|