@reuters-graphics/graphics-components 3.0.12 → 3.0.13

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.
Files changed (75) hide show
  1. package/dist/components/@types/global.d.ts +45 -0
  2. package/dist/components/Scroller/Scroller.mdx +2 -2
  3. package/dist/components/ScrollerBase/ScrollerBase.mdx +0 -5
  4. package/dist/components/ScrollerVideo/Debug.svelte +207 -0
  5. package/dist/components/ScrollerVideo/Debug.svelte.d.ts +5 -0
  6. package/dist/components/ScrollerVideo/ScrollerVideo.mdx +462 -0
  7. package/dist/components/ScrollerVideo/ScrollerVideo.stories.svelte +190 -0
  8. package/dist/components/ScrollerVideo/ScrollerVideo.stories.svelte.d.ts +19 -0
  9. package/dist/components/ScrollerVideo/ScrollerVideo.svelte +292 -0
  10. package/dist/components/ScrollerVideo/ScrollerVideo.svelte.d.ts +58 -0
  11. package/dist/components/ScrollerVideo/ScrollerVideoForeground.svelte +164 -0
  12. package/dist/components/ScrollerVideo/ScrollerVideoForeground.svelte.d.ts +17 -0
  13. package/dist/components/ScrollerVideo/demo/AdvancedUsecases.svelte +114 -0
  14. package/dist/components/ScrollerVideo/demo/AdvancedUsecases.svelte.d.ts +3 -0
  15. package/dist/components/ScrollerVideo/demo/Embedded.svelte +94 -0
  16. package/dist/components/ScrollerVideo/demo/Embedded.svelte.d.ts +3 -0
  17. package/dist/components/ScrollerVideo/demo/WithAi2svelteForegrounds.svelte +117 -0
  18. package/dist/components/ScrollerVideo/demo/WithAi2svelteForegrounds.svelte.d.ts +3 -0
  19. package/dist/components/ScrollerVideo/demo/WithScrollerBase.svelte +80 -0
  20. package/dist/components/ScrollerVideo/demo/WithScrollerBase.svelte.d.ts +3 -0
  21. package/dist/components/ScrollerVideo/demo/WithTextForegrounds.svelte +72 -0
  22. package/dist/components/ScrollerVideo/demo/WithTextForegrounds.svelte.d.ts +18 -0
  23. package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/ai-chart.svelte +631 -0
  24. package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/ai-chart.svelte.d.ts +3 -0
  25. package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation1.svelte +428 -0
  26. package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation1.svelte.d.ts +26 -0
  27. package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation2.svelte +402 -0
  28. package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation2.svelte.d.ts +26 -0
  29. package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation3.svelte +398 -0
  30. package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation3.svelte.d.ts +26 -0
  31. package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation4.svelte +360 -0
  32. package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation4.svelte.d.ts +26 -0
  33. package/dist/components/ScrollerVideo/demo/graphic/imgs/ai-chart-md.png +0 -0
  34. package/dist/components/ScrollerVideo/demo/graphic/imgs/ai-chart-sm.png +0 -0
  35. package/dist/components/ScrollerVideo/demo/graphic/imgs/ai-chart-xs.png +0 -0
  36. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation1-lg.png +0 -0
  37. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation1-md.png +0 -0
  38. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation1-sm.png +0 -0
  39. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation1-xl.png +0 -0
  40. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation1-xs.png +0 -0
  41. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation2-lg.png +0 -0
  42. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation2-md.png +0 -0
  43. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation2-sm.png +0 -0
  44. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation2-xl.png +0 -0
  45. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation2-xs.png +0 -0
  46. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation3-lg.png +0 -0
  47. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation3-md.png +0 -0
  48. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation3-sm.png +0 -0
  49. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation3-xl.png +0 -0
  50. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation3-xs.png +0 -0
  51. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation4-lg.png +0 -0
  52. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation4-md.png +0 -0
  53. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation4-sm.png +0 -0
  54. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation4-xl.png +0 -0
  55. package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation4-xs.png +0 -0
  56. package/dist/components/ScrollerVideo/ts/ScrollerVideo.d.ts +248 -0
  57. package/dist/components/ScrollerVideo/ts/ScrollerVideo.js +762 -0
  58. package/dist/components/ScrollerVideo/ts/mp4box.d.ts +137 -0
  59. package/dist/components/ScrollerVideo/ts/state.svelte.d.ts +51 -0
  60. package/dist/components/ScrollerVideo/ts/state.svelte.js +25 -0
  61. package/dist/components/ScrollerVideo/ts/utils.d.ts +70 -0
  62. package/dist/components/ScrollerVideo/ts/utils.js +92 -0
  63. package/dist/components/ScrollerVideo/ts/videoDecoder.d.ts +11 -0
  64. package/dist/components/ScrollerVideo/ts/videoDecoder.js +193 -0
  65. package/dist/components/ScrollerVideo/videos/HPO.mp4 +0 -0
  66. package/dist/components/ScrollerVideo/videos/drone.mp4 +0 -0
  67. package/dist/components/ScrollerVideo/videos/goldengate.mp4 +0 -0
  68. package/dist/components/ScrollerVideo/videos/tennis.mp4 +0 -0
  69. package/dist/components/ScrollerVideo/videos/waves_lg.mp4 +0 -0
  70. package/dist/components/ScrollerVideo/videos/waves_md.mp4 +0 -0
  71. package/dist/components/ScrollerVideo/videos/waves_sm.mp4 +0 -0
  72. package/dist/components/SiteHeadline/SiteHeadline.mdx +4 -1
  73. package/dist/index.d.ts +3 -1
  74. package/dist/index.js +2 -0
  75. package/package.json +3 -1
