@scarlett-player/gestures 1.6.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.
@@ -0,0 +1,266 @@
1
+ import { Plugin } from '@scarlett-player/core';
2
+
3
+ /**
4
+ * Types for the Gestures Plugin.
5
+ */
6
+ /** Horizontal region of the gesture surface a pointer landed in. */
7
+ type GestureZone = 'left' | 'middle' | 'right';
8
+ /**
9
+ * A pointer event reduced to what the recognizer needs.
10
+ *
11
+ * Deliberately not a DOM event: the recognizer is a pure state machine, and
12
+ * every timing rule in it is testable without a browser.
13
+ */
14
+ interface PointerRecord {
15
+ /** What happened. */
16
+ type: 'down' | 'move' | 'up' | 'cancel';
17
+ /** Horizontal position in CSS pixels, for slop measurement. */
18
+ x: number;
19
+ /** Vertical position in CSS pixels, for slop measurement. */
20
+ y: number;
21
+ /** Horizontal position as a fraction of the surface width, for zoning. */
22
+ fraction: number;
23
+ /** Pointer identity, so a second finger can be told apart from a retap. */
24
+ pointerId: number;
25
+ /** Event timestamp in milliseconds. */
26
+ timeStamp: number;
27
+ }
28
+ /** What the recognizer concluded from a stream of pointer records. */
29
+ type RecognizerEvent = {
30
+ type: 'tap';
31
+ zone: GestureZone;
32
+ } | {
33
+ type: 'double-tap';
34
+ zone: GestureZone;
35
+ count: number;
36
+ } | {
37
+ type: 'accumulate';
38
+ zone: GestureZone;
39
+ count: number;
40
+ } | {
41
+ type: 'cancel';
42
+ };
43
+ /** Recognizer tuning. */
44
+ interface RecognizerOptions {
45
+ /** Milliseconds within which a second tap counts as a double tap. */
46
+ doubleTapWindowMs: number;
47
+ /** Milliseconds within which a further tap extends an active seek sequence. */
48
+ accumulationWindowMs: number;
49
+ /** Fraction of the width belonging to the left zone. */
50
+ leftZone: number;
51
+ /** Fraction of the width belonging to the right zone. */
52
+ rightZone: number;
53
+ /** Movement in CSS pixels that turns a tap into a drag and cancels it. */
54
+ slopPx: number;
55
+ }
56
+ /** Configuration for the Gestures Plugin. */
57
+ interface GesturesPluginConfig {
58
+ /**
59
+ * Whether gestures are active.
60
+ *
61
+ * `'auto'` enables them when the device reports a coarse pointer, and every
62
+ * individual gesture is additionally checked for `pointerType === 'touch'`,
63
+ * so a touchscreen laptop works with a finger and is untouched by the mouse.
64
+ * Never UA sniffing.
65
+ *
66
+ * @defaultValue 'auto'
67
+ */
68
+ enabled?: boolean | 'auto';
69
+ /**
70
+ * Seconds moved per seek step.
71
+ *
72
+ * @defaultValue 10
73
+ */
74
+ seekSeconds?: number;
75
+ /**
76
+ * Milliseconds within which a second tap counts as a double tap.
77
+ *
78
+ * @defaultValue 275
79
+ */
80
+ doubleTapWindowMs?: number;
81
+ /**
82
+ * Milliseconds within which a further tap extends the current seek.
83
+ *
84
+ * @defaultValue 650
85
+ */
86
+ accumulationWindowMs?: number;
87
+ /**
88
+ * Fractions of the width given to the seek zones. The remainder in the middle
89
+ * is deliberately inert: a mistap in the centre of the picture should do
90
+ * nothing rather than jump the video.
91
+ *
92
+ * @defaultValue \{ left: 0.33, right: 0.33 \}
93
+ */
94
+ zones?: {
95
+ left?: number;
96
+ right?: number;
97
+ };
98
+ /**
99
+ * Movement in CSS pixels that cancels a tap.
100
+ *
101
+ * @defaultValue 10
102
+ */
103
+ slopPx?: number;
104
+ /**
105
+ * Show the ripple and seek amount.
106
+ *
107
+ * @defaultValue true
108
+ */
109
+ feedback?: boolean;
110
+ /**
111
+ * Vibrate on each seek step where the platform supports it. Android Chrome
112
+ * does; iOS Safari has no vibration API and silently ignores it.
113
+ *
114
+ * @defaultValue true
115
+ */
116
+ haptics?: boolean;
117
+ /**
118
+ * Let a single tap toggle the controls.
119
+ *
120
+ * @defaultValue true
121
+ */
122
+ tapToToggleControls?: boolean;
123
+ /** Index signature for PluginConfig compatibility */
124
+ [key: string]: unknown;
125
+ }
126
+
127
+ /**
128
+ * Gesture recognizer.
129
+ *
130
+ * A pure state machine. No DOM, no timers, no player: it consumes reduced
131
+ * pointer records and returns what they mean. Every timing and tolerance rule
132
+ * lives here so all of them are testable with plain numbers.
133
+ *
134
+ * The machine has three resting points:
135
+ *
136
+ * - idle: nothing in flight
137
+ * - awaiting-second: one tap landed, a second within the double-tap window
138
+ * would make it a seek
139
+ * - accumulating: a seek is under way, and each further tap in the same zone
140
+ * extends it, which is the behaviour every major player now shares
141
+ */
142
+
143
+ declare const DEFAULT_RECOGNIZER_OPTIONS: RecognizerOptions;
144
+ /** The recognizer's public surface. */
145
+ interface Recognizer {
146
+ /** Feed one pointer record. Returns everything it concluded, in order. */
147
+ handle(record: PointerRecord): RecognizerEvent[];
148
+ /**
149
+ * Advance time without a pointer event, so expiry is observable.
150
+ *
151
+ * @param now - Current time in milliseconds
152
+ */
153
+ tick(now: number): RecognizerEvent[];
154
+ /** Drop all in-flight state, for example when the media source changed. */
155
+ reset(): void;
156
+ /** Whether a seek sequence is currently accumulating. */
157
+ isAccumulating(): boolean;
158
+ }
159
+ /**
160
+ * Work out which zone a horizontal position belongs to.
161
+ *
162
+ * @param fraction - Position as a fraction of the surface width
163
+ * @param options - Zone sizes
164
+ * @returns The zone
165
+ */
166
+ declare function zoneFor(fraction: number, options: RecognizerOptions): GestureZone;
167
+ /**
168
+ * Create a gesture recognizer.
169
+ *
170
+ * @param options - Tuning overrides
171
+ * @returns A recognizer instance
172
+ */
173
+ declare function createRecognizer(options?: Partial<RecognizerOptions>): Recognizer;
174
+
175
+ /**
176
+ * Gesture surface and feedback.
177
+ *
178
+ * Layering rules that matter, and why:
179
+ *
180
+ * - The surface sits above the gradient and below the control bar, so the
181
+ * buttons and the progress bar keep every pixel they already own.
182
+ * - It never calls `preventDefault` or `stopPropagation` on a tap. The settings
183
+ * and quality menus close via document-level click listeners; swallowing the
184
+ * event would leave them stuck open.
185
+ * - Feedback elements are `pointer-events: none` and `aria-hidden`, with a
186
+ * single polite live region carrying the announcement instead.
187
+ */
188
+
189
+ /** Callbacks the overlay reports pointer activity through. */
190
+ interface OverlayOptions {
191
+ /** Called for every pointer record, already reduced and normalised. */
192
+ onPointer: (record: PointerRecord) => void;
193
+ /** Whether to render ripple and text feedback. */
194
+ feedback: boolean;
195
+ }
196
+ declare class GestureOverlay {
197
+ private container;
198
+ private options;
199
+ private el;
200
+ private zones;
201
+ private labels;
202
+ private live;
203
+ private styleEl;
204
+ private hideTimer;
205
+ private readonly pointerHandler;
206
+ constructor(container: HTMLElement, options: OverlayOptions);
207
+ /** Size the zones to match the recognizer's split. */
208
+ setZoneWidths(left: number, right: number): void;
209
+ /**
210
+ * Show the cumulative seek for a zone.
211
+ *
212
+ * @param zone - Which side was tapped
213
+ * @param seconds - Total seconds this sequence has moved
214
+ */
215
+ showSeek(zone: GestureZone, seconds: number): void;
216
+ /** Announce that a forward seek was refused because the viewer is at the live edge. */
217
+ announceLiveEdge(): void;
218
+ destroy(): void;
219
+ /** Exposed for tests and for hosts that want to inspect the surface. */
220
+ getElement(): HTMLElement;
221
+ private createZone;
222
+ private injectStyles;
223
+ }
224
+
225
+ /**
226
+ * Gestures Plugin for Scarlett Player
227
+ *
228
+ * Double-tap the right of the picture to jump forward, the left to jump back,
229
+ * and keep tapping to go further. This is the interaction every phone viewer
230
+ * already knows from YouTube, and on a PPV stream watched mostly on phones it
231
+ * is the difference between scrubbing blind and landing where you meant to.
232
+ *
233
+ * Touch only, by input type rather than user agent: a mouse or pen never
234
+ * triggers any of it, so desktop behaviour is unchanged.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * import { createGesturesPlugin } from '@scarlett-player/gestures';
239
+ *
240
+ * const player = new ScarlettPlayer({
241
+ * container: '#player',
242
+ * plugins: [uiPlugin(), createGesturesPlugin({ seekSeconds: 10 })],
243
+ * });
244
+ * ```
245
+ */
246
+
247
+ /** Public surface, including the ownership hook the UI package checks. */
248
+ interface GesturesPlugin extends Plugin {
249
+ /**
250
+ * Whether this plugin is handling taps right now.
251
+ *
252
+ * The UI package calls this before running its own show-controls logic on a
253
+ * touch interaction. Structural check, no package dependency in either
254
+ * direction.
255
+ */
256
+ ownsTapInteraction(): boolean;
257
+ }
258
+ /**
259
+ * Create a Gestures Plugin instance.
260
+ *
261
+ * @param config - Plugin configuration
262
+ * @returns Gestures Plugin instance
263
+ */
264
+ declare function createGesturesPlugin(config?: GesturesPluginConfig): GesturesPlugin;
265
+
266
+ export { DEFAULT_RECOGNIZER_OPTIONS, GestureOverlay, type GestureZone, type GesturesPlugin, type GesturesPluginConfig, type PointerRecord, type Recognizer, type RecognizerEvent, createGesturesPlugin, createRecognizer, createGesturesPlugin as default, zoneFor };
@@ -0,0 +1,266 @@
1
+ import { Plugin } from '@scarlett-player/core';
2
+
3
+ /**
4
+ * Types for the Gestures Plugin.
5
+ */
6
+ /** Horizontal region of the gesture surface a pointer landed in. */
7
+ type GestureZone = 'left' | 'middle' | 'right';
8
+ /**
9
+ * A pointer event reduced to what the recognizer needs.
10
+ *
11
+ * Deliberately not a DOM event: the recognizer is a pure state machine, and
12
+ * every timing rule in it is testable without a browser.
13
+ */
14
+ interface PointerRecord {
15
+ /** What happened. */
16
+ type: 'down' | 'move' | 'up' | 'cancel';
17
+ /** Horizontal position in CSS pixels, for slop measurement. */
18
+ x: number;
19
+ /** Vertical position in CSS pixels, for slop measurement. */
20
+ y: number;
21
+ /** Horizontal position as a fraction of the surface width, for zoning. */
22
+ fraction: number;
23
+ /** Pointer identity, so a second finger can be told apart from a retap. */
24
+ pointerId: number;
25
+ /** Event timestamp in milliseconds. */
26
+ timeStamp: number;
27
+ }
28
+ /** What the recognizer concluded from a stream of pointer records. */
29
+ type RecognizerEvent = {
30
+ type: 'tap';
31
+ zone: GestureZone;
32
+ } | {
33
+ type: 'double-tap';
34
+ zone: GestureZone;
35
+ count: number;
36
+ } | {
37
+ type: 'accumulate';
38
+ zone: GestureZone;
39
+ count: number;
40
+ } | {
41
+ type: 'cancel';
42
+ };
43
+ /** Recognizer tuning. */
44
+ interface RecognizerOptions {
45
+ /** Milliseconds within which a second tap counts as a double tap. */
46
+ doubleTapWindowMs: number;
47
+ /** Milliseconds within which a further tap extends an active seek sequence. */
48
+ accumulationWindowMs: number;
49
+ /** Fraction of the width belonging to the left zone. */
50
+ leftZone: number;
51
+ /** Fraction of the width belonging to the right zone. */
52
+ rightZone: number;
53
+ /** Movement in CSS pixels that turns a tap into a drag and cancels it. */
54
+ slopPx: number;
55
+ }
56
+ /** Configuration for the Gestures Plugin. */
57
+ interface GesturesPluginConfig {
58
+ /**
59
+ * Whether gestures are active.
60
+ *
61
+ * `'auto'` enables them when the device reports a coarse pointer, and every
62
+ * individual gesture is additionally checked for `pointerType === 'touch'`,
63
+ * so a touchscreen laptop works with a finger and is untouched by the mouse.
64
+ * Never UA sniffing.
65
+ *
66
+ * @defaultValue 'auto'
67
+ */
68
+ enabled?: boolean | 'auto';
69
+ /**
70
+ * Seconds moved per seek step.
71
+ *
72
+ * @defaultValue 10
73
+ */
74
+ seekSeconds?: number;
75
+ /**
76
+ * Milliseconds within which a second tap counts as a double tap.
77
+ *
78
+ * @defaultValue 275
79
+ */
80
+ doubleTapWindowMs?: number;
81
+ /**
82
+ * Milliseconds within which a further tap extends the current seek.
83
+ *
84
+ * @defaultValue 650
85
+ */
86
+ accumulationWindowMs?: number;
87
+ /**
88
+ * Fractions of the width given to the seek zones. The remainder in the middle
89
+ * is deliberately inert: a mistap in the centre of the picture should do
90
+ * nothing rather than jump the video.
91
+ *
92
+ * @defaultValue \{ left: 0.33, right: 0.33 \}
93
+ */
94
+ zones?: {
95
+ left?: number;
96
+ right?: number;
97
+ };
98
+ /**
99
+ * Movement in CSS pixels that cancels a tap.
100
+ *
101
+ * @defaultValue 10
102
+ */
103
+ slopPx?: number;
104
+ /**
105
+ * Show the ripple and seek amount.
106
+ *
107
+ * @defaultValue true
108
+ */
109
+ feedback?: boolean;
110
+ /**
111
+ * Vibrate on each seek step where the platform supports it. Android Chrome
112
+ * does; iOS Safari has no vibration API and silently ignores it.
113
+ *
114
+ * @defaultValue true
115
+ */
116
+ haptics?: boolean;
117
+ /**
118
+ * Let a single tap toggle the controls.
119
+ *
120
+ * @defaultValue true
121
+ */
122
+ tapToToggleControls?: boolean;
123
+ /** Index signature for PluginConfig compatibility */
124
+ [key: string]: unknown;
125
+ }
126
+
127
+ /**
128
+ * Gesture recognizer.
129
+ *
130
+ * A pure state machine. No DOM, no timers, no player: it consumes reduced
131
+ * pointer records and returns what they mean. Every timing and tolerance rule
132
+ * lives here so all of them are testable with plain numbers.
133
+ *
134
+ * The machine has three resting points:
135
+ *
136
+ * - idle: nothing in flight
137
+ * - awaiting-second: one tap landed, a second within the double-tap window
138
+ * would make it a seek
139
+ * - accumulating: a seek is under way, and each further tap in the same zone
140
+ * extends it, which is the behaviour every major player now shares
141
+ */
142
+
143
+ declare const DEFAULT_RECOGNIZER_OPTIONS: RecognizerOptions;
144
+ /** The recognizer's public surface. */
145
+ interface Recognizer {
146
+ /** Feed one pointer record. Returns everything it concluded, in order. */
147
+ handle(record: PointerRecord): RecognizerEvent[];
148
+ /**
149
+ * Advance time without a pointer event, so expiry is observable.
150
+ *
151
+ * @param now - Current time in milliseconds
152
+ */
153
+ tick(now: number): RecognizerEvent[];
154
+ /** Drop all in-flight state, for example when the media source changed. */
155
+ reset(): void;
156
+ /** Whether a seek sequence is currently accumulating. */
157
+ isAccumulating(): boolean;
158
+ }
159
+ /**
160
+ * Work out which zone a horizontal position belongs to.
161
+ *
162
+ * @param fraction - Position as a fraction of the surface width
163
+ * @param options - Zone sizes
164
+ * @returns The zone
165
+ */
166
+ declare function zoneFor(fraction: number, options: RecognizerOptions): GestureZone;
167
+ /**
168
+ * Create a gesture recognizer.
169
+ *
170
+ * @param options - Tuning overrides
171
+ * @returns A recognizer instance
172
+ */
173
+ declare function createRecognizer(options?: Partial<RecognizerOptions>): Recognizer;
174
+
175
+ /**
176
+ * Gesture surface and feedback.
177
+ *
178
+ * Layering rules that matter, and why:
179
+ *
180
+ * - The surface sits above the gradient and below the control bar, so the
181
+ * buttons and the progress bar keep every pixel they already own.
182
+ * - It never calls `preventDefault` or `stopPropagation` on a tap. The settings
183
+ * and quality menus close via document-level click listeners; swallowing the
184
+ * event would leave them stuck open.
185
+ * - Feedback elements are `pointer-events: none` and `aria-hidden`, with a
186
+ * single polite live region carrying the announcement instead.
187
+ */
188
+
189
+ /** Callbacks the overlay reports pointer activity through. */
190
+ interface OverlayOptions {
191
+ /** Called for every pointer record, already reduced and normalised. */
192
+ onPointer: (record: PointerRecord) => void;
193
+ /** Whether to render ripple and text feedback. */
194
+ feedback: boolean;
195
+ }
196
+ declare class GestureOverlay {
197
+ private container;
198
+ private options;
199
+ private el;
200
+ private zones;
201
+ private labels;
202
+ private live;
203
+ private styleEl;
204
+ private hideTimer;
205
+ private readonly pointerHandler;
206
+ constructor(container: HTMLElement, options: OverlayOptions);
207
+ /** Size the zones to match the recognizer's split. */
208
+ setZoneWidths(left: number, right: number): void;
209
+ /**
210
+ * Show the cumulative seek for a zone.
211
+ *
212
+ * @param zone - Which side was tapped
213
+ * @param seconds - Total seconds this sequence has moved
214
+ */
215
+ showSeek(zone: GestureZone, seconds: number): void;
216
+ /** Announce that a forward seek was refused because the viewer is at the live edge. */
217
+ announceLiveEdge(): void;
218
+ destroy(): void;
219
+ /** Exposed for tests and for hosts that want to inspect the surface. */
220
+ getElement(): HTMLElement;
221
+ private createZone;
222
+ private injectStyles;
223
+ }
224
+
225
+ /**
226
+ * Gestures Plugin for Scarlett Player
227
+ *
228
+ * Double-tap the right of the picture to jump forward, the left to jump back,
229
+ * and keep tapping to go further. This is the interaction every phone viewer
230
+ * already knows from YouTube, and on a PPV stream watched mostly on phones it
231
+ * is the difference between scrubbing blind and landing where you meant to.
232
+ *
233
+ * Touch only, by input type rather than user agent: a mouse or pen never
234
+ * triggers any of it, so desktop behaviour is unchanged.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * import { createGesturesPlugin } from '@scarlett-player/gestures';
239
+ *
240
+ * const player = new ScarlettPlayer({
241
+ * container: '#player',
242
+ * plugins: [uiPlugin(), createGesturesPlugin({ seekSeconds: 10 })],
243
+ * });
244
+ * ```
245
+ */
246
+
247
+ /** Public surface, including the ownership hook the UI package checks. */
248
+ interface GesturesPlugin extends Plugin {
249
+ /**
250
+ * Whether this plugin is handling taps right now.
251
+ *
252
+ * The UI package calls this before running its own show-controls logic on a
253
+ * touch interaction. Structural check, no package dependency in either
254
+ * direction.
255
+ */
256
+ ownsTapInteraction(): boolean;
257
+ }
258
+ /**
259
+ * Create a Gestures Plugin instance.
260
+ *
261
+ * @param config - Plugin configuration
262
+ * @returns Gestures Plugin instance
263
+ */
264
+ declare function createGesturesPlugin(config?: GesturesPluginConfig): GesturesPlugin;
265
+
266
+ export { DEFAULT_RECOGNIZER_OPTIONS, GestureOverlay, type GestureZone, type GesturesPlugin, type GesturesPluginConfig, type PointerRecord, type Recognizer, type RecognizerEvent, createGesturesPlugin, createRecognizer, createGesturesPlugin as default, zoneFor };