@streamscloud/embeddable 6.0.4 → 6.1.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/ui/player/cmp.player-slider.svelte +3 -3
- package/dist/ui/player/player-buffer.svelte.d.ts +1 -1
- package/dist/ui/player/player-buffer.svelte.js +1 -1
- package/dist/ui/player/wheel-gestures-adapter.d.ts +17 -0
- package/dist/ui/player/wheel-gestures-adapter.js +79 -0
- package/package.json +3 -2
- package/dist/ui/player/wheel-peak-detector.d.ts +0 -22
- package/dist/ui/player/wheel-peak-detector.js +0 -157
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
1
|
+
<script lang="ts">import { createWheelAdapter } from './wheel-gestures-adapter';
|
|
2
2
|
import { onDestroy, onMount, untrack } from 'svelte';
|
|
3
3
|
let { buffer, on, children } = $props();
|
|
4
4
|
let slidesRef;
|
|
@@ -115,7 +115,7 @@ const styles = $derived.by(() => {
|
|
|
115
115
|
];
|
|
116
116
|
return values.join(';');
|
|
117
117
|
});
|
|
118
|
-
const
|
|
118
|
+
const wheelCallbacks = {
|
|
119
119
|
canLoadNext: () => buffer.canLoadNext,
|
|
120
120
|
canLoadPrevious: () => buffer.canLoadPrevious,
|
|
121
121
|
onTrigger: (direction) => {
|
|
@@ -132,7 +132,7 @@ const peakDetectorCallbacks = {
|
|
|
132
132
|
</script>
|
|
133
133
|
|
|
134
134
|
<div class="player-slider">
|
|
135
|
-
<div class="player-slider__slides" bind:this={slidesRef} use:
|
|
135
|
+
<div class="player-slider__slides" bind:this={slidesRef} use:createWheelAdapter={{ cbs: wheelCallbacks }} style={styles}>
|
|
136
136
|
{#each buffer.loaded as item, index (item)}
|
|
137
137
|
<div class="player-slider__slide">
|
|
138
138
|
{#if index >= activeIndex - 1 && index <= activeIndex + 1}
|
|
@@ -8,7 +8,7 @@ export declare class PlayerBuffer<T extends {
|
|
|
8
8
|
readonly canLoadNext: boolean;
|
|
9
9
|
readonly canLoadPrevious: boolean;
|
|
10
10
|
readonly navigationDisabled: boolean;
|
|
11
|
-
readonly animationDuration =
|
|
11
|
+
readonly animationDuration = 500;
|
|
12
12
|
private _currentIndex;
|
|
13
13
|
private _loaded;
|
|
14
14
|
private loadMoreFn;
|
|
@@ -6,7 +6,7 @@ export class PlayerBuffer {
|
|
|
6
6
|
canLoadNext = $derived(this.currentIndex < this.loaded.length - 1);
|
|
7
7
|
canLoadPrevious = $derived(this.currentIndex > 0);
|
|
8
8
|
navigationDisabled = $derived(!this.canLoadNext && !this.canLoadPrevious);
|
|
9
|
-
animationDuration =
|
|
9
|
+
animationDuration = 500;
|
|
10
10
|
_currentIndex = $state(-1);
|
|
11
11
|
_loaded = $state.raw([]);
|
|
12
12
|
loadMoreFn;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type WheelAdapterCallbacks = {
|
|
2
|
+
canLoadNext: () => boolean;
|
|
3
|
+
canLoadPrevious: () => boolean;
|
|
4
|
+
onTrigger: (direction: number) => void;
|
|
5
|
+
getAnimationDurationMs: () => number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Minimal, robust wheel adapter:
|
|
9
|
+
* - EMA over axisVelocity[1] to smooth noisy streams (esp. on Windows)
|
|
10
|
+
* - Cooldown blocks triggers while animation runs
|
|
11
|
+
* - Mouse fallback: if velocity is near zero but delta is large, treat as a discrete "kick"
|
|
12
|
+
*/
|
|
13
|
+
export declare const createWheelAdapter: (target: HTMLElement, params: {
|
|
14
|
+
cbs: WheelAdapterCallbacks;
|
|
15
|
+
}) => {
|
|
16
|
+
destroy(): void;
|
|
17
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// wheel-gestures-adapter.ts
|
|
2
|
+
import { WheelGestures } from 'wheel-gestures';
|
|
3
|
+
/**
|
|
4
|
+
* Minimal, robust wheel adapter:
|
|
5
|
+
* - EMA over axisVelocity[1] to smooth noisy streams (esp. on Windows)
|
|
6
|
+
* - Cooldown blocks triggers while animation runs
|
|
7
|
+
* - Mouse fallback: if velocity is near zero but delta is large, treat as a discrete "kick"
|
|
8
|
+
*/
|
|
9
|
+
export const createWheelAdapter = (target, params) => {
|
|
10
|
+
const { cbs } = params;
|
|
11
|
+
// Tunables
|
|
12
|
+
const PEAK_THRESHOLD = 0.4; // EMA magnitude threshold to consider as a "peak"
|
|
13
|
+
const ACCEL_THRESHOLD = 0.02; // minimal directional EMA rise to count as real acceleration
|
|
14
|
+
const EMA_ALPHA = 0.35; // EMA smoothing factor (0..1); higher = snappier, noisier
|
|
15
|
+
const MOUSE_DELTA_KICK = 12; // mouse: large delta step (≈12 mac / ≈100 win)
|
|
16
|
+
const MOUSE_STEP_RATIO_KICK = 350; // dimensionless; tune 200–500 if needed
|
|
17
|
+
const wheelGestures = WheelGestures({ preventWheelAction: true, reverseSign: false });
|
|
18
|
+
wheelGestures.observe(target);
|
|
19
|
+
let isAnimating = false;
|
|
20
|
+
let cooldownTimer = null;
|
|
21
|
+
// EMA state
|
|
22
|
+
let emaVelocity = 0;
|
|
23
|
+
let previousEmaVelocity = 0;
|
|
24
|
+
const startCooldown = () => {
|
|
25
|
+
if (cooldownTimer) {
|
|
26
|
+
clearTimeout(cooldownTimer);
|
|
27
|
+
}
|
|
28
|
+
cooldownTimer = window.setTimeout(() => {
|
|
29
|
+
isAnimating = false;
|
|
30
|
+
}, cbs.getAnimationDurationMs() + 100);
|
|
31
|
+
};
|
|
32
|
+
const fire = (direction) => {
|
|
33
|
+
// Respect external guards (e.g., paging boundaries)
|
|
34
|
+
if ((direction > 0 && !cbs.canLoadNext()) || (direction < 0 && !cbs.canLoadPrevious())) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
isAnimating = true;
|
|
38
|
+
cbs.onTrigger(direction);
|
|
39
|
+
startCooldown();
|
|
40
|
+
};
|
|
41
|
+
wheelGestures.on('wheel', ({ axisDelta: [, axisDeltaY], axisVelocity: [, axisVelocityY] }) => {
|
|
42
|
+
const velocityY = axisVelocityY || 0;
|
|
43
|
+
const deltaY = axisDeltaY || 0;
|
|
44
|
+
// Tracking only: always update EMA and compute signs/acceleration
|
|
45
|
+
previousEmaVelocity = emaVelocity;
|
|
46
|
+
emaVelocity += (velocityY - emaVelocity) * EMA_ALPHA;
|
|
47
|
+
const emaMagnitude = Math.abs(emaVelocity);
|
|
48
|
+
const velocitySign = Math.sign(emaVelocity) || Math.sign(velocityY);
|
|
49
|
+
const emaAcceleration = emaVelocity - previousEmaVelocity;
|
|
50
|
+
// During animation we only track; no arming, no triggering
|
|
51
|
+
if (isAnimating) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Path 1: mouse-like discrete kick (platform-agnostic via delta/velocity ratio)
|
|
55
|
+
const absDelta = Math.abs(deltaY);
|
|
56
|
+
const absVel = Math.abs(velocityY);
|
|
57
|
+
const stepRatio = absDelta / Math.max(1e-6, absVel);
|
|
58
|
+
const isMouseLikeKick = absDelta >= MOUSE_DELTA_KICK && stepRatio >= MOUSE_STEP_RATIO_KICK;
|
|
59
|
+
if (isMouseLikeKick) {
|
|
60
|
+
const direction = deltaY > 0 ? 1 : -1; // 1 = next/down, -1 = previous/up
|
|
61
|
+
fire(direction);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Path 2: trackpad/inertia via EMA acceleration and peak gating
|
|
65
|
+
const isAcceleratingInDirection = velocitySign !== 0 && emaAcceleration * velocitySign > ACCEL_THRESHOLD;
|
|
66
|
+
if (isAcceleratingInDirection && emaMagnitude > PEAK_THRESHOLD) {
|
|
67
|
+
const direction = velocitySign > 0 ? 1 : -1; // 1 = next/down, -1 = previous/up
|
|
68
|
+
fire(direction);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
destroy() {
|
|
73
|
+
wheelGestures.unobserve(target);
|
|
74
|
+
if (cooldownTimer) {
|
|
75
|
+
clearTimeout(cooldownTimer);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamscloud/embeddable",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"author": "StreamsCloud",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -157,6 +157,7 @@
|
|
|
157
157
|
"vite-tsconfig-paths": "^5.1.4"
|
|
158
158
|
},
|
|
159
159
|
"dependencies": {
|
|
160
|
-
"@popperjs/core": "^2.11.8"
|
|
160
|
+
"@popperjs/core": "^2.11.8",
|
|
161
|
+
"wheel-gestures": "^2.2.48"
|
|
161
162
|
}
|
|
162
163
|
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export type PeakDetectorConfig = {
|
|
2
|
-
mouseGapMs?: number;
|
|
3
|
-
mouseDeltaThreshold?: number;
|
|
4
|
-
interEventTimeout?: number;
|
|
5
|
-
minPeakToTrigger?: number;
|
|
6
|
-
waveMaxAgeMs?: number;
|
|
7
|
-
directionFlipEndsWave?: boolean;
|
|
8
|
-
animationCooldownMs?: number;
|
|
9
|
-
directionChangeMinAbsDelta?: number;
|
|
10
|
-
};
|
|
11
|
-
export type PeakDetectorCallbacks = {
|
|
12
|
-
canLoadNext: () => boolean;
|
|
13
|
-
canLoadPrevious: () => boolean;
|
|
14
|
-
onTrigger: (direction: number) => void;
|
|
15
|
-
getAnimationDurationMs: () => number;
|
|
16
|
-
};
|
|
17
|
-
export declare const createWheelPeakDetector: (target: HTMLElement, params: {
|
|
18
|
-
cbs: PeakDetectorCallbacks;
|
|
19
|
-
cfg?: PeakDetectorConfig;
|
|
20
|
-
}) => {
|
|
21
|
-
destroy(): void;
|
|
22
|
-
};
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { isScrollingPrevented } from './prevent-slider-scroll';
|
|
2
|
-
export const createWheelPeakDetector = (target, params) => {
|
|
3
|
-
const { cbs, cfg } = params;
|
|
4
|
-
// Defaults tuned for smooth touchpads; adjust in caller if needed.
|
|
5
|
-
const config = {
|
|
6
|
-
mouseGapMs: cfg?.mouseGapMs ?? 120,
|
|
7
|
-
mouseDeltaThreshold: cfg?.mouseDeltaThreshold ?? 12,
|
|
8
|
-
interEventTimeout: cfg?.interEventTimeout ?? 180,
|
|
9
|
-
minPeakToTrigger: cfg?.minPeakToTrigger ?? 6,
|
|
10
|
-
waveMaxAgeMs: cfg?.waveMaxAgeMs ?? 1800,
|
|
11
|
-
directionFlipEndsWave: cfg?.directionFlipEndsWave ?? true,
|
|
12
|
-
animationCooldownMs: cfg?.animationCooldownMs ?? 100, // extra after CSS transition
|
|
13
|
-
directionChangeMinAbsDelta: cfg?.directionChangeMinAbsDelta ?? 2 // ignore tiny opposite spikes
|
|
14
|
-
};
|
|
15
|
-
let wave = null;
|
|
16
|
-
let lastWheelTime = 0;
|
|
17
|
-
let lastDelta = 0;
|
|
18
|
-
let isAnimating = false;
|
|
19
|
-
let cooldownTimer = null;
|
|
20
|
-
const clearWave = () => {
|
|
21
|
-
wave = null;
|
|
22
|
-
};
|
|
23
|
-
const setAnimatingWithCooldown = () => {
|
|
24
|
-
isAnimating = true;
|
|
25
|
-
const total = cbs.getAnimationDurationMs() + config.animationCooldownMs;
|
|
26
|
-
if (cooldownTimer) {
|
|
27
|
-
clearTimeout(cooldownTimer);
|
|
28
|
-
}
|
|
29
|
-
cooldownTimer = window.setTimeout(() => {
|
|
30
|
-
isAnimating = false;
|
|
31
|
-
cooldownTimer = null;
|
|
32
|
-
}, total);
|
|
33
|
-
};
|
|
34
|
-
const trigger = (direction) => {
|
|
35
|
-
if (direction > 0 && !cbs.canLoadNext()) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
if (direction < 0 && !cbs.canLoadPrevious()) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
setAnimatingWithCooldown();
|
|
42
|
-
cbs.onTrigger(direction);
|
|
43
|
-
};
|
|
44
|
-
const canHandle = (node) => {
|
|
45
|
-
while (node && node !== target) {
|
|
46
|
-
if (isScrollingPrevented(node)) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
node = node.parentElement;
|
|
50
|
-
}
|
|
51
|
-
return true;
|
|
52
|
-
};
|
|
53
|
-
const finalizeWaveWithTrigger = () => {
|
|
54
|
-
if (wave && !isAnimating) {
|
|
55
|
-
if (wave.maxAbsDelta >= config.minPeakToTrigger) {
|
|
56
|
-
trigger(wave.direction);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
clearWave();
|
|
60
|
-
};
|
|
61
|
-
const finalizeWaveSilently = () => {
|
|
62
|
-
// Finish the wave without triggering (used on direction flip to avoid ghost slide)
|
|
63
|
-
clearWave();
|
|
64
|
-
};
|
|
65
|
-
const onWheel = (e) => {
|
|
66
|
-
e.preventDefault();
|
|
67
|
-
if (!canHandle(e.target)) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const now = Date.now();
|
|
71
|
-
const dy = e.deltaY;
|
|
72
|
-
const absDelta = Math.abs(dy);
|
|
73
|
-
console.warn(absDelta);
|
|
74
|
-
const dir = Math.sign(dy);
|
|
75
|
-
// Mouse branch: big, sparse deltas
|
|
76
|
-
const timeSinceLast = now - lastWheelTime;
|
|
77
|
-
if (absDelta >= config.mouseDeltaThreshold && timeSinceLast > config.mouseGapMs && !isAnimating) {
|
|
78
|
-
trigger(dir);
|
|
79
|
-
clearWave();
|
|
80
|
-
lastWheelTime = now;
|
|
81
|
-
lastDelta = dy;
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
// Determine direction change with hysteresis to avoid micro flips
|
|
85
|
-
const directionChanged = !!wave && dir !== 0 && dir !== wave.direction && absDelta >= config.directionChangeMinAbsDelta;
|
|
86
|
-
const inactiveTooLong = wave && now - wave?.lastEventTime > config.interEventTimeout;
|
|
87
|
-
const waveTooOld = wave && now - wave?.startTime > config.waveMaxAgeMs;
|
|
88
|
-
// If direction flip should end the wave, do it silently (no trigger of the old direction)
|
|
89
|
-
if (wave && config.directionFlipEndsWave && directionChanged) {
|
|
90
|
-
finalizeWaveSilently();
|
|
91
|
-
// Start a new wave immediately in the new direction
|
|
92
|
-
wave = {
|
|
93
|
-
startTime: now,
|
|
94
|
-
lastEventTime: now,
|
|
95
|
-
direction: dir,
|
|
96
|
-
maxAbsDelta: absDelta,
|
|
97
|
-
sumAbsDelta: absDelta,
|
|
98
|
-
events: 1
|
|
99
|
-
};
|
|
100
|
-
lastWheelTime = now;
|
|
101
|
-
lastDelta = dy;
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
// Finalize by inactivity or age (these DO trigger)
|
|
105
|
-
if (inactiveTooLong || waveTooOld) {
|
|
106
|
-
finalizeWaveWithTrigger();
|
|
107
|
-
}
|
|
108
|
-
// Start condition for new wave:
|
|
109
|
-
// - no wave
|
|
110
|
-
// - current amplitude grows vs previous
|
|
111
|
-
// - long gap since last wheel
|
|
112
|
-
const growing = absDelta > Math.abs(lastDelta);
|
|
113
|
-
const longGap = timeSinceLast > config.interEventTimeout;
|
|
114
|
-
if (!wave || growing || longGap) {
|
|
115
|
-
// Opportunistic finalize only if not animating and not caused by a flip
|
|
116
|
-
// (flip branch handled earlier and is silent)
|
|
117
|
-
if (wave && !isAnimating) {
|
|
118
|
-
if (wave.maxAbsDelta >= config.minPeakToTrigger) {
|
|
119
|
-
trigger(wave.direction);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
wave = {
|
|
123
|
-
startTime: now,
|
|
124
|
-
lastEventTime: now,
|
|
125
|
-
direction: dir || (wave?.direction ?? 0),
|
|
126
|
-
maxAbsDelta: absDelta,
|
|
127
|
-
sumAbsDelta: absDelta,
|
|
128
|
-
events: 1
|
|
129
|
-
};
|
|
130
|
-
lastWheelTime = now;
|
|
131
|
-
lastDelta = dy;
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
// Continue current wave
|
|
135
|
-
if (wave) {
|
|
136
|
-
wave.lastEventTime = now;
|
|
137
|
-
wave.events += 1;
|
|
138
|
-
wave.sumAbsDelta += absDelta;
|
|
139
|
-
if (absDelta > wave.maxAbsDelta) {
|
|
140
|
-
wave.maxAbsDelta = absDelta; // peak update
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
lastWheelTime = now;
|
|
144
|
-
lastDelta = dy;
|
|
145
|
-
};
|
|
146
|
-
target.addEventListener('wheel', onWheel, { passive: false });
|
|
147
|
-
return {
|
|
148
|
-
destroy() {
|
|
149
|
-
target.removeEventListener('wheel', onWheel);
|
|
150
|
-
if (cooldownTimer) {
|
|
151
|
-
clearTimeout(cooldownTimer);
|
|
152
|
-
cooldownTimer = null;
|
|
153
|
-
}
|
|
154
|
-
wave = null;
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
};
|