@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.
- package/dist/components/@types/global.d.ts +45 -0
- package/dist/components/Scroller/Scroller.mdx +2 -2
- package/dist/components/ScrollerBase/ScrollerBase.mdx +0 -5
- package/dist/components/ScrollerVideo/Debug.svelte +207 -0
- package/dist/components/ScrollerVideo/Debug.svelte.d.ts +5 -0
- package/dist/components/ScrollerVideo/ScrollerVideo.mdx +462 -0
- package/dist/components/ScrollerVideo/ScrollerVideo.stories.svelte +190 -0
- package/dist/components/ScrollerVideo/ScrollerVideo.stories.svelte.d.ts +19 -0
- package/dist/components/ScrollerVideo/ScrollerVideo.svelte +292 -0
- package/dist/components/ScrollerVideo/ScrollerVideo.svelte.d.ts +58 -0
- package/dist/components/ScrollerVideo/ScrollerVideoForeground.svelte +164 -0
- package/dist/components/ScrollerVideo/ScrollerVideoForeground.svelte.d.ts +17 -0
- package/dist/components/ScrollerVideo/demo/AdvancedUsecases.svelte +114 -0
- package/dist/components/ScrollerVideo/demo/AdvancedUsecases.svelte.d.ts +3 -0
- package/dist/components/ScrollerVideo/demo/Embedded.svelte +94 -0
- package/dist/components/ScrollerVideo/demo/Embedded.svelte.d.ts +3 -0
- package/dist/components/ScrollerVideo/demo/WithAi2svelteForegrounds.svelte +117 -0
- package/dist/components/ScrollerVideo/demo/WithAi2svelteForegrounds.svelte.d.ts +3 -0
- package/dist/components/ScrollerVideo/demo/WithScrollerBase.svelte +80 -0
- package/dist/components/ScrollerVideo/demo/WithScrollerBase.svelte.d.ts +3 -0
- package/dist/components/ScrollerVideo/demo/WithTextForegrounds.svelte +72 -0
- package/dist/components/ScrollerVideo/demo/WithTextForegrounds.svelte.d.ts +18 -0
- package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/ai-chart.svelte +631 -0
- package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/ai-chart.svelte.d.ts +3 -0
- package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation1.svelte +428 -0
- package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation1.svelte.d.ts +26 -0
- package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation2.svelte +402 -0
- package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation2.svelte.d.ts +26 -0
- package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation3.svelte +398 -0
- package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation3.svelte.d.ts +26 -0
- package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation4.svelte +360 -0
- package/dist/components/ScrollerVideo/demo/graphic/ai2svelte/annotation4.svelte.d.ts +26 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/ai-chart-md.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/ai-chart-sm.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/ai-chart-xs.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation1-lg.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation1-md.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation1-sm.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation1-xl.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation1-xs.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation2-lg.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation2-md.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation2-sm.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation2-xl.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation2-xs.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation3-lg.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation3-md.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation3-sm.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation3-xl.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation3-xs.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation4-lg.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation4-md.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation4-sm.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation4-xl.png +0 -0
- package/dist/components/ScrollerVideo/demo/graphic/imgs/annotation4-xs.png +0 -0
- package/dist/components/ScrollerVideo/ts/ScrollerVideo.d.ts +248 -0
- package/dist/components/ScrollerVideo/ts/ScrollerVideo.js +762 -0
- package/dist/components/ScrollerVideo/ts/mp4box.d.ts +137 -0
- package/dist/components/ScrollerVideo/ts/state.svelte.d.ts +51 -0
- package/dist/components/ScrollerVideo/ts/state.svelte.js +25 -0
- package/dist/components/ScrollerVideo/ts/utils.d.ts +70 -0
- package/dist/components/ScrollerVideo/ts/utils.js +92 -0
- package/dist/components/ScrollerVideo/ts/videoDecoder.d.ts +11 -0
- package/dist/components/ScrollerVideo/ts/videoDecoder.js +193 -0
- package/dist/components/ScrollerVideo/videos/HPO.mp4 +0 -0
- package/dist/components/ScrollerVideo/videos/drone.mp4 +0 -0
- package/dist/components/ScrollerVideo/videos/goldengate.mp4 +0 -0
- package/dist/components/ScrollerVideo/videos/tennis.mp4 +0 -0
- package/dist/components/ScrollerVideo/videos/waves_lg.mp4 +0 -0
- package/dist/components/ScrollerVideo/videos/waves_md.mp4 +0 -0
- package/dist/components/ScrollerVideo/videos/waves_sm.mp4 +0 -0
- package/dist/components/SiteHeadline/SiteHeadline.mdx +4 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- 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;
|