@streamscloud/embeddable 6.0.1 → 6.0.3
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/core/browser.js +1 -1
- package/dist/media-center/media-center/cmp.media-center.svelte +27 -29
- package/dist/ui/dropdown/cmp.dropdown.svelte +1 -2
- package/dist/ui/player/cmp.player-slider.svelte +16 -86
- package/dist/ui/player/wheel-peak-detector.d.ts +22 -0
- package/dist/ui/player/wheel-peak-detector.js +157 -0
- package/package.json +1 -1
package/dist/core/browser.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const runningInBrowser = () =>
|
|
1
|
+
export const runningInBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
@@ -17,9 +17,9 @@ import { MediaCenterLocalization } from './media-center-localization';
|
|
|
17
17
|
import { default as Overview } from './overview.svelte';
|
|
18
18
|
import { makeShortVideosProvider } from './short-video-resources-generator';
|
|
19
19
|
import { MediaCenterMode } from './types';
|
|
20
|
-
import IconTextColumnThree from '@fluentui/svg-icons/icons/text_column_three_20_regular.svg?raw';
|
|
21
20
|
import IconLineHorizontal3 from '@fluentui/svg-icons/icons/line_horizontal_3_20_regular.svg?raw';
|
|
22
|
-
import
|
|
21
|
+
import IconTextColumnThree from '@fluentui/svg-icons/icons/text_column_three_20_regular.svg?raw';
|
|
22
|
+
import { onDestroy, onMount } from 'svelte';
|
|
23
23
|
import { fade } from 'svelte/transition';
|
|
24
24
|
let { dataProvider, playerProps, localization: localizationInit = 'en' } = $props();
|
|
25
25
|
const localization = $derived(new MediaCenterLocalization(localizationInit));
|
|
@@ -32,6 +32,7 @@ let headerHeight = $state(0);
|
|
|
32
32
|
let shortVideoProps = $state.raw(playerProps.type === MediaCenterMode.ShortVideos ? playerProps.props : null);
|
|
33
33
|
let streamProps = $state.raw(playerProps.type === MediaCenterMode.Stream ? playerProps.props : null);
|
|
34
34
|
let overviewData = $state.raw(null);
|
|
35
|
+
let scrollResizeObserver = null;
|
|
35
36
|
const categories = $derived.by(() => {
|
|
36
37
|
if (!mediaCenterConfig) {
|
|
37
38
|
return [];
|
|
@@ -60,7 +61,13 @@ onMount(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
60
61
|
mediaDataLoading = false;
|
|
61
62
|
}
|
|
62
63
|
}
|
|
64
|
+
scrollResizeObserver = new ResizeObserver(() => {
|
|
65
|
+
updateScrollShadows();
|
|
66
|
+
});
|
|
63
67
|
}));
|
|
68
|
+
onDestroy(() => {
|
|
69
|
+
scrollResizeObserver === null || scrollResizeObserver === void 0 ? void 0 : scrollResizeObserver.disconnect();
|
|
70
|
+
});
|
|
64
71
|
const selectCategory = (categoryId) => {
|
|
65
72
|
if (!dataProvider) {
|
|
66
73
|
return;
|
|
@@ -145,8 +152,7 @@ let scrollRef = null;
|
|
|
145
152
|
let scrollHasLeft = $state(false);
|
|
146
153
|
let scrollHasRight = $state(false);
|
|
147
154
|
const mounted = (node, callback) => {
|
|
148
|
-
|
|
149
|
-
scrollResizeObserver.observe(node);
|
|
155
|
+
scrollResizeObserver === null || scrollResizeObserver === void 0 ? void 0 : scrollResizeObserver.observe(node);
|
|
150
156
|
const heightResizeObserver = new ResizeObserver(() => {
|
|
151
157
|
headerHeight = node.clientHeight;
|
|
152
158
|
callback({ height: headerHeight });
|
|
@@ -154,7 +160,6 @@ const mounted = (node, callback) => {
|
|
|
154
160
|
heightResizeObserver.observe(node);
|
|
155
161
|
return {
|
|
156
162
|
destroy: () => {
|
|
157
|
-
scrollResizeObserver.disconnect();
|
|
158
163
|
heightResizeObserver.disconnect();
|
|
159
164
|
}
|
|
160
165
|
};
|
|
@@ -169,9 +174,7 @@ const updateScrollShadows = () => {
|
|
|
169
174
|
};
|
|
170
175
|
const onScrollMounted = (node) => {
|
|
171
176
|
scrollRef = node;
|
|
172
|
-
|
|
173
|
-
updateScrollShadows();
|
|
174
|
-
});
|
|
177
|
+
scrollResizeObserver === null || scrollResizeObserver === void 0 ? void 0 : scrollResizeObserver.observe(node);
|
|
175
178
|
};
|
|
176
179
|
</script>
|
|
177
180
|
|
|
@@ -181,9 +184,6 @@ const onScrollMounted = (node) => {
|
|
|
181
184
|
{#snippet categoriesSwitcher(data: { maxItemsWidth: Number; onMounted: (data: { height: Number }) => void })}
|
|
182
185
|
<div class="media-center" use:mounted={data.onMounted}>
|
|
183
186
|
<div class="media-center__row" style={`max-width: ${data.maxItemsWidth}px;`}>
|
|
184
|
-
<button type="button" class="media-center__overview-button" onclick={toggleOverview}>
|
|
185
|
-
<Icon src={IconTextColumnThree} />
|
|
186
|
-
</button>
|
|
187
187
|
<div
|
|
188
188
|
class="media-center__scroll"
|
|
189
189
|
class:media-center__scroll--has-left={scrollHasLeft}
|
|
@@ -191,16 +191,17 @@ const onScrollMounted = (node) => {
|
|
|
191
191
|
class:media-center__scroll--has-both={scrollHasRight && scrollHasLeft}
|
|
192
192
|
use:onScrollMounted
|
|
193
193
|
onscroll={updateScrollShadows}>
|
|
194
|
-
<
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
194
|
+
<button type="button" class="media-center__overview-button" onclick={toggleOverview}>
|
|
195
|
+
<Icon src={IconTextColumnThree} />
|
|
196
|
+
</button>
|
|
197
|
+
{#each categories as category (category.id)}
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
class="media-center__category-button"
|
|
201
|
+
class:media-center__category-button--active={selectedCategoryId === category.id}
|
|
202
|
+
title={category.name}
|
|
203
|
+
onclick={() => selectCategory(category.id)}>{category.name}</button>
|
|
204
|
+
{/each}
|
|
204
205
|
</div>
|
|
205
206
|
</div>
|
|
206
207
|
<div class="media-center__overview-dropdown">
|
|
@@ -276,6 +277,7 @@ const onScrollMounted = (node) => {
|
|
|
276
277
|
width: 100%;
|
|
277
278
|
display: flex;
|
|
278
279
|
align-items: center;
|
|
280
|
+
justify-content: center;
|
|
279
281
|
gap: 0.75rem;
|
|
280
282
|
/* Set 'container-type: inline-size;' to reference container*/
|
|
281
283
|
}
|
|
@@ -288,12 +290,16 @@ const onScrollMounted = (node) => {
|
|
|
288
290
|
pointer-events: auto;
|
|
289
291
|
position: relative;
|
|
290
292
|
flex: 1 1 auto;
|
|
293
|
+
max-width: max-content;
|
|
291
294
|
min-width: 0;
|
|
292
295
|
overflow-x: auto;
|
|
293
296
|
overflow-y: hidden;
|
|
294
297
|
-webkit-overflow-scrolling: touch;
|
|
295
298
|
scrollbar-width: none;
|
|
296
299
|
display: flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
gap: 0.75rem;
|
|
302
|
+
flex-wrap: nowrap;
|
|
297
303
|
mask-image: none;
|
|
298
304
|
}
|
|
299
305
|
.media-center__scroll::-webkit-scrollbar {
|
|
@@ -308,14 +314,6 @@ const onScrollMounted = (node) => {
|
|
|
308
314
|
.media-center__scroll--has-both {
|
|
309
315
|
mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0, rgb(0, 0, 0) 32px, rgb(0, 0, 0) calc(100% - 32px), rgba(0, 0, 0, 0) 100%);
|
|
310
316
|
}
|
|
311
|
-
.media-center__items {
|
|
312
|
-
display: inline-flex;
|
|
313
|
-
align-items: center;
|
|
314
|
-
gap: 0.75rem;
|
|
315
|
-
flex-wrap: nowrap;
|
|
316
|
-
pointer-events: none;
|
|
317
|
-
padding-inline: 0.25rem;
|
|
318
|
-
}
|
|
319
317
|
.media-center__overview-button {
|
|
320
318
|
pointer-events: auto;
|
|
321
319
|
padding: 0.375rem 0.75rem;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">import { runningInBrowser } from '../../core/browser';
|
|
2
|
-
import { isIgnored } from './dropdown-ignore';
|
|
3
2
|
import { Icon } from '../icon';
|
|
3
|
+
import { isIgnored } from './dropdown-ignore';
|
|
4
4
|
import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_20_regular.svg?raw';
|
|
5
5
|
import { createPopper } from '@popperjs/core';
|
|
6
6
|
import { onDestroy } from 'svelte';
|
|
@@ -28,7 +28,6 @@ $effect(() => {
|
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
|
-
const id = Math.random();
|
|
32
31
|
let opened = $state(false);
|
|
33
32
|
$effect(() => {
|
|
34
33
|
var _a, _b;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
1
|
+
<script lang="ts">import { createWheelPeakDetector } from './wheel-peak-detector';
|
|
2
2
|
import { onDestroy, onMount, untrack } from 'svelte';
|
|
3
|
-
const MOUSE_DETECTION_THRESHOLD_MS = 100;
|
|
4
3
|
let { buffer, on, children } = $props();
|
|
5
4
|
let slidesRef;
|
|
6
5
|
let sliderHeight = $state(0);
|
|
@@ -90,89 +89,6 @@ onMount(() => {
|
|
|
90
89
|
}
|
|
91
90
|
reset();
|
|
92
91
|
});
|
|
93
|
-
let waveDetector = {
|
|
94
|
-
events: [],
|
|
95
|
-
direction: 0,
|
|
96
|
-
peakReached: false,
|
|
97
|
-
lastPeak: 0,
|
|
98
|
-
waveStartTime: 0
|
|
99
|
-
};
|
|
100
|
-
let isAnimatingWheel = false;
|
|
101
|
-
const triggerAnimation = (direction) => {
|
|
102
|
-
isAnimatingWheel = true;
|
|
103
|
-
if (direction > 0 && buffer.canLoadNext) {
|
|
104
|
-
buffer.loadNext();
|
|
105
|
-
}
|
|
106
|
-
else if (direction < 0 && buffer.canLoadPrevious) {
|
|
107
|
-
buffer.loadPrevious();
|
|
108
|
-
}
|
|
109
|
-
setTimeout(() => {
|
|
110
|
-
isAnimatingWheel = false;
|
|
111
|
-
}, buffer.animationDuration + 100);
|
|
112
|
-
};
|
|
113
|
-
slidesRef.addEventListener('wheel', (e) => {
|
|
114
|
-
e.preventDefault();
|
|
115
|
-
const checkCanHandleWheel = (node) => {
|
|
116
|
-
while (node && node !== slidesRef) {
|
|
117
|
-
if (isScrollingPrevented(node)) {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
node = node.parentElement;
|
|
121
|
-
}
|
|
122
|
-
return true;
|
|
123
|
-
};
|
|
124
|
-
if (!checkCanHandleWheel(e.target)) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const now = Date.now();
|
|
128
|
-
const absDelta = Math.abs(e.deltaY);
|
|
129
|
-
const direction = Math.sign(e.deltaY);
|
|
130
|
-
// Mouse - large stable values, trigger immediately
|
|
131
|
-
if (absDelta >= 10 && !isAnimatingWheel) {
|
|
132
|
-
const lastEvent = waveDetector.events[waveDetector.events.length - 1];
|
|
133
|
-
const timeSinceLastEvent = lastEvent ? now - lastEvent.time : 1000;
|
|
134
|
-
// If enough time has passed since the last event - it's a mouse
|
|
135
|
-
if (timeSinceLastEvent > MOUSE_DETECTION_THRESHOLD_MS) {
|
|
136
|
-
triggerAnimation(direction);
|
|
137
|
-
waveDetector = { events: [], direction: 0, peakReached: false, lastPeak: 0, waveStartTime: 0 };
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
// Touchpad - small, variable values, need to analyze the wave
|
|
142
|
-
// New wave if: direction changed or a lot of time has passed
|
|
143
|
-
if (direction !== waveDetector.direction || (waveDetector.waveStartTime && now - waveDetector.waveStartTime > 1500)) {
|
|
144
|
-
// Finalize the previous wave if it existed
|
|
145
|
-
if (waveDetector.peakReached && !isAnimatingWheel) {
|
|
146
|
-
triggerAnimation(waveDetector.direction);
|
|
147
|
-
}
|
|
148
|
-
// Start a new wave
|
|
149
|
-
waveDetector = {
|
|
150
|
-
events: [{ delta: absDelta, time: now }],
|
|
151
|
-
direction: direction,
|
|
152
|
-
peakReached: false,
|
|
153
|
-
lastPeak: absDelta,
|
|
154
|
-
waveStartTime: now
|
|
155
|
-
};
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
// Continue the current wave
|
|
159
|
-
waveDetector.events.push({ delta: absDelta, time: now });
|
|
160
|
-
// Determine the phase of the wave
|
|
161
|
-
if (absDelta > waveDetector.lastPeak) {
|
|
162
|
-
// The wave is growing
|
|
163
|
-
waveDetector.lastPeak = absDelta;
|
|
164
|
-
waveDetector.peakReached = absDelta >= 5; // The minimum peak to consider a valid wave
|
|
165
|
-
}
|
|
166
|
-
else if (absDelta < waveDetector.lastPeak * 0.5) {
|
|
167
|
-
// The wave has dropped significantly - consider the gesture complete
|
|
168
|
-
if (waveDetector.peakReached && !isAnimatingWheel) {
|
|
169
|
-
triggerAnimation(waveDetector.direction);
|
|
170
|
-
waveDetector = { events: [], direction: 0, peakReached: false, lastPeak: 0, waveStartTime: 0 };
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
// Cleanup old events
|
|
174
|
-
waveDetector.events = waveDetector.events.filter((evt) => now - evt.time < 2000);
|
|
175
|
-
});
|
|
176
92
|
slidesRef.addEventListener('transitionend', (e) => {
|
|
177
93
|
if (e.target !== slidesRef) {
|
|
178
94
|
return;
|
|
@@ -199,10 +115,24 @@ const styles = $derived.by(() => {
|
|
|
199
115
|
];
|
|
200
116
|
return values.join(';');
|
|
201
117
|
});
|
|
118
|
+
const peakDetectorCallbacks = {
|
|
119
|
+
canLoadNext: () => buffer.canLoadNext,
|
|
120
|
+
canLoadPrevious: () => buffer.canLoadPrevious,
|
|
121
|
+
onTrigger: (direction) => {
|
|
122
|
+
// direction: 1 -> next, -1 -> previous
|
|
123
|
+
if (direction > 0) {
|
|
124
|
+
buffer.loadNext();
|
|
125
|
+
}
|
|
126
|
+
else if (direction < 0) {
|
|
127
|
+
buffer.loadPrevious();
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
getAnimationDurationMs: () => buffer.animationDuration
|
|
131
|
+
};
|
|
202
132
|
</script>
|
|
203
133
|
|
|
204
134
|
<div class="player-slider">
|
|
205
|
-
<div class="player-slider__slides" bind:this={slidesRef} style={styles}>
|
|
135
|
+
<div class="player-slider__slides" bind:this={slidesRef} use:createWheelPeakDetector={{ cbs: peakDetectorCallbacks }} style={styles}>
|
|
206
136
|
{#each buffer.loaded as item, index (item)}
|
|
207
137
|
<div class="player-slider__slide">
|
|
208
138
|
{#if index >= activeIndex - 1 && index <= activeIndex + 1}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
};
|