@@ -0,0 +1,762 @@
1
+ import { UAParser } from 'ua-parser-js';
2
+ import videoDecoder from './videoDecoder';
3
+ import { debounce, isScrollPositionAtTarget, map, constrain } from './utils';
4
+ import { createComponentState } from './state.svelte';
5
+ /**
6
+ * ScrollerVideo class for scroll-driven or programmatic video playback with Svelte integration.
7
+ */
8
+ class ScrollerVideo {
9
+ /**
10
+ * The container element for the video or canvas.
11
+ * @type {HTMLElement | null}
12
+ */
13
+ container;
14
+ /**
15
+ * The original container argument (element or string ID).
16
+ * @type {Element | string | undefined}
17
+ */
18
+ scrollerVideoContainer;
19
+ /**
20
+ * Video source URL.
21
+ * @type {string}
22
+ */
23
+ src;
24
+ /**
25
+ * Speed of transitions.
26
+ * @type {number}
27
+ */
28
+ transitionSpeed;
29
+ /**
30
+ * Threshold for frame transitions.
31
+ * @type {number}
32
+ */
33
+ frameThreshold;
34
+ /**
35
+ * Whether to use WebCodecs for decoding.
36
+ * @type {boolean}
37
+ */
38
+ useWebCodecs;
39
+ /**
40
+ * CSS object-fit property for video/canvas.
41
+ * @type {string}
42
+ */
43
+ objectFit;
44
+ /**
45
+ * Whether to use sticky positioning.
46
+ * @type {boolean}
47
+ */
48
+ sticky;
49
+ /**
50
+ * Whether to track scroll position.
51
+ * @type {boolean}
52
+ */
53
+ trackScroll;
54
+ /**
55
+ * Callback when ready.
56
+ * @type {() => void}
57
+ */
58
+ onReady;
59
+ /**
60
+ * Callback on scroll percentage change.
61
+ * @type {(percentage?: number) => void}
62
+ */
63
+ onChange;
64
+ /**
65
+ * Enable debug logging.
66
+ * @type {boolean}
67
+ */
68
+ debug;
69
+ /**
70
+ * Enable autoplay.
71
+ * @type {boolean}
72
+ */
73
+ autoplay;
74
+ /**
75
+ * The HTML video element.
76
+ * @type {HTMLVideoElement | undefined}
77
+ */
78
+ video;
79
+ /**
80
+ * Current scroll/video percentage (0-1).
81
+ * @type {number}
82
+ */
83
+ videoPercentage;
84
+ /**
85
+ * True if browser is Safari.
86
+ * @type {boolean}
87
+ */
88
+ isSafari;
89
+ /**
90
+ * Current video time in seconds.
91
+ * @type {number}
92
+ */
93
+ currentTime;
94
+ /**
95
+ * Target video time in seconds.
96
+ * @type {number}
97
+ */
98
+ targetTime;
99
+ /**
100
+ * Canvas for rendering frames (if using WebCodecs).
101
+ * @type {HTMLCanvasElement | null}
102
+ */
103
+ canvas;
104
+ /**
105
+ * 2D context for the canvas.
106
+ * @type {CanvasRenderingContext2D | null}
107
+ */
108
+ context;
109
+ /**
110
+ * Decoded video frames (if using WebCodecs).
111
+ * @type {ImageBitmap[] | null}
112
+ */
113
+ frames;
114
+ /**
115
+ * Video frame rate.
116
+ * @type {number}
117
+ */
118
+ frameRate;
119
+ /**
120
+ * Target scroll position in pixels, if set.
121
+ * @type {number | null}
122
+ */
123
+ targetScrollPosition = null;
124
+ /**
125
+ * Current frame index (if using WebCodecs).
126
+ * @type {number}
127
+ */
128
+ currentFrame;
129
+ /**
130
+ * True if using WebCodecs for decoding.
131
+ * @type {boolean}
132
+ */
133
+ usingWebCodecs;
134
+ /**
135
+ * Total video duration in seconds.
136
+ * @type {number}
137
+ */
138
+ totalTime;
139
+ /**
140
+ * RequestAnimationFrame ID for transitions.
141
+ * @type {number | null}
142
+ */
143
+ transitioningRaf;
144
+ /**
145
+ * State object for component-level state.
146
+ * @type {ScrollerVideoState}
147
+ */
148
+ componentState;
149
+ /**
150
+ * Function to update scroll percentage (set in constructor).
151
+ * @type {((jump: boolean) => void) | undefined}
152
+ */
153
+ updateScrollPercentage;
154
+ /**
155
+ * Function to handle resize events (set in constructor).
156
+ * @type {(() => void) | undefined}
157
+ */
158
+ resize;
159
+ /**
160
+ * Creates a new ScrollerVideo instance.
161
+ * @param {ScrollerVideoArgs} args - The arguments for initialization.
162
+ */
163
+ constructor({ src, scrollerVideoContainer, objectFit = 'cover', sticky = true, full = true, trackScroll = true, lockScroll = true, transitionSpeed = 8, frameThreshold = 0.1, useWebCodecs = true, onReady = () => { }, onChange = (_percentage) => { }, debug = false, autoplay = false, }) {
164
+ this.src = src;
165
+ this.scrollerVideoContainer = scrollerVideoContainer;
166
+ this.objectFit = objectFit;
167
+ this.sticky = sticky;
168
+ this.trackScroll = trackScroll;
169
+ this.transitionSpeed = transitionSpeed;
170
+ this.frameThreshold = frameThreshold;
171
+ this.useWebCodecs = useWebCodecs;
172
+ this.onReady = onReady;
173
+ this.onChange = onChange;
174
+ this.debug = debug;
175
+ this.autoplay = autoplay;
176
+ this.videoPercentage = 0;
177
+ this.isSafari = false;
178
+ this.currentTime = 0;
179
+ this.targetTime = 0;
180
+ this.canvas = null;
181
+ this.context = null;
182
+ this.container = null;
183
+ this.frames = null;
184
+ this.frameRate = 0;
185
+ this.currentTime = 0; // Saves the currentTime of the video, synced with this.video.currentTime
186
+ this.targetTime = 0; // The target time before a transition happens
187
+ this.canvas = null; // The canvas for drawing the frames decoded by webCodecs
188
+ this.context = null; // The canvas context
189
+ this.frames = []; // The frames decoded by webCodecs
190
+ this.frameRate = 0; // Calculation of frameRate so we know which frame to paint
191
+ this.currentFrame = 0;
192
+ this.videoPercentage = 0;
193
+ this.usingWebCodecs = false; // Whether we are using webCodecs
194
+ this.totalTime = 0; // The total time of the video, used for calculating percentage
195
+ this.transitioningRaf = null;
196
+ this.componentState = createComponentState();
197
+ this.componentState.willAutoPlay = autoplay;
198
+ // Save the container. If the container is a string we get the element
199
+ if (scrollerVideoContainer && scrollerVideoContainer instanceof HTMLElement)
200
+ this.container = scrollerVideoContainer;
201
+ // otherwise it should better be an element
202
+ else if (typeof scrollerVideoContainer === 'string') {
203
+ this.container = document.getElementById(scrollerVideoContainer) || null;
204
+ if (!this.container)
205
+ throw new Error('scrollerVideoContainer must be a valid DOM object');
206
+ }
207
+ else {
208
+ throw new Error('scrollerVideoContainer must be a valid DOM object');
209
+ }
210
+ // Create the initial video object. Even if we are going to use webcodecs,
211
+ // we start with a paused video object
212
+ this.video = document.createElement('video');
213
+ this.video.src = src;
214
+ this.video.preload = 'auto';
215
+ this.video.tabIndex = 0;
216
+ this.video.preload = 'auto';
217
+ this.video.playsInline = true;
218
+ this.video.muted = true;
219
+ this.video.pause();
220
+ this.video.load();
221
+ this.video.addEventListener('canplaythrough', () => {
222
+ this.onReady();
223
+ if (this.autoplay && !this.useWebCodecs) {
224
+ this.autoplayScroll();
225
+ }
226
+ }, { once: true });
227
+ // Start the video percentage at 0
228
+ this.videoPercentage = 0;
229
+ // Adds the video to the container
230
+ this.container.appendChild(this.video);
231
+ // Setting CSS properties for sticky
232
+ if (sticky) {
233
+ this.container.style.display = 'block';
234
+ this.container.style.position = 'sticky';
235
+ this.container.style.top = '0';
236
+ }
237
+ // Setting CSS properties for full
238
+ if (full) {
239
+ this.container.style.width = '100%';
240
+ this.container.style.height = '100lvh';
241
+ this.container.style.overflow = 'hidden';
242
+ }
243
+ // Setting CSS properties for cover
244
+ if (objectFit)
245
+ this.setCoverStyle(this.video);
246
+ // Detect webkit (safari), because webkit requires special attention
247
+ const browserEngine = new UAParser().getEngine();
248
+ this.isSafari = browserEngine.name === 'WebKit';
249
+ if (debug && this.isSafari)
250
+ console.info('Safari browser detected');
251
+ const debouncedScroll = debounce(() => {
252
+ window.requestAnimationFrame(() => {
253
+ this.setScrollPercent(this.videoPercentage);
254
+ });
255
+ }, 100);
256
+ // Add scroll listener for responding to scroll position
257
+ this.updateScrollPercentage = (jump = false) => {
258
+ // Used for internally setting the scroll percentage based on built-in listeners
259
+ let containerBoundingClientRect;
260
+ if (this.container &&
261
+ this.container.parentNode &&
262
+ this.container.parentNode.getBoundingClientRect) {
263
+ containerBoundingClientRect = this.container.parentNode.getBoundingClientRect();
264
+ }
265
+ else {
266
+ if (this.debug) {
267
+ console.error('ScrollerVideo: container or parentNode is null or invalid.');
268
+ }
269
+ return;
270
+ }
271
+ // Calculate the current scroll percent of the video
272
+ let scrollPercent = -containerBoundingClientRect.top /
273
+ (containerBoundingClientRect.height - window.innerHeight);
274
+ // if autplay, trim the playing time to last locked video position
275
+ if (this.componentState.autoplayProgress > 0) {
276
+ scrollPercent = map(scrollPercent, 0, 1, this.componentState.autoplayProgress, 1);
277
+ }
278
+ if (this.debug) {
279
+ console.info('ScrollerVideo scrolled to', scrollPercent);
280
+ }
281
+ // toggle autoplaying state on manual intervention
282
+ if (this.componentState.isAutoPlaying && this.frames) {
283
+ if (this.debug)
284
+ console.warn('Stopping autoplay due to manual scroll');
285
+ if (this.usingWebCodecs) {
286
+ this.componentState.autoplayProgress = parseFloat((this.currentFrame / this.frames.length).toFixed(4));
287
+ }
288
+ else {
289
+ this.componentState.autoplayProgress = parseFloat((this.currentTime / this.totalTime).toFixed(4));
290
+ }
291
+ this.componentState.isAutoPlaying = false;
292
+ }
293
+ this.videoPercentage = scrollPercent;
294
+ if (this.targetScrollPosition == null) {
295
+ this.setTargetTimePercent(scrollPercent, { jump });
296
+ this.onChange(scrollPercent);
297
+ }
298
+ else if (isScrollPositionAtTarget(this.targetScrollPosition)) {
299
+ this.targetScrollPosition = null;
300
+ }
301
+ else if (lockScroll && this.targetScrollPosition != null) {
302
+ debouncedScroll();
303
+ }
304
+ this.updateDebugInfo();
305
+ };
306
+ // Add our event listeners for handling changes to the window or scroll
307
+ if (this.trackScroll) {
308
+ window.addEventListener('scroll', () => {
309
+ if (this.updateScrollPercentage) {
310
+ this.updateScrollPercentage(false);
311
+ }
312
+ });
313
+ // Set the initial scroll percentage
314
+ this.video.addEventListener('loadedmetadata', () => {
315
+ if (this.updateScrollPercentage) {
316
+ this.updateScrollPercentage(true);
317
+ }
318
+ if (this.video) {
319
+ this.totalTime = this.video.duration;
320
+ }
321
+ this.setCoverStyle(this.canvas || this.video);
322
+ }, { once: true });
323
+ }
324
+ else {
325
+ this.video.addEventListener('loadedmetadata', () => {
326
+ this.setTargetTimePercent(0, { jump: true });
327
+ if (this.video) {
328
+ this.totalTime = this.video.duration;
329
+ }
330
+ this.setCoverStyle(this.canvas || this.video);
331
+ }, { once: true });
332
+ }
333
+ // Add resize function
334
+ this.resize = () => {
335
+ if (this.debug)
336
+ console.info('ScrollerVideo resizing...');
337
+ // On resize, we need to reset the cover style
338
+ if (this.objectFit)
339
+ this.setCoverStyle(this.canvas || this.video);
340
+ // Then repaint the canvas, if we are in useWebcodecs
341
+ this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
342
+ };
343
+ window.addEventListener('resize', this.resize);
344
+ this.video.addEventListener('progress', this.resize);
345
+ // Calls decode video to attempt webcodecs method
346
+ this.decodeVideo();
347
+ this.updateDebugInfo();
348
+ }
349
+ /**
350
+ * Sets the currentTime of the video as a specified percentage of its total duration.
351
+ *
352
+ * @param percentage - The percentage of the video duration to set as the current time.
353
+ * @param options - Configuration options for adjusting the video playback.
354
+ * - autoplay: boolean - If true, the video will start playing immediately after setting the percentage. Default is false.
355
+ * - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time.
356
+ * - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8.
357
+ * - easing: (progress: number) => number - A function that defines the easing curve for the transition. It takes the progress ratio (a number between 0 and 1) as an argument and returns the eased value, affecting the playback speed during the transition.
358
+ */
359
+ setVideoPercentage(percentage, options = { jump: false, transitionSpeed: 8 }) {
360
+ // Early termination if the video percentage is already at the percentage that is intended.
361
+ if (this.videoPercentage === percentage)
362
+ return;
363
+ if (this.transitioningRaf) {
364
+ window.cancelAnimationFrame(this.transitioningRaf);
365
+ }
366
+ this.videoPercentage = percentage;
367
+ this.onChange(percentage);
368
+ if (this.trackScroll && !options.autoplay) {
369
+ this.setScrollPercent(percentage);
370
+ }
371
+ this.setTargetTimePercent(percentage, options);
372
+ }
373
+ /**
374
+ * Sets the style of the video or canvas to "cover" its container.
375
+ * @param {HTMLElement | HTMLCanvasElement | undefined} el - The element to style.
376
+ */
377
+ setCoverStyle(el) {
378
+ if (!el) {
379
+ if (this.debug)
380
+ console.warn('No element to set cover style on');
381
+ return;
382
+ }
383
+ if (this.objectFit) {
384
+ el.style.position = 'absolute';
385
+ el.style.top = '50%';
386
+ el.style.left = '50%';
387
+ el.style.transform = 'translate(-50%, -50%)';
388
+ // el.style.minWidth = '101%';
389
+ // el.style.minHeight = '101%';
390
+ // Gets the width and height of the container
391
+ const { width: containerWidth, height: containerHeight } = this.container?.getBoundingClientRect() || { width: 0, height: 0 };
392
+ let width = 0, height = 0;
393
+ if (el instanceof HTMLVideoElement) {
394
+ width = el.videoWidth;
395
+ height = el.videoHeight;
396
+ }
397
+ else if (el instanceof HTMLCanvasElement) {
398
+ width = el.width;
399
+ height = el.height;
400
+ }
401
+ if (this.debug)
402
+ console.info('Container dimensions:', [
403
+ containerWidth,
404
+ containerHeight,
405
+ ]);
406
+ if (this.debug)
407
+ console.info('Element dimensions:', [width, height]);
408
+ // Determines which axis needs to be 100% and which needs to be scaled
409
+ if (this.objectFit == 'cover') {
410
+ if (containerWidth / containerHeight > width / height) {
411
+ el.style.width = '100%';
412
+ el.style.height = 'auto';
413
+ }
414
+ else {
415
+ el.style.height = '100%';
416
+ el.style.width = 'auto';
417
+ }
418
+ }
419
+ else if (this.objectFit == 'contain') {
420
+ if (containerWidth / containerHeight > width / height) {
421
+ el.style.height = '100%';
422
+ el.style.width = 'auto';
423
+ }
424
+ else {
425
+ el.style.width = '100%';
426
+ el.style.height = 'auto';
427
+ }
428
+ }
429
+ }
430
+ }
431
+ /**
432
+ * Uses webCodecs to decode the video into frames.
433
+ * @returns {Promise<void>} Resolves when decoding is complete.
434
+ */
435
+ async decodeVideo() {
436
+ if (!this.useWebCodecs) {
437
+ if (this.debug)
438
+ console.warn('Cannot perform video decode: `useWebCodes` disabled');
439
+ return;
440
+ }
441
+ if (!this.src) {
442
+ if (this.debug)
443
+ console.warn('Cannot perform video decode: no `src` found');
444
+ return;
445
+ }
446
+ try {
447
+ await videoDecoder(this.src, (frame) => {
448
+ this.frames?.push(frame);
449
+ }, this.debug).then((codec) => {
450
+ this.usingWebCodecs = true;
451
+ if (typeof codec == 'string') {
452
+ this.componentState.framesData.codec = codec;
453
+ }
454
+ });
455
+ }
456
+ catch (error) {
457
+ if (this.debug)
458
+ console.error('Error encountered while decoding video', error);
459
+ // Remove all decoded frames if a failure happens during decoding
460
+ this.frames = [];
461
+ // Force a video reload when videoDecoder fails
462
+ this.video?.load();
463
+ }
464
+ // If no frames, something went wrong
465
+ if (this.frames?.length === 0) {
466
+ if (this.debug)
467
+ console.error('No frames were received from webCodecs');
468
+ this.onReady();
469
+ return;
470
+ }
471
+ // Calculate the frameRate based on number of frames and the duration
472
+ this.frameRate =
473
+ this.frames && this.video ? this.frames.length / this.video.duration : 0;
474
+ if (this.debug)
475
+ console.info('Received', this.frames?.length, 'frames. Video frame rate:', this.frameRate);
476
+ // Remove the video and add the canvas
477
+ this.canvas = document.createElement('canvas');
478
+ this.context = this.canvas.getContext('2d');
479
+ // Hide the video and add the canvas to the container
480
+ if (this.video) {
481
+ this.video.style.display = 'none';
482
+ }
483
+ if (this.container) {
484
+ this.container.appendChild(this.canvas);
485
+ }
486
+ if (this.objectFit)
487
+ this.setCoverStyle(this.canvas);
488
+ // Paint our first frame
489
+ this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
490
+ this.onReady();
491
+ if (this.autoplay)
492
+ this.autoplayScroll();
493
+ }
494
+ /**
495
+ * Paints the frame to the canvas.
496
+ * @param {number} frameNum - The frame index to paint.
497
+ */
498
+ paintCanvasFrame(frameNum) {
499
+ if (!this.frames) {
500
+ if (this.debug)
501
+ console.warn('No frames available to paint');
502
+ return;
503
+ }
504
+ // Get the frame and paint it to the canvas
505
+ const currFrame = this.frames[frameNum];
506
+ this.currentFrame = frameNum;
507
+ if (!this.canvas || !currFrame) {
508
+ return;
509
+ }
510
+ if (this.debug) {
511
+ console.info('Painting frame', frameNum);
512
+ }
513
+ // Make sure the canvas is scaled properly, similar to setCoverStyle
514
+ this.canvas.width = currFrame.width;
515
+ this.canvas.height = currFrame.height;
516
+ const { width, height } = this.container?.getBoundingClientRect() || {
517
+ width: 0,
518
+ height: 0,
519
+ };
520
+ if (this.objectFit == 'cover') {
521
+ if (width / height > currFrame.width / currFrame.height) {
522
+ this.canvas.style.width = '100%';
523
+ this.canvas.style.height = 'auto';
524
+ }
525
+ else {
526
+ this.canvas.style.height = '100%';
527
+ this.canvas.style.width = 'auto';
528
+ }
529
+ }
530
+ else if (this.objectFit == 'contain') {
531
+ if (width / height > currFrame.width / currFrame.height) {
532
+ this.canvas.style.height = '100%';
533
+ this.canvas.style.width = 'auto';
534
+ }
535
+ else {
536
+ this.canvas.style.width = '100%';
537
+ this.canvas.style.height = 'auto';
538
+ }
539
+ }
540
+ // Draw the frame to the canvas context
541
+ if (!this.context) {
542
+ if (this.debug)
543
+ console.warn('No canvas context available to paint');
544
+ return;
545
+ }
546
+ this.context.drawImage(currFrame, 0, 0, currFrame.width, currFrame.height);
547
+ this.updateDebugInfo();
548
+ }
549
+ /**
550
+ * Transitions the video or the canvas to the proper frame.
551
+ *
552
+ * @param options - Configuration options for adjusting the video playback.
553
+ * - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time.
554
+ * - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8.
555
+ * - easing: (progress: number) => number - A function that defines the easing curve for the transition. It takes the progress ratio (a number between 0 and 1) as an argument and returns the eased value, affecting the playback speed during the transition.
556
+ */
557
+ transitionToTargetTime({ jump, transitionSpeed = this.transitionSpeed, easing = null, }) {
558
+ if (!this.video) {
559
+ console.warn('No video found');
560
+ return;
561
+ }
562
+ if (this.debug) {
563
+ console.info('Transitioning targetTime:', this.targetTime, 'currentTime:', this.currentTime);
564
+ }
565
+ const diff = this.targetTime - this.currentTime;
566
+ const distance = Math.abs(diff);
567
+ const duration = distance * 1000;
568
+ const isForwardTransition = diff > 0;
569
+ const tick = ({ startCurrentTime, startTimestamp, timestamp, }) => {
570
+ if (!this.video) {
571
+ console.warn('No video found during transition tick');
572
+ return;
573
+ }
574
+ const progress = (timestamp - startTimestamp) / duration;
575
+ // if frameThreshold is too low to catch condition Math.abs(this.targetTime - this.currentTime) < this.frameThreshold
576
+ const hasPassedThreshold = isForwardTransition ?
577
+ this.currentTime >= this.targetTime
578
+ : this.currentTime <= this.targetTime;
579
+ if (this.componentState.isAutoPlaying) {
580
+ this.componentState.autoplayProgress = parseFloat((this.currentTime / this.totalTime).toFixed(4));
581
+ }
582
+ // If we are already close enough to our target, pause the video and return.
583
+ // This is the base case of the recursive function
584
+ if (isNaN(this.targetTime) ||
585
+ // If the currentTime is already close enough to the targetTime
586
+ Math.abs(this.targetTime - this.currentTime) < this.frameThreshold ||
587
+ hasPassedThreshold) {
588
+ this.video?.pause();
589
+ if (this.transitioningRaf) {
590
+ cancelAnimationFrame(this.transitioningRaf);
591
+ this.transitioningRaf = null;
592
+ }
593
+ return;
594
+ }
595
+ // Make sure we don't go out of time bounds
596
+ if (this.targetTime > this.video.duration)
597
+ this.targetTime = this.video.duration;
598
+ if (this.targetTime < 0)
599
+ this.targetTime = 0;
600
+ // How far forward we need to transition
601
+ const transitionForward = this.targetTime - this.currentTime;
602
+ const easedProgress = easing && Number.isFinite(progress) ? easing(progress) : 0;
603
+ const easedCurrentTime = isForwardTransition ?
604
+ startCurrentTime +
605
+ easedProgress * Math.abs(distance) * transitionSpeed
606
+ : startCurrentTime -
607
+ easedProgress * Math.abs(distance) * transitionSpeed;
608
+ if (this.canvas) {
609
+ if (jump) {
610
+ // If jump, we go directly to the frame
611
+ this.currentTime = this.targetTime;
612
+ }
613
+ else if (easedProgress) {
614
+ this.currentTime = easedCurrentTime;
615
+ }
616
+ else {
617
+ this.currentTime += transitionForward / (256 / transitionSpeed);
618
+ }
619
+ this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
620
+ }
621
+ else if (jump || this.isSafari || !isForwardTransition) {
622
+ // We can't use a negative playbackRate, so if the video needs to go backwards,
623
+ // We have to use the inefficient method of modifying currentTime rapidly to
624
+ // get an effect.
625
+ this.video.pause();
626
+ if (easedProgress) {
627
+ this.currentTime = easedCurrentTime;
628
+ }
629
+ else {
630
+ this.currentTime += transitionForward / (64 / transitionSpeed);
631
+ }
632
+ // If jump, we go directly to the frame
633
+ if (jump) {
634
+ this.currentTime = this.targetTime;
635
+ }
636
+ this.video.currentTime = this.currentTime;
637
+ }
638
+ else {
639
+ // Otherwise, we play the video and adjust the playbackRate to get a smoother
640
+ // animation effect.
641
+ const playbackRate = Math.max(Math.min(transitionForward * 4, transitionSpeed, 16), 1);
642
+ if (this.debug)
643
+ console.info('ScrollerVideo playbackRate:', playbackRate);
644
+ if (!isNaN(playbackRate)) {
645
+ this.video.playbackRate = playbackRate;
646
+ this.video.play();
647
+ }
648
+ // Set the currentTime to the video's currentTime
649
+ this.currentTime = this.video.currentTime;
650
+ }
651
+ // Recursively calls ourselves until the animation is done.
652
+ if (typeof requestAnimationFrame === 'function') {
653
+ this.transitioningRaf = requestAnimationFrame((currentTimestamp) => tick({
654
+ startCurrentTime,
655
+ startTimestamp,
656
+ timestamp: currentTimestamp,
657
+ }));
658
+ }
659
+ };
660
+ if (typeof requestAnimationFrame === 'function') {
661
+ this.transitioningRaf = requestAnimationFrame((startTimestamp) => {
662
+ tick({
663
+ startCurrentTime: this.currentTime,
664
+ startTimestamp,
665
+ timestamp: startTimestamp,
666
+ });
667
+ });
668
+ }
669
+ }
670
+ /**
671
+ * Sets the currentTime of the video as a specified percentage of its total duration.
672
+ *
673
+ * @param percentage - The percentage of the video duration to set as the current time.
674
+ * @param options - Configuration options for adjusting the video playback.
675
+ * - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time.
676
+ * - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8.
677
+ * - easing: (progress: number) => number - A function that defines the easing curve for the transition. It takes the progress ratio (a number between 0 and 1) as an argument and returns the eased value, affecting the playback speed during the transition.
678
+ */
679
+ setTargetTimePercent(percentage, options = { jump: false, transitionSpeed: 8 }) {
680
+ const targetDuration = this.frames?.length && this.frameRate ?
681
+ this.frames.length / this.frameRate
682
+ : this.video?.duration || 0;
683
+ // The time we want to transition to
684
+ this.targetTime = Math.max(Math.min(percentage, 1), 0) * targetDuration;
685
+ // If we are close enough, return early
686
+ if (!options.jump &&
687
+ Math.abs(this.currentTime - this.targetTime) < this.frameThreshold)
688
+ return;
689
+ // Play the video if we are in video mode
690
+ if (!this.canvas && !this.video?.paused)
691
+ this.video?.play();
692
+ this.transitionToTargetTime(options);
693
+ }
694
+ /**
695
+ * Simulate trackScroll programmatically (scrolls on page by percentage of video).
696
+ * @param {number} percentage - The percentage of the video to scroll to.
697
+ */
698
+ setScrollPercent(percentage) {
699
+ if (!this.trackScroll) {
700
+ console.warn('`setScrollPercent` requires enabled `trackScroll`');
701
+ return;
702
+ }
703
+ const parent = this.container?.parentNode;
704
+ let top = 0, height = 0;
705
+ if (parent && parent instanceof Element) {
706
+ const rect = parent.getBoundingClientRect();
707
+ top = rect.top;
708
+ height = rect.height;
709
+ }
710
+ const startPoint = top + window.pageYOffset;
711
+ const containerHeightInViewport = height - window.innerHeight;
712
+ const targetPosition = startPoint + containerHeightInViewport * percentage;
713
+ if (isScrollPositionAtTarget(targetPosition)) {
714
+ this.targetScrollPosition = null;
715
+ }
716
+ else {
717
+ window.scrollTo({ top: targetPosition, behavior: 'smooth' });
718
+ this.targetScrollPosition = targetPosition;
719
+ }
720
+ }
721
+ /**
722
+ * Call to destroy this ScrollerVideo object.
723
+ */
724
+ destroy() {
725
+ if (this.debug)
726
+ console.info('Destroying ScrollerVideo');
727
+ if (this.trackScroll && this.updateScrollPercentage)
728
+ window.removeEventListener('scroll', () => this.updateScrollPercentage);
729
+ if (this.resize) {
730
+ window.removeEventListener('resize', this.resize);
731
+ }
732
+ // Clear component
733
+ if (this.container)
734
+ this.container.innerHTML = '';
735
+ }
736
+ /**
737
+ * Autoplay the video by scrolling to the end.
738
+ */
739
+ autoplayScroll() {
740
+ this.setVideoPercentage(1, {
741
+ jump: false,
742
+ transitionSpeed: this.totalTime * 0.1,
743
+ easing: (i) => i,
744
+ autoplay: true,
745
+ });
746
+ this.componentState.isAutoPlaying = true;
747
+ }
748
+ /**
749
+ * Updates debug information in the component state.
750
+ */
751
+ updateDebugInfo() {
752
+ this.componentState.generalData.src = this.src;
753
+ this.componentState.generalData.videoPercentage = constrain(parseFloat(this.videoPercentage.toFixed(4)), 0, 1);
754
+ this.componentState.generalData.frameRate = parseFloat(this.frameRate.toFixed(2));
755
+ this.componentState.generalData.currentTime = parseFloat(this.currentTime.toFixed(4));
756
+ this.componentState.generalData.totalTime = parseFloat(this.totalTime.toFixed(4));
757
+ this.componentState.usingWebCodecs = this.usingWebCodecs;
758
+ this.componentState.framesData.currentFrame = this.currentFrame;
759
+ this.componentState.framesData.totalFrames = this.frames?.length || 0;
760
+ }
761
+ }
762
+ export default ScrollerVideo;