@uinstinct/svelte-wheel-picker 0.1.21 → 0.1.23
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/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# svelte-wheel-picker 🖱️
|
|
2
2
|
|
|
3
|
+
[](https://github.com/uinstinct/svelte-wheel-picker/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/uinstinct/svelte-wheel-picker/actions/workflows/deploy.yml)
|
|
5
|
+
[](https://github.com/uinstinct/svelte-wheel-picker/actions/workflows/release.yml)
|
|
3
6
|
[](https://www.npmjs.com/package/@uinstinct/svelte-wheel-picker)
|
|
4
7
|
[](https://github.com/uinstinct/svelte-wheel-picker/blob/main/LICENSE)
|
|
5
8
|
|
package/dist/WheelPicker.svelte
CHANGED
|
@@ -39,11 +39,14 @@
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
// Controlled/uncontrolled state management
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// untrack: constructor only needs initial values; effects below handle reactive updates
|
|
43
|
+
const state = untrack(() =>
|
|
44
|
+
useControllableState({
|
|
45
|
+
value,
|
|
46
|
+
defaultValue,
|
|
47
|
+
onChange: onValueChange,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
47
50
|
|
|
48
51
|
// Derive the currently selected index
|
|
49
52
|
const selectedIndex = $derived(options.findIndex((o) => o.value === state.current));
|
|
@@ -55,40 +58,51 @@
|
|
|
55
58
|
return first >= 0 ? first : 0;
|
|
56
59
|
});
|
|
57
60
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
onSnap: (index: number) => {
|
|
68
|
-
console.log('[onSnap] index=', index, 'infinite=', infinite, 'offset=', physics.offset);
|
|
69
|
-
if (infinite) {
|
|
70
|
-
// D-04: Normalize offset on snap settle
|
|
71
|
-
const wrappedIndex = wrapIndex(index, options.length);
|
|
72
|
-
physics.jumpTo(wrappedIndex);
|
|
73
|
-
const opt = options[wrappedIndex];
|
|
74
|
-
console.log(
|
|
75
|
-
'[onSnap] wrappedIndex=',
|
|
76
|
-
wrappedIndex,
|
|
77
|
-
'opt=',
|
|
78
|
-
opt?.value,
|
|
79
|
-
'jumpTo offset=',
|
|
80
|
-
physics.offset,
|
|
81
|
-
);
|
|
82
|
-
if (opt && !opt.disabled) {
|
|
83
|
-
state.current = opt.value;
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
const opt = options[index];
|
|
87
|
-
if (opt && !opt.disabled) {
|
|
88
|
-
state.current = opt.value;
|
|
89
|
-
}
|
|
61
|
+
// Named snap handler — extracted to allow $effect to reference it reactively
|
|
62
|
+
function handleSnap(index: number) {
|
|
63
|
+
if (infinite) {
|
|
64
|
+
// D-04: Normalize offset on snap settle
|
|
65
|
+
const wrappedIndex = wrapIndex(index, options.length);
|
|
66
|
+
physics.jumpTo(wrappedIndex);
|
|
67
|
+
const opt = options[wrappedIndex];
|
|
68
|
+
if (opt && !opt.disabled) {
|
|
69
|
+
state.current = opt.value;
|
|
90
70
|
}
|
|
91
|
-
}
|
|
71
|
+
} else {
|
|
72
|
+
const opt = options[index];
|
|
73
|
+
if (opt && !opt.disabled) {
|
|
74
|
+
state.current = opt.value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Instantiate the physics engine
|
|
80
|
+
// untrack: constructor only needs initial values; $effect below syncs all prop changes
|
|
81
|
+
const physics = untrack(
|
|
82
|
+
() =>
|
|
83
|
+
new WheelPhysics({
|
|
84
|
+
itemHeight: optionItemHeight,
|
|
85
|
+
visibleCount: visibleCount,
|
|
86
|
+
dragSensitivity,
|
|
87
|
+
scrollSensitivity,
|
|
88
|
+
options,
|
|
89
|
+
initialIndex,
|
|
90
|
+
infinite,
|
|
91
|
+
onSnap: handleSnap,
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Sync prop changes to physics engine — fixes state_referenced_locally warnings
|
|
96
|
+
$effect(() => {
|
|
97
|
+
physics.update({
|
|
98
|
+
itemHeight: optionItemHeight,
|
|
99
|
+
visibleCount,
|
|
100
|
+
dragSensitivity,
|
|
101
|
+
scrollSensitivity,
|
|
102
|
+
infinite,
|
|
103
|
+
options,
|
|
104
|
+
onSnap: handleSnap,
|
|
105
|
+
});
|
|
92
106
|
});
|
|
93
107
|
|
|
94
108
|
// Typeahead search instance
|
|
@@ -196,20 +210,8 @@
|
|
|
196
210
|
|
|
197
211
|
// Pointer event handlers (Pattern 2: Pointer Capture for reliable drag tracking)
|
|
198
212
|
function onPointerDown(e: PointerEvent) {
|
|
199
|
-
console.log(
|
|
200
|
-
'[onPointerDown] type=',
|
|
201
|
-
e.type,
|
|
202
|
-
'currentTarget=',
|
|
203
|
-
e.currentTarget,
|
|
204
|
-
'target=',
|
|
205
|
-
e.target,
|
|
206
|
-
);
|
|
207
213
|
const el = e.currentTarget as HTMLElement;
|
|
208
214
|
el.setPointerCapture(e.pointerId);
|
|
209
|
-
console.log(
|
|
210
|
-
'[onPointerDown] setPointerCapture called, hasCapture=',
|
|
211
|
-
el.hasPointerCapture(e.pointerId),
|
|
212
|
-
);
|
|
213
215
|
physics.startDrag(e.clientY);
|
|
214
216
|
}
|
|
215
217
|
|
|
@@ -218,16 +220,6 @@
|
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
function onPointerUp(e: PointerEvent) {
|
|
221
|
-
console.log(
|
|
222
|
-
'[onPointerUp] type=',
|
|
223
|
-
e.type,
|
|
224
|
-
'currentTarget=',
|
|
225
|
-
e.currentTarget,
|
|
226
|
-
'target=',
|
|
227
|
-
e.target,
|
|
228
|
-
'clientY=',
|
|
229
|
-
e.clientY,
|
|
230
|
-
);
|
|
231
223
|
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
|
|
232
224
|
physics.endDrag();
|
|
233
225
|
}
|
|
@@ -52,19 +52,28 @@ export declare class WheelPhysics {
|
|
|
52
52
|
*/
|
|
53
53
|
endDrag(): void;
|
|
54
54
|
/**
|
|
55
|
-
* Called on wheel event.
|
|
55
|
+
* Called on wheel event. Moves proportionally to deltaY magnitude.
|
|
56
56
|
*
|
|
57
|
-
* deltaY > 0: scroll down → move to next item (higher index)
|
|
58
|
-
* deltaY < 0: scroll up → move to previous item (lower index)
|
|
57
|
+
* deltaY > 0: scroll down → move to next item(s) (higher index)
|
|
58
|
+
* deltaY < 0: scroll up → move to previous item(s) (lower index)
|
|
59
|
+
*
|
|
60
|
+
* A typical mouse wheel notch sends deltaY ~100-150px. Dividing by itemHeight
|
|
61
|
+
* gives a proportional item count, so scrolling feels natural and fast.
|
|
62
|
+
* The in-flight animation is cancelled before computing the new target so
|
|
63
|
+
* rapid wheel events feel snappy (each event immediately updates the target).
|
|
59
64
|
*/
|
|
60
65
|
handleWheel(deltaY: number): void;
|
|
61
66
|
/**
|
|
62
67
|
* Animates the offset to the position corresponding to targetIndex.
|
|
63
68
|
* Uses easeOutCubic easing. Calls onSnap when complete.
|
|
64
69
|
*
|
|
70
|
+
* When `velocity` is provided (inertia flick), duration is computed from
|
|
71
|
+
* velocity for natural deceleration proportional to flick speed. Without
|
|
72
|
+
* velocity (keyboard, wheel, slow release), duration is distance-based.
|
|
73
|
+
*
|
|
65
74
|
* Cancels any currently running animation before starting.
|
|
66
75
|
*/
|
|
67
|
-
animateTo(targetIndex: number): void;
|
|
76
|
+
animateTo(targetIndex: number, velocity?: number): void;
|
|
68
77
|
/**
|
|
69
78
|
* Returns the option index that the current offset visually corresponds to.
|
|
70
79
|
* Used to guard against redundant animations when the wheel is already positioned correctly.
|
|
@@ -41,8 +41,6 @@ export class WheelPhysics {
|
|
|
41
41
|
#dragStartY = 0;
|
|
42
42
|
/** Recent pointer positions for velocity calculation: [clientY, timestamp][] */
|
|
43
43
|
#yList = [];
|
|
44
|
-
/** Timestamp of the last wheel event (100ms debounce guard) */
|
|
45
|
-
#lastWheelTime = -Infinity;
|
|
46
44
|
/** True while a snap or inertia animation is running */
|
|
47
45
|
#animating = false;
|
|
48
46
|
// ---------------------------------------------------------------------------
|
|
@@ -148,7 +146,6 @@ export class WheelPhysics {
|
|
|
148
146
|
* Called on pointerup. Computes velocity and kicks off inertia or direct snap.
|
|
149
147
|
*/
|
|
150
148
|
endDrag() {
|
|
151
|
-
console.log('[endDrag] called, isDragging=', this.#isDragging, 'offset=', this.offset);
|
|
152
149
|
if (!this.#isDragging)
|
|
153
150
|
return;
|
|
154
151
|
this.#isDragging = false;
|
|
@@ -158,11 +155,9 @@ export class WheelPhysics {
|
|
|
158
155
|
const currentIndex = this.#infinite
|
|
159
156
|
? wrapIndex(rawIndex, N)
|
|
160
157
|
: clampIndex(rawIndex, N);
|
|
161
|
-
console.log('[endDrag] velocity=', velocity, 'rawIndex=', rawIndex, 'currentIndex=', currentIndex, 'infinite=', this.#infinite);
|
|
162
158
|
if (Math.abs(velocity) < 0.5) {
|
|
163
159
|
// Slow release — snap directly to nearest enabled option
|
|
164
160
|
const snapIndex = snapToNearestEnabled(currentIndex, this.#options);
|
|
165
|
-
console.log('[endDrag] slow path, snapIndex=', snapIndex);
|
|
166
161
|
if (this.#infinite) {
|
|
167
162
|
// Preserve ghost-section context so the snap animation continues in the
|
|
168
163
|
// same direction as the drag rather than jumping backward to real-section.
|
|
@@ -191,13 +186,12 @@ export class WheelPhysics {
|
|
|
191
186
|
// loop, so the animation moves in the same direction as the drag.
|
|
192
187
|
// snapIndex is in [0,N-1]; loopOffset is -N, 0, or +N based on current section.
|
|
193
188
|
const loopOffset = rawIndex < 0 ? -N : rawIndex >= N ? N : 0;
|
|
194
|
-
|
|
195
|
-
this.animateTo(snapIndex + loopOffset);
|
|
189
|
+
this.animateTo(snapIndex + loopOffset, velocity);
|
|
196
190
|
}
|
|
197
191
|
else {
|
|
198
192
|
const clamped = clampIndex(rawTarget, N);
|
|
199
193
|
const snapIndex = snapToNearestEnabled(clamped, this.#options);
|
|
200
|
-
this.animateTo(snapIndex);
|
|
194
|
+
this.animateTo(snapIndex, velocity);
|
|
201
195
|
}
|
|
202
196
|
}
|
|
203
197
|
}
|
|
@@ -205,30 +199,38 @@ export class WheelPhysics {
|
|
|
205
199
|
// Wheel event handler
|
|
206
200
|
// ---------------------------------------------------------------------------
|
|
207
201
|
/**
|
|
208
|
-
* Called on wheel event.
|
|
202
|
+
* Called on wheel event. Moves proportionally to deltaY magnitude.
|
|
203
|
+
*
|
|
204
|
+
* deltaY > 0: scroll down → move to next item(s) (higher index)
|
|
205
|
+
* deltaY < 0: scroll up → move to previous item(s) (lower index)
|
|
209
206
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
207
|
+
* A typical mouse wheel notch sends deltaY ~100-150px. Dividing by itemHeight
|
|
208
|
+
* gives a proportional item count, so scrolling feels natural and fast.
|
|
209
|
+
* The in-flight animation is cancelled before computing the new target so
|
|
210
|
+
* rapid wheel events feel snappy (each event immediately updates the target).
|
|
212
211
|
*/
|
|
213
212
|
handleWheel(deltaY) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// deltaY > 0 = scroll down = move to next item (increment index)
|
|
213
|
+
// Cancel any in-flight animation so rapid scrolling feels immediate
|
|
214
|
+
this.#cancelRaf();
|
|
215
|
+
// Calculate number of items to move based on deltaY magnitude and scroll sensitivity.
|
|
216
|
+
// A typical mouse wheel notch sends deltaY ~100-150px.
|
|
217
|
+
// Multiply by scrollSensitivity / DEFAULT to amplify or dampen the movement.
|
|
218
|
+
// sensitivity 1 → 0.2x (barely moves), sensitivity 5 → 1x (default), sensitivity 20 → 4x (fast)
|
|
219
|
+
const sensitivityMultiplier = this.#scrollSensitivity / DEFAULT_SCROLL_SENSITIVITY;
|
|
220
|
+
const itemsToMove = Math.max(1, Math.round((Math.abs(deltaY) * sensitivityMultiplier) / this.#itemHeight));
|
|
223
221
|
const direction = deltaY > 0 ? 1 : -1;
|
|
222
|
+
const steps = itemsToMove * direction;
|
|
223
|
+
const rawIndex = this.#offsetToIndex(this.offset);
|
|
224
|
+
const N = this.#options.length;
|
|
225
|
+
const currentIndex = this.#infinite ? wrapIndex(rawIndex, N) : clampIndex(rawIndex, N);
|
|
224
226
|
if (this.#infinite) {
|
|
225
|
-
const next = currentIndex +
|
|
226
|
-
const wrapped = wrapIndex(next,
|
|
227
|
+
const next = currentIndex + steps;
|
|
228
|
+
const wrapped = wrapIndex(next, N);
|
|
227
229
|
const snapIndex = snapToNearestEnabled(wrapped, this.#options);
|
|
228
230
|
this.animateTo(snapIndex);
|
|
229
231
|
}
|
|
230
232
|
else {
|
|
231
|
-
const targetIndex = clampIndex(currentIndex +
|
|
233
|
+
const targetIndex = clampIndex(currentIndex + steps, N);
|
|
232
234
|
const snapIndex = snapToNearestEnabled(targetIndex, this.#options);
|
|
233
235
|
this.animateTo(snapIndex);
|
|
234
236
|
}
|
|
@@ -240,16 +242,19 @@ export class WheelPhysics {
|
|
|
240
242
|
* Animates the offset to the position corresponding to targetIndex.
|
|
241
243
|
* Uses easeOutCubic easing. Calls onSnap when complete.
|
|
242
244
|
*
|
|
245
|
+
* When `velocity` is provided (inertia flick), duration is computed from
|
|
246
|
+
* velocity for natural deceleration proportional to flick speed. Without
|
|
247
|
+
* velocity (keyboard, wheel, slow release), duration is distance-based.
|
|
248
|
+
*
|
|
243
249
|
* Cancels any currently running animation before starting.
|
|
244
250
|
*/
|
|
245
|
-
animateTo(targetIndex) {
|
|
246
|
-
console.log('[animateTo] targetIndex=', targetIndex, 'from offset=', this.offset);
|
|
251
|
+
animateTo(targetIndex, velocity) {
|
|
247
252
|
this.#cancelRaf();
|
|
248
253
|
this.#animating = true;
|
|
249
254
|
const startOffset = this.offset;
|
|
250
255
|
const targetOffset = this.#indexToOffset(targetIndex);
|
|
251
256
|
const distance = Math.abs(targetIndex - this.#offsetToIndex(startOffset));
|
|
252
|
-
const durationSec = computeAnimationDuration(distance, this.#scrollSensitivity);
|
|
257
|
+
const durationSec = computeAnimationDuration(distance, this.#scrollSensitivity, velocity);
|
|
253
258
|
const durationMs = durationSec * 1000;
|
|
254
259
|
const startTime = performance.now();
|
|
255
260
|
const tick = (now) => {
|
|
@@ -112,14 +112,21 @@ export declare function computeSnapTarget(currentIndexFromOffset: number, veloci
|
|
|
112
112
|
/**
|
|
113
113
|
* Computes the duration of a snap animation in seconds.
|
|
114
114
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
115
|
+
* When `velocity` is provided and |velocity| >= 0.5 (inertia flick), uses the
|
|
116
|
+
* kinematic formula: `|velocity| / (scrollSensitivity * 6)`, clamped to [0.1, 1.2].
|
|
117
|
+
* This matches the React reference's velocity-based duration model, producing
|
|
118
|
+
* natural deceleration where faster flicks get proportionally longer animations.
|
|
119
|
+
*
|
|
120
|
+
* When `velocity` is undefined or < 0.5 (keyboard, wheel, slow release), falls
|
|
121
|
+
* back to the distance-based formula: `Math.sqrt(|distance| / scrollSensitivity)`,
|
|
122
|
+
* clamped to [0.1, 0.6] — unchanged from the original implementation.
|
|
117
123
|
*
|
|
118
124
|
* @param distance - Distance in index steps to travel
|
|
119
125
|
* @param scrollSensitivity - Scroll sensitivity (affects duration)
|
|
126
|
+
* @param velocity - Optional scroll velocity in items/second (from inertia flick)
|
|
120
127
|
* @returns Animation duration in seconds
|
|
121
128
|
*/
|
|
122
|
-
export declare function computeAnimationDuration(distance: number, scrollSensitivity: number): number;
|
|
129
|
+
export declare function computeAnimationDuration(distance: number, scrollSensitivity: number, velocity?: number): number;
|
|
123
130
|
/**
|
|
124
131
|
* Computes per-item scaleY (and opacity) for cylindrical drum mode.
|
|
125
132
|
*
|
|
@@ -162,14 +162,30 @@ export function computeSnapTarget(currentIndexFromOffset, velocity, dragSensitiv
|
|
|
162
162
|
/**
|
|
163
163
|
* Computes the duration of a snap animation in seconds.
|
|
164
164
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
165
|
+
* When `velocity` is provided and |velocity| >= 0.5 (inertia flick), uses the
|
|
166
|
+
* kinematic formula: `|velocity| / (scrollSensitivity * 6)`, clamped to [0.1, 1.2].
|
|
167
|
+
* This matches the React reference's velocity-based duration model, producing
|
|
168
|
+
* natural deceleration where faster flicks get proportionally longer animations.
|
|
169
|
+
*
|
|
170
|
+
* When `velocity` is undefined or < 0.5 (keyboard, wheel, slow release), falls
|
|
171
|
+
* back to the distance-based formula: `Math.sqrt(|distance| / scrollSensitivity)`,
|
|
172
|
+
* clamped to [0.1, 0.6] — unchanged from the original implementation.
|
|
167
173
|
*
|
|
168
174
|
* @param distance - Distance in index steps to travel
|
|
169
175
|
* @param scrollSensitivity - Scroll sensitivity (affects duration)
|
|
176
|
+
* @param velocity - Optional scroll velocity in items/second (from inertia flick)
|
|
170
177
|
* @returns Animation duration in seconds
|
|
171
178
|
*/
|
|
172
|
-
export function computeAnimationDuration(distance, scrollSensitivity) {
|
|
179
|
+
export function computeAnimationDuration(distance, scrollSensitivity, velocity) {
|
|
180
|
+
if (velocity !== undefined && Math.abs(velocity) >= 0.5) {
|
|
181
|
+
// Velocity-based duration: time proportional to flick speed.
|
|
182
|
+
// deceleration = scrollSensitivity * 6 produces duration=1.0s at v=30
|
|
183
|
+
// (MAX_VELOCITY) with scrollSensitivity=5, a reasonable maximum.
|
|
184
|
+
const deceleration = scrollSensitivity * 6;
|
|
185
|
+
const raw = Math.abs(velocity) / deceleration;
|
|
186
|
+
return Math.max(0.1, Math.min(1.2, raw));
|
|
187
|
+
}
|
|
188
|
+
// Distance-based fallback (keyboard, wheel events, slow releases)
|
|
173
189
|
const raw = Math.sqrt(Math.abs(distance) / scrollSensitivity);
|
|
174
190
|
return Math.max(0.1, Math.min(0.6, raw));
|
|
175
191
|
}
|
package/package.json
CHANGED