|
24 | 24 | /** multiplier for acceleration */ |
25 | 25 | acceleration?: number; |
26 | 26 | maxSpeed?: number; |
27 | | - /** initial delay before acceleration starts (ms) */ |
28 | | - initialDelay?: number; |
29 | 27 |
|
30 | 28 | defaultValue?: number | string; |
31 | 29 | label?: string; |
|
43 | 41 | import { clamp } from './helpers/clamp.js'; |
44 | 42 | import { normalize, format, unnormalizeToString, unnormalizeToNumber } from './params.js'; |
45 | 43 | import type { EnumParam, FloatParam } from './params.js'; |
| 44 | + import './shield.css'; |
46 | 45 |
|
47 | 46 | type KnobBaseProps = { |
48 | 47 | ui: Snippet<[SnippetProps]>; |
| 48 | + // FIX unwanted knob jiggle |
49 | 49 | rotationDegrees: Spring<number>; |
50 | 50 | } & SharedKnobProps; |
51 | 51 |
|
|
57 | 57 | onChange, |
58 | 58 | value = $bindable(), |
59 | 59 | step = 0.01, |
60 | | - acceleration = 1.4, |
| 60 | + acceleration = 1.2, |
61 | 61 | maxSpeed = 0.2, |
62 | | - initialDelay = 100, |
63 | 62 | defaultValue, |
64 | 63 | param, |
65 | 64 | rotationDegrees, |
|
77 | 76 | let startY: number; |
78 | 77 | let startValue: number; |
79 | 78 |
|
80 | | - // This is needed in case some snap value is very close to the min or max range |
81 | | - // preventing the user from selecting that value |
82 | 79 | function completeFixedSnapValues(snapValues: number[]) { |
83 | 80 | if (param.type === 'enum-param') return []; |
84 | 81 | if (snapValues.length < 1) return []; |
|
100 | 97 | function toMobile(handler: ({ clientY }: MouseEvent) => void | boolean) { |
101 | 98 | return (event: TouchEvent) => { |
102 | 99 | const touch = event.touches?.[0]; |
103 | | - if (touch === undefined) return; |
104 | | -
|
| 100 | + if (!touch) return; |
105 | 101 | const clientY = touch.clientY; |
106 | | -
|
107 | 102 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions |
108 | 103 | handler({ clientY } as MouseEvent) && event.preventDefault(); |
109 | 104 | }; |
110 | 105 | } |
111 | 106 |
|
112 | 107 | function handleMouseDown({ clientY }: MouseEvent) { |
113 | | - if (!draggable) return; |
| 108 | + if (!draggable || isDisabled) return; |
114 | 109 | isDragging = true; |
115 | 110 | startY = clientY; |
116 | 111 | startValue = normalizedValue; |
117 | | -
|
118 | 112 | return true; |
119 | 113 | } |
120 | 114 |
|
121 | 115 | function handleMouseMove({ clientY }: MouseEvent) { |
122 | | - if (!draggable) return; |
123 | | - if (isDisabled) return; |
124 | | - if (!isDragging) return; |
| 116 | + if (!draggable || isDisabled || !isDragging) return; |
125 | 117 | const deltaY = startY - clientY; |
126 | 118 | const deltaValue = deltaY / 200; |
127 | 119 | setValue(startValue + deltaValue); |
128 | | -
|
129 | 120 | return true; |
130 | 121 | } |
131 | 122 |
|
|
136 | 127 | function handleDblClick() { |
137 | 128 | const val = |
138 | 129 | defaultValue ?? |
139 | | - (param as FloatParam)?.range.min ?? |
| 130 | + (param as FloatParam)?.range?.min ?? |
140 | 131 | (param as EnumParam<string[]>).variants?.[0]; |
| 132 | +
|
141 | 133 | if (val === undefined) return; |
142 | 134 |
|
143 | 135 | setValue(normalize(val, param)); |
|
148 | 140 |
|
149 | 141 | type Direction = 'left' | 'right'; |
150 | 142 |
|
151 | | - let intervalId = -1; |
152 | 143 | let currentSpeed = step; |
153 | 144 |
|
154 | 145 | const directions: Record<string, Direction> = { |
|
158 | 149 | ArrowUp: 'right' |
159 | 150 | }; |
160 | 151 |
|
161 | | - function adjustValue(direction: Direction) { |
162 | | - const delta = direction === 'right' ? currentSpeed : -currentSpeed; |
163 | | - console.log(direction); |
164 | | - setValue(normalizedValue + delta); |
| 152 | + function handleKeyDown(e: KeyboardEvent) { |
| 153 | + if (isDisabled || !(e.key in directions)) return; |
| 154 | + isDragging = true; |
| 155 | + const direction = directions[e.key]; |
165 | 156 |
|
166 | | - currentSpeed = Math.min(maxSpeed, currentSpeed * acceleration); |
167 | | - } |
| 157 | + if (param.type === 'enum-param') { |
| 158 | + const i = param.variants.findIndex((v) => v === value) ?? 0; |
| 159 | + const step = direction === 'right' ? 1 : -1; |
168 | 160 |
|
169 | | - function handleKeyDown(e: KeyboardEvent) { |
170 | | - if (isDisabled) return; |
171 | | - if (!(e.key in directions)) return; |
172 | | - if (intervalId > -1) return; |
| 161 | + value = param.variants[clamp(i + step, 0, param.variants.length - 1)]; |
| 162 | + onChange?.(value); |
173 | 163 |
|
174 | | - intervalId = window.setInterval(() => adjustValue(directions[e.key]), initialDelay); |
| 164 | + return; |
| 165 | + } |
| 166 | +
|
| 167 | + const delta = direction === 'right' ? currentSpeed : -currentSpeed; |
| 168 | + setValue(normalizedValue + delta); |
| 169 | + currentSpeed = Math.min(maxSpeed, currentSpeed * acceleration); |
175 | 170 | } |
176 | 171 |
|
177 | 172 | function handleKeyUp() { |
178 | | - if (intervalId === -1) return; |
179 | | -
|
180 | | - window.clearInterval(intervalId); |
181 | | - intervalId = -1; |
| 173 | + isDragging = false; |
182 | 174 | currentSpeed = step; |
183 | 175 | } |
184 | 176 |
|
185 | 177 | $effect(() => { |
186 | 178 | rotationDegrees.set(normalizedValue * 270 - 135); |
187 | | -
|
188 | | - // this was easier in svelte 4 :/ |
189 | 179 | window.addEventListener('touchmove', handleTouchMove, { passive: false }); |
| 180 | +
|
190 | 181 | return () => window.removeEventListener('touchmove', handleTouchMove); |
191 | 182 | }); |
192 | 183 |
|
193 | 184 | function setValue(newNormalizedValue: number) { |
194 | 185 | if (param.type === 'enum-param') { |
195 | | - const newValue = unnormalizeToString(newNormalizedValue, param); |
196 | | -
|
| 186 | + const newValue = unnormalizeToString(clamp(newNormalizedValue, 0, 1), param); |
197 | 187 | if (value !== newValue) { |
198 | 188 | value = newValue; |
199 | 189 | onChange?.(value); |
200 | 190 | } |
201 | | -
|
202 | 191 | return; |
203 | 192 | } |
204 | 193 |
|
205 | 194 | let newValue = unnormalizeToNumber(clamp(newNormalizedValue, 0, 1), param); |
206 | | -
|
207 | 195 | if (fixedSnapValues.length > 0) { |
208 | 196 | const nearestSnapValue = fixedSnapValues.reduce((prev, curr) => { |
209 | 197 | const currNormalized = normalize(curr, param); |
|
213 | 201 | ? curr |
214 | 202 | : prev; |
215 | 203 | }); |
216 | | -
|
217 | 204 | const nearestSnapNormalized = normalize(nearestSnapValue, param); |
218 | 205 | if (Math.abs(nearestSnapNormalized - newNormalizedValue) <= snapThreshold) { |
219 | 206 | newValue = nearestSnapValue; |
220 | 207 | } |
221 | 208 | } |
222 | 209 |
|
223 | 210 | if (value !== newValue) { |
| 211 | + if (isNaN(newValue)) { |
| 212 | + newValue = 0; |
| 213 | + console.warn('newValue is NaN'); |
| 214 | + } |
224 | 215 | value = newValue; |
225 | 216 | onChange?.(value); |
226 | 217 | } |
227 | 218 | } |
| 219 | +
|
| 220 | + let shield = document.createElement('div'); |
| 221 | +
|
| 222 | + $effect(() => { |
| 223 | + if (isDragging) { |
| 224 | + shield.className = 'shield tf68Uh'; |
| 225 | + document.body.append(shield); |
| 226 | + document.body.style.userSelect = 'none'; |
| 227 | + } else { |
| 228 | + shield.remove(); |
| 229 | + document.body.style.userSelect = ''; |
| 230 | + } |
| 231 | + }); |
228 | 232 | </script> |
229 | 233 |
|
230 | 234 | <svelte:window |
|
259 | 263 | span { |
260 | 264 | user-select: none; |
261 | 265 | } |
262 | | -
|
263 | 266 | .container { |
264 | 267 | position: relative; |
265 | 268 | display: flex; |
|
0 commit comments