js to ts
This commit is contained in:
parent
f8421b675e
commit
a2502283f8
9 changed files with 524 additions and 353 deletions
|
|
@ -174,7 +174,7 @@
|
||||||
<ScrollyVideo
|
<ScrollyVideo
|
||||||
{...args}
|
{...args}
|
||||||
src={videoSrc.Goldengate}
|
src={videoSrc.Goldengate}
|
||||||
useWebCodecs={true}
|
useWebCodecs={false}
|
||||||
autoplay={true}
|
autoplay={true}
|
||||||
></ScrollyVideo>
|
></ScrollyVideo>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
||||||
140
src/components/ScrollyVideo/js/ScrollyVideo.d.ts
vendored
140
src/components/ScrollyVideo/js/ScrollyVideo.d.ts
vendored
|
|
@ -1,140 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
|
||||||
|
|
||||||
export default ScrollyVideo;
|
|
||||||
/**
|
|
||||||
* ____ _ _ __ ___ _
|
|
||||||
* / ___| ___ _ __ ___ | | |_ \ \ / (_) __| | ___ ___
|
|
||||||
* \___ \ / __| '__/ _ \| | | | | \ \ / /| |/ _` |/ _ \/ _ \
|
|
||||||
* ___) | (__| | | (_) | | | |_| |\ V / | | (_| | __/ (_) |
|
|
||||||
* |____/ \___|_| \___/|_|_|\__, | \_/ |_|\__,_|\___|\___/
|
|
||||||
* |___/
|
|
||||||
*
|
|
||||||
* Responsive scrollable videos without obscure video encoding requirements.
|
|
||||||
* Compatible with React, Svelte, Vue, and plain HTML.
|
|
||||||
*/
|
|
||||||
declare class ScrollyVideo {
|
|
||||||
constructor({
|
|
||||||
src,
|
|
||||||
scrollyVideoContainer,
|
|
||||||
objectFit,
|
|
||||||
sticky,
|
|
||||||
full,
|
|
||||||
trackScroll,
|
|
||||||
lockScroll,
|
|
||||||
transitionSpeed,
|
|
||||||
frameThreshold,
|
|
||||||
useWebCodecs,
|
|
||||||
onReady,
|
|
||||||
onChange,
|
|
||||||
debug,
|
|
||||||
autoplay,
|
|
||||||
}: {
|
|
||||||
src?: string;
|
|
||||||
scrollyVideoContainer: string | HTMLDivElement | undefined;
|
|
||||||
objectFit?: string;
|
|
||||||
sticky?: boolean;
|
|
||||||
full?: boolean;
|
|
||||||
trackScroll?: boolean;
|
|
||||||
lockScroll?: boolean;
|
|
||||||
transitionSpeed?: number;
|
|
||||||
frameThreshold?: number;
|
|
||||||
useWebCodecs?: boolean;
|
|
||||||
onReady?: () => void;
|
|
||||||
onChange?: () => void;
|
|
||||||
debug?: boolean;
|
|
||||||
autoplay?: boolean;
|
|
||||||
});
|
|
||||||
container: Element;
|
|
||||||
src: string;
|
|
||||||
transitionSpeed: number;
|
|
||||||
frameThreshold: number;
|
|
||||||
useWebCodecs: boolean;
|
|
||||||
objectFit: string;
|
|
||||||
sticky: boolean;
|
|
||||||
trackScroll: boolean;
|
|
||||||
onReady: () => void;
|
|
||||||
onChange: () => void;
|
|
||||||
debug: boolean;
|
|
||||||
autoplay: boolean;
|
|
||||||
video: HTMLVideoElement;
|
|
||||||
videoPercentage: number;
|
|
||||||
isSafari: boolean;
|
|
||||||
currentTime: number;
|
|
||||||
targetTime: number;
|
|
||||||
canvas: HTMLCanvasElement;
|
|
||||||
context: CanvasRenderingContext2D;
|
|
||||||
frames: ImageBitmap[] | null;
|
|
||||||
frameRate: number;
|
|
||||||
currentFrameNumber: number;
|
|
||||||
updateScrollPercentage: (jump: boolean) => void;
|
|
||||||
targetScrollPosition: number | null;
|
|
||||||
resize: () => void;
|
|
||||||
/**
|
|
||||||
* Sets the currentTime of the video as a specified percentage of its total duration.
|
|
||||||
*
|
|
||||||
* @param percentage - The percentage of the video duration to set as the current time.
|
|
||||||
* @param options - Configuration options for adjusting the video playback.
|
|
||||||
* - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time.
|
|
||||||
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8.
|
|
||||||
* - 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.
|
|
||||||
*/
|
|
||||||
setVideoPercentage(percentage: number, options?: {}): void;
|
|
||||||
/**
|
|
||||||
* Sets the style of the video or canvas to "cover" it's container
|
|
||||||
*
|
|
||||||
* @param el
|
|
||||||
*/
|
|
||||||
setCoverStyle(el: string): void;
|
|
||||||
/**
|
|
||||||
* Uses webCodecs to decode the video into frames
|
|
||||||
*/
|
|
||||||
decodeVideo(): Promise<void>;
|
|
||||||
/**
|
|
||||||
* Paints the frame of to the canvas
|
|
||||||
*
|
|
||||||
* @param frameNum
|
|
||||||
*/
|
|
||||||
paintCanvasFrame(frameNum: number): void;
|
|
||||||
/**
|
|
||||||
* Transitions the video or the canvas to the proper frame.
|
|
||||||
*
|
|
||||||
* @param options - Configuration options for adjusting the video playback.
|
|
||||||
* - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time.
|
|
||||||
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8.
|
|
||||||
* - 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.
|
|
||||||
*/
|
|
||||||
transitionToTargetTime({
|
|
||||||
jump,
|
|
||||||
transitionSpeed,
|
|
||||||
easing,
|
|
||||||
}: {
|
|
||||||
jump: boolean;
|
|
||||||
transitionSpeed?: number;
|
|
||||||
easing?: (progress: number) => number;
|
|
||||||
}): void;
|
|
||||||
transitioningRaf: number;
|
|
||||||
/**
|
|
||||||
* Sets the currentTime of the video as a specified percentage of its total duration.
|
|
||||||
*
|
|
||||||
* @param percentage - The percentage of the video duration to set as the current time.
|
|
||||||
* @param options - Configuration options for adjusting the video playback.
|
|
||||||
* - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time.
|
|
||||||
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8.
|
|
||||||
* - 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.
|
|
||||||
*/
|
|
||||||
setTargetTimePercent(percentage: number, options?: {}): void;
|
|
||||||
/**
|
|
||||||
* Simulate trackScroll programmatically (scrolls on page by percentage of video)
|
|
||||||
*
|
|
||||||
* @param percentage
|
|
||||||
*/
|
|
||||||
setScrollPercent(percentage: number): void;
|
|
||||||
/**
|
|
||||||
* Call to destroy this ScrollyVideo object
|
|
||||||
*/
|
|
||||||
destroy(): void;
|
|
||||||
/**
|
|
||||||
* Call to initiate autoplay
|
|
||||||
*/
|
|
||||||
autoplayScroll(): void;
|
|
||||||
}
|
|
||||||
|
|
@ -3,23 +3,111 @@ import videoDecoder from './videoDecoder';
|
||||||
import { debounce, isScrollPositionAtTarget, map } from './utils';
|
import { debounce, isScrollPositionAtTarget, map } from './utils';
|
||||||
import { scrollyVideoState } from './state.svelte';
|
import { scrollyVideoState } from './state.svelte';
|
||||||
|
|
||||||
|
interface ScrollyVideoArgs {
|
||||||
|
src?: string;
|
||||||
|
scrollyVideoContainer: HTMLElement | string;
|
||||||
|
objectFit?: string;
|
||||||
|
sticky?: boolean;
|
||||||
|
full?: boolean;
|
||||||
|
trackScroll?: boolean;
|
||||||
|
lockScroll?: boolean;
|
||||||
|
transitionSpeed?: number;
|
||||||
|
frameThreshold?: number;
|
||||||
|
useWebCodecs?: boolean;
|
||||||
|
onReady?: () => void;
|
||||||
|
onChange?: (percentage?: number) => void;
|
||||||
|
debug?: boolean;
|
||||||
|
autoplay?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransitionOptions {
|
||||||
|
jump: boolean;
|
||||||
|
transitionSpeed?: number;
|
||||||
|
easing?: ((progress: number) => number) | null;
|
||||||
|
autoplay?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
class ScrollyVideo {
|
class ScrollyVideo {
|
||||||
|
container: HTMLElement | null;
|
||||||
|
scrollyVideoContainer: Element | string | undefined;
|
||||||
|
src: string;
|
||||||
|
transitionSpeed: number;
|
||||||
|
frameThreshold: number;
|
||||||
|
useWebCodecs: boolean;
|
||||||
|
objectFit: string;
|
||||||
|
sticky: boolean;
|
||||||
|
trackScroll: boolean;
|
||||||
|
onReady: () => void;
|
||||||
|
onChange: (percentage?: number) => void;
|
||||||
|
debug: boolean;
|
||||||
|
autoplay: boolean;
|
||||||
|
video: HTMLVideoElement | undefined;
|
||||||
|
videoPercentage: number;
|
||||||
|
isSafari: boolean;
|
||||||
|
currentTime: number;
|
||||||
|
targetTime: number;
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
context: CanvasRenderingContext2D | null;
|
||||||
|
frames: ImageBitmap[] | null;
|
||||||
|
frameRate: number;
|
||||||
|
targetScrollPosition: number | null = null;
|
||||||
|
currentFrame: number;
|
||||||
|
usingWebCodecs: boolean; // Whether we are using webCodecs
|
||||||
|
totalTime: number; // The total time of the video, used for calculating percentage
|
||||||
|
transitioningRaf: number | null;
|
||||||
|
|
||||||
|
updateScrollPercentage: ((jump: boolean) => void) | undefined;
|
||||||
|
resize: (() => void) | undefined;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
src = 'https://scrollyvideo.js.org/goldengate.mp4', // The src of the video, required
|
src = 'https://scrollyvideo.js.org/goldengate.mp4',
|
||||||
scrollyVideoContainer, // The dom element or id that this object will be created in, required
|
scrollyVideoContainer,
|
||||||
objectFit = 'cover', // Whether the video should "cover" inside the container
|
objectFit = 'cover',
|
||||||
sticky = true, // Whether the video should "stick" to the top of the container
|
sticky = true,
|
||||||
full = true, // Whether the container should expand to 100vh and 100vw
|
full = true,
|
||||||
trackScroll = true, // Whether this object should automatically respond to scroll
|
trackScroll = true,
|
||||||
lockScroll = true, // Whether it ignores human scroll while it runs `setVideoPercentage` with enabled `trackScroll`
|
lockScroll = true,
|
||||||
transitionSpeed = 8, // How fast the video transitions between points
|
transitionSpeed = 8,
|
||||||
frameThreshold = 0.1, // When to stop the video animation, in seconds
|
frameThreshold = 0.1,
|
||||||
useWebCodecs = true, // Whether to try using the webcodecs approach
|
useWebCodecs = true,
|
||||||
onReady = () => {}, // A callback that invokes on video decode
|
onReady = () => {},
|
||||||
onChange = () => {}, // A callback that invokes on video percentage change
|
onChange = (_percentage?: number) => {},
|
||||||
debug = false, // Whether to print debug stats to the console
|
debug = false,
|
||||||
autoplay = false, // Whether to autoplay the video when it is ready
|
autoplay = false,
|
||||||
}) {
|
}: ScrollyVideoArgs) {
|
||||||
|
this.src = src;
|
||||||
|
this.scrollyVideoContainer = scrollyVideoContainer;
|
||||||
|
this.objectFit = objectFit;
|
||||||
|
this.sticky = sticky;
|
||||||
|
this.trackScroll = trackScroll;
|
||||||
|
this.transitionSpeed = transitionSpeed;
|
||||||
|
this.frameThreshold = frameThreshold;
|
||||||
|
this.useWebCodecs = useWebCodecs;
|
||||||
|
this.onReady = onReady;
|
||||||
|
this.onChange = onChange;
|
||||||
|
this.debug = debug;
|
||||||
|
this.autoplay = autoplay;
|
||||||
|
this.videoPercentage = 0;
|
||||||
|
this.isSafari = false;
|
||||||
|
this.currentTime = 0;
|
||||||
|
this.targetTime = 0;
|
||||||
|
this.canvas = null;
|
||||||
|
this.context = null;
|
||||||
|
this.container = null;
|
||||||
|
this.frames = null;
|
||||||
|
this.frameRate = 0;
|
||||||
|
this.currentTime = 0; // Saves the currentTime of the video, synced with this.video.currentTime
|
||||||
|
this.targetTime = 0; // The target time before a transition happens
|
||||||
|
this.canvas = null; // The canvas for drawing the frames decoded by webCodecs
|
||||||
|
this.context = null; // The canvas context
|
||||||
|
this.frames = []; // The frames decoded by webCodecs
|
||||||
|
this.frameRate = 0; // Calculation of frameRate so we know which frame to paint
|
||||||
|
this.currentFrame = 0;
|
||||||
|
this.videoPercentage = 0;
|
||||||
|
this.usingWebCodecs = false; // Whether we are using webCodecs
|
||||||
|
this.totalTime = 0; // The total time of the video, used for calculating percentage
|
||||||
|
this.transitioningRaf = null;
|
||||||
|
|
||||||
// Make sure that we have a DOM
|
// Make sure that we have a DOM
|
||||||
if (typeof document !== 'object') {
|
if (typeof document !== 'object') {
|
||||||
console.error('ScrollyVideo must be initiated in a DOM context');
|
console.error('ScrollyVideo must be initiated in a DOM context');
|
||||||
|
|
@ -38,30 +126,17 @@ class ScrollyVideo {
|
||||||
|
|
||||||
// Save the container. If the container is a string we get the element
|
// Save the container. If the container is a string we get the element
|
||||||
|
|
||||||
if (scrollyVideoContainer instanceof Element)
|
if (scrollyVideoContainer && scrollyVideoContainer instanceof HTMLElement)
|
||||||
this.container = scrollyVideoContainer;
|
this.container = scrollyVideoContainer;
|
||||||
// otherwise it should better be an element
|
// otherwise it should better be an element
|
||||||
else if (typeof scrollyVideoContainer === 'string') {
|
else if (typeof scrollyVideoContainer === 'string') {
|
||||||
this.container = document.getElementById(scrollyVideoContainer);
|
this.container = document.getElementById(scrollyVideoContainer) || null;
|
||||||
if (!this.container)
|
if (!this.container)
|
||||||
throw new Error('scrollyVideoContainer must be a valid DOM object');
|
throw new Error('scrollyVideoContainer must be a valid DOM object');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('scrollyVideoContainer must be a valid DOM object');
|
throw new Error('scrollyVideoContainer must be a valid DOM object');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the constructor options
|
|
||||||
this.src = src;
|
|
||||||
this.transitionSpeed = transitionSpeed;
|
|
||||||
this.frameThreshold = frameThreshold;
|
|
||||||
this.useWebCodecs = useWebCodecs;
|
|
||||||
this.objectFit = objectFit;
|
|
||||||
this.sticky = sticky;
|
|
||||||
this.trackScroll = trackScroll;
|
|
||||||
this.onReady = onReady;
|
|
||||||
this.onChange = onChange;
|
|
||||||
this.debug = debug;
|
|
||||||
this.autoplay = autoplay;
|
|
||||||
|
|
||||||
// Create the initial video object. Even if we are going to use webcodecs,
|
// Create the initial video object. Even if we are going to use webcodecs,
|
||||||
// we start with a paused video object
|
// we start with a paused video object
|
||||||
|
|
||||||
|
|
@ -69,7 +144,7 @@ class ScrollyVideo {
|
||||||
this.video.src = src;
|
this.video.src = src;
|
||||||
this.video.preload = 'auto';
|
this.video.preload = 'auto';
|
||||||
this.video.tabIndex = 0;
|
this.video.tabIndex = 0;
|
||||||
this.video.autobuffer = true;
|
this.video.preload = 'auto';
|
||||||
this.video.playsInline = true;
|
this.video.playsInline = true;
|
||||||
this.video.muted = true;
|
this.video.muted = true;
|
||||||
this.video.pause();
|
this.video.pause();
|
||||||
|
|
@ -115,18 +190,6 @@ class ScrollyVideo {
|
||||||
this.isSafari = browserEngine.name === 'WebKit';
|
this.isSafari = browserEngine.name === 'WebKit';
|
||||||
if (debug && this.isSafari) console.info('Safari browser detected');
|
if (debug && this.isSafari) console.info('Safari browser detected');
|
||||||
|
|
||||||
// Initialize state variables
|
|
||||||
this.currentTime = 0; // Saves the currentTime of the video, synced with this.video.currentTime
|
|
||||||
this.targetTime = 0; // The target time before a transition happens
|
|
||||||
this.canvas = null; // The canvas for drawing the frames decoded by webCodecs
|
|
||||||
this.context = null; // The canvas context
|
|
||||||
this.frames = []; // The frames decoded by webCodecs
|
|
||||||
this.frameRate = 0; // Calculation of frameRate so we know which frame to paint
|
|
||||||
this.currentFrame = 0;
|
|
||||||
this.videoPercentage = 0;
|
|
||||||
this.usingWebCodecs = false; // Whether we are using webCodecs
|
|
||||||
this.totalTime = 0; // The total time of the video, used for calculating percentage
|
|
||||||
|
|
||||||
const debouncedScroll = debounce(() => {
|
const debouncedScroll = debounce(() => {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
this.setScrollPercent(this.videoPercentage);
|
this.setScrollPercent(this.videoPercentage);
|
||||||
|
|
@ -134,10 +197,25 @@ class ScrollyVideo {
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// Add scroll listener for responding to scroll position
|
// Add scroll listener for responding to scroll position
|
||||||
this.updateScrollPercentage = (jump) => {
|
this.updateScrollPercentage = (jump = false) => {
|
||||||
// Used for internally setting the scroll percentage based on built-in listeners
|
// Used for internally setting the scroll percentage based on built-in listeners
|
||||||
const containerBoundingClientRect =
|
let containerBoundingClientRect;
|
||||||
this.container.parentNode.getBoundingClientRect();
|
if (
|
||||||
|
this.container &&
|
||||||
|
this.container.parentNode &&
|
||||||
|
(this.container.parentNode as Element).getBoundingClientRect
|
||||||
|
) {
|
||||||
|
containerBoundingClientRect = (
|
||||||
|
this.container.parentNode as Element
|
||||||
|
).getBoundingClientRect();
|
||||||
|
} else {
|
||||||
|
if (this.debug) {
|
||||||
|
console.error(
|
||||||
|
'ScrollyVideo: container or parentNode is null or invalid.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the current scroll percent of the video
|
// Calculate the current scroll percent of the video
|
||||||
let scrollPercent =
|
let scrollPercent =
|
||||||
|
|
@ -160,7 +238,7 @@ class ScrollyVideo {
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggle autoplaying state on manual intervention
|
// toggle autoplaying state on manual intervention
|
||||||
if (scrollyVideoState.isAutoPlaying) {
|
if (scrollyVideoState.isAutoPlaying && this.frames) {
|
||||||
if (this.debug) console.warn('Stopping autoplay due to manual scroll');
|
if (this.debug) console.warn('Stopping autoplay due to manual scroll');
|
||||||
|
|
||||||
if (this.usingWebCodecs) {
|
if (this.usingWebCodecs) {
|
||||||
|
|
@ -192,14 +270,22 @@ class ScrollyVideo {
|
||||||
|
|
||||||
// Add our event listeners for handling changes to the window or scroll
|
// Add our event listeners for handling changes to the window or scroll
|
||||||
if (this.trackScroll) {
|
if (this.trackScroll) {
|
||||||
window.addEventListener('scroll', this.updateScrollPercentage);
|
window.addEventListener('scroll', () => {
|
||||||
|
if (this.updateScrollPercentage) {
|
||||||
|
this.updateScrollPercentage(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Set the initial scroll percentage
|
// Set the initial scroll percentage
|
||||||
this.video.addEventListener(
|
this.video.addEventListener(
|
||||||
'loadedmetadata',
|
'loadedmetadata',
|
||||||
() => {
|
() => {
|
||||||
this.updateScrollPercentage(true);
|
if (this.updateScrollPercentage) {
|
||||||
this.totalTime = this.video.duration;
|
this.updateScrollPercentage(true);
|
||||||
|
}
|
||||||
|
if (this.video) {
|
||||||
|
this.totalTime = this.video.duration;
|
||||||
|
}
|
||||||
this.setCoverStyle(this.canvas || this.video);
|
this.setCoverStyle(this.canvas || this.video);
|
||||||
},
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
|
|
@ -209,7 +295,9 @@ class ScrollyVideo {
|
||||||
'loadedmetadata',
|
'loadedmetadata',
|
||||||
() => {
|
() => {
|
||||||
this.setTargetTimePercent(0, { jump: true });
|
this.setTargetTimePercent(0, { jump: true });
|
||||||
this.totalTime = this.video.duration;
|
if (this.video) {
|
||||||
|
this.totalTime = this.video.duration;
|
||||||
|
}
|
||||||
this.setCoverStyle(this.canvas || this.video);
|
this.setCoverStyle(this.canvas || this.video);
|
||||||
},
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
|
|
@ -243,7 +331,7 @@ class ScrollyVideo {
|
||||||
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8.
|
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8.
|
||||||
* - 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.
|
* - 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.
|
||||||
*/
|
*/
|
||||||
setVideoPercentage(percentage, options = { autoplay: false }) {
|
setVideoPercentage(percentage: number, options: TransitionOptions): void {
|
||||||
// Early termination if the video percentage is already at the percentage that is intended.
|
// Early termination if the video percentage is already at the percentage that is intended.
|
||||||
if (this.videoPercentage === percentage) return;
|
if (this.videoPercentage === percentage) return;
|
||||||
|
|
||||||
|
|
@ -267,7 +355,12 @@ class ScrollyVideo {
|
||||||
*
|
*
|
||||||
* @param el
|
* @param el
|
||||||
*/
|
*/
|
||||||
setCoverStyle(el) {
|
setCoverStyle(el: HTMLElement | HTMLCanvasElement | undefined): void {
|
||||||
|
if (!el) {
|
||||||
|
if (this.debug) console.warn('No element to set cover style on');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.objectFit) {
|
if (this.objectFit) {
|
||||||
el.style.position = 'absolute';
|
el.style.position = 'absolute';
|
||||||
el.style.top = '50%';
|
el.style.top = '50%';
|
||||||
|
|
@ -278,11 +371,18 @@ class ScrollyVideo {
|
||||||
|
|
||||||
// Gets the width and height of the container
|
// Gets the width and height of the container
|
||||||
const { width: containerWidth, height: containerHeight } =
|
const { width: containerWidth, height: containerHeight } =
|
||||||
this.container.getBoundingClientRect();
|
this.container?.getBoundingClientRect() || { width: 0, height: 0 };
|
||||||
|
|
||||||
// Gets the width and height of the video frames
|
let width = 0,
|
||||||
const width = el.videoWidth || el.width;
|
height = 0;
|
||||||
const height = el.videoHeight || el.height;
|
|
||||||
|
if (el instanceof HTMLVideoElement) {
|
||||||
|
width = el.videoWidth;
|
||||||
|
height = el.videoHeight;
|
||||||
|
} else if (el instanceof HTMLCanvasElement) {
|
||||||
|
width = el.width;
|
||||||
|
height = el.height;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.debug)
|
if (this.debug)
|
||||||
console.info('Container dimensions:', [
|
console.info('Container dimensions:', [
|
||||||
|
|
@ -334,7 +434,7 @@ class ScrollyVideo {
|
||||||
await videoDecoder(
|
await videoDecoder(
|
||||||
this.src,
|
this.src,
|
||||||
(frame) => {
|
(frame) => {
|
||||||
this.frames.push(frame);
|
this.frames?.push(frame);
|
||||||
},
|
},
|
||||||
this.debug
|
this.debug
|
||||||
).then(() => {
|
).then(() => {
|
||||||
|
|
@ -348,11 +448,11 @@ class ScrollyVideo {
|
||||||
this.frames = [];
|
this.frames = [];
|
||||||
|
|
||||||
// Force a video reload when videoDecoder fails
|
// Force a video reload when videoDecoder fails
|
||||||
this.video.load();
|
this.video?.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no frames, something went wrong
|
// If no frames, something went wrong
|
||||||
if (this.frames.length === 0) {
|
if (this.frames?.length === 0) {
|
||||||
if (this.debug) console.error('No frames were received from webCodecs');
|
if (this.debug) console.error('No frames were received from webCodecs');
|
||||||
|
|
||||||
this.onReady();
|
this.onReady();
|
||||||
|
|
@ -360,11 +460,12 @@ class ScrollyVideo {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the frameRate based on number of frames and the duration
|
// Calculate the frameRate based on number of frames and the duration
|
||||||
this.frameRate = this.frames.length / this.video.duration;
|
this.frameRate =
|
||||||
|
this.frames && this.video ? this.frames.length / this.video.duration : 0;
|
||||||
if (this.debug)
|
if (this.debug)
|
||||||
console.info(
|
console.info(
|
||||||
'Received',
|
'Received',
|
||||||
this.frames.length,
|
this.frames?.length,
|
||||||
'frames. Video frame rate:',
|
'frames. Video frame rate:',
|
||||||
this.frameRate
|
this.frameRate
|
||||||
);
|
);
|
||||||
|
|
@ -374,8 +475,12 @@ class ScrollyVideo {
|
||||||
this.context = this.canvas.getContext('2d');
|
this.context = this.canvas.getContext('2d');
|
||||||
|
|
||||||
// Hide the video and add the canvas to the container
|
// Hide the video and add the canvas to the container
|
||||||
this.video.style.display = 'none';
|
if (this.video) {
|
||||||
this.container.appendChild(this.canvas);
|
this.video.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (this.container) {
|
||||||
|
this.container.appendChild(this.canvas);
|
||||||
|
}
|
||||||
if (this.objectFit) this.setCoverStyle(this.canvas);
|
if (this.objectFit) this.setCoverStyle(this.canvas);
|
||||||
|
|
||||||
// Paint our first frame
|
// Paint our first frame
|
||||||
|
|
@ -390,7 +495,12 @@ class ScrollyVideo {
|
||||||
*
|
*
|
||||||
* @param frameNum
|
* @param frameNum
|
||||||
*/
|
*/
|
||||||
paintCanvasFrame(frameNum) {
|
paintCanvasFrame(frameNum: number): void {
|
||||||
|
if (!this.frames) {
|
||||||
|
if (this.debug) console.warn('No frames available to paint');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the frame and paint it to the canvas
|
// Get the frame and paint it to the canvas
|
||||||
const currFrame = this.frames[frameNum];
|
const currFrame = this.frames[frameNum];
|
||||||
this.currentFrame = frameNum;
|
this.currentFrame = frameNum;
|
||||||
|
|
@ -406,7 +516,10 @@ class ScrollyVideo {
|
||||||
// Make sure the canvas is scaled properly, similar to setCoverStyle
|
// Make sure the canvas is scaled properly, similar to setCoverStyle
|
||||||
this.canvas.width = currFrame.width;
|
this.canvas.width = currFrame.width;
|
||||||
this.canvas.height = currFrame.height;
|
this.canvas.height = currFrame.height;
|
||||||
const { width, height } = this.container.getBoundingClientRect();
|
const { width, height } = this.container?.getBoundingClientRect() || {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
};
|
||||||
|
|
||||||
if (this.objectFit == 'cover') {
|
if (this.objectFit == 'cover') {
|
||||||
if (width / height > currFrame.width / currFrame.height) {
|
if (width / height > currFrame.width / currFrame.height) {
|
||||||
|
|
@ -427,6 +540,11 @@ class ScrollyVideo {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the frame to the canvas context
|
// Draw the frame to the canvas context
|
||||||
|
if (!this.context) {
|
||||||
|
if (this.debug) console.warn('No canvas context available to paint');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.context.drawImage(currFrame, 0, 0, currFrame.width, currFrame.height);
|
this.context.drawImage(currFrame, 0, 0, currFrame.width, currFrame.height);
|
||||||
this.updateDebugInfo();
|
this.updateDebugInfo();
|
||||||
}
|
}
|
||||||
|
|
@ -443,7 +561,12 @@ class ScrollyVideo {
|
||||||
jump,
|
jump,
|
||||||
transitionSpeed = this.transitionSpeed,
|
transitionSpeed = this.transitionSpeed,
|
||||||
easing = null,
|
easing = null,
|
||||||
}) {
|
}: TransitionOptions) {
|
||||||
|
if (!this.video) {
|
||||||
|
console.warn('No video found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.info(
|
console.info(
|
||||||
'Transitioning targetTime:',
|
'Transitioning targetTime:',
|
||||||
|
|
@ -458,7 +581,20 @@ class ScrollyVideo {
|
||||||
const duration = distance * 1000;
|
const duration = distance * 1000;
|
||||||
const isForwardTransition = diff > 0;
|
const isForwardTransition = diff > 0;
|
||||||
|
|
||||||
const tick = ({ startCurrentTime, startTimestamp, timestamp }) => {
|
const tick = ({
|
||||||
|
startCurrentTime,
|
||||||
|
startTimestamp,
|
||||||
|
timestamp,
|
||||||
|
}: {
|
||||||
|
startCurrentTime: number;
|
||||||
|
startTimestamp: number;
|
||||||
|
timestamp: number;
|
||||||
|
}) => {
|
||||||
|
if (!this.video) {
|
||||||
|
console.warn('No video found during transition tick');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const progress = (timestamp - startTimestamp) / duration;
|
const progress = (timestamp - startTimestamp) / duration;
|
||||||
|
|
||||||
// if frameThreshold is too low to catch condition Math.abs(this.targetTime - this.currentTime) < this.frameThreshold
|
// if frameThreshold is too low to catch condition Math.abs(this.targetTime - this.currentTime) < this.frameThreshold
|
||||||
|
|
@ -479,7 +615,7 @@ class ScrollyVideo {
|
||||||
Math.abs(this.targetTime - this.currentTime) < this.frameThreshold ||
|
Math.abs(this.targetTime - this.currentTime) < this.frameThreshold ||
|
||||||
hasPassedThreshold
|
hasPassedThreshold
|
||||||
) {
|
) {
|
||||||
this.video.pause();
|
this.video?.pause();
|
||||||
|
|
||||||
if (this.transitioningRaf) {
|
if (this.transitioningRaf) {
|
||||||
cancelAnimationFrame(this.transitioningRaf);
|
cancelAnimationFrame(this.transitioningRaf);
|
||||||
|
|
@ -497,7 +633,7 @@ class ScrollyVideo {
|
||||||
// How far forward we need to transition
|
// How far forward we need to transition
|
||||||
const transitionForward = this.targetTime - this.currentTime;
|
const transitionForward = this.targetTime - this.currentTime;
|
||||||
const easedProgress =
|
const easedProgress =
|
||||||
easing && Number.isFinite(progress) ? easing(progress) : null;
|
easing && Number.isFinite(progress) ? easing(progress) : 0;
|
||||||
const easedCurrentTime =
|
const easedCurrentTime =
|
||||||
isForwardTransition ?
|
isForwardTransition ?
|
||||||
startCurrentTime +
|
startCurrentTime +
|
||||||
|
|
@ -584,11 +720,14 @@ class ScrollyVideo {
|
||||||
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8.
|
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8.
|
||||||
* - 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.
|
* - 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.
|
||||||
*/
|
*/
|
||||||
setTargetTimePercent(percentage, options = {}) {
|
setTargetTimePercent(
|
||||||
|
percentage: number,
|
||||||
|
options: TransitionOptions = { jump: false, transitionSpeed: 8 }
|
||||||
|
) {
|
||||||
const targetDuration =
|
const targetDuration =
|
||||||
this.frames.length && this.frameRate ?
|
this.frames?.length && this.frameRate ?
|
||||||
this.frames.length / this.frameRate
|
this.frames.length / this.frameRate
|
||||||
: this.video.duration;
|
: this.video?.duration || 0;
|
||||||
// The time we want to transition to
|
// The time we want to transition to
|
||||||
this.targetTime = Math.max(Math.min(percentage, 1), 0) * targetDuration;
|
this.targetTime = Math.max(Math.min(percentage, 1), 0) * targetDuration;
|
||||||
|
|
||||||
|
|
@ -600,7 +739,7 @@ class ScrollyVideo {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Play the video if we are in video mode
|
// Play the video if we are in video mode
|
||||||
if (!this.canvas && !this.video.paused) this.video.play();
|
if (!this.canvas && !this.video?.paused) this.video?.play();
|
||||||
|
|
||||||
this.transitionToTargetTime(options);
|
this.transitionToTargetTime(options);
|
||||||
}
|
}
|
||||||
|
|
@ -610,14 +749,21 @@ class ScrollyVideo {
|
||||||
*
|
*
|
||||||
* @param percentage
|
* @param percentage
|
||||||
*/
|
*/
|
||||||
setScrollPercent(percentage) {
|
setScrollPercent(percentage: number) {
|
||||||
if (!this.trackScroll) {
|
if (!this.trackScroll) {
|
||||||
console.warn('`setScrollPercent` requires enabled `trackScroll`');
|
console.warn('`setScrollPercent` requires enabled `trackScroll`');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parent = this.container.parentNode;
|
const parent = this.container?.parentNode;
|
||||||
const { top, height } = parent.getBoundingClientRect();
|
let top = 0,
|
||||||
|
height = 0;
|
||||||
|
|
||||||
|
if (parent && parent instanceof Element) {
|
||||||
|
const rect = parent.getBoundingClientRect();
|
||||||
|
top = rect.top;
|
||||||
|
height = rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
const startPoint = top + window.pageYOffset;
|
const startPoint = top + window.pageYOffset;
|
||||||
|
|
||||||
|
|
@ -638,10 +784,12 @@ class ScrollyVideo {
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.debug) console.info('Destroying ScrollyVideo');
|
if (this.debug) console.info('Destroying ScrollyVideo');
|
||||||
|
|
||||||
if (this.trackScroll)
|
if (this.trackScroll && this.updateScrollPercentage)
|
||||||
window.removeEventListener('scroll', this.updateScrollPercentage);
|
window.removeEventListener('scroll', () => this.updateScrollPercentage);
|
||||||
|
|
||||||
window.removeEventListener('resize', this.resize);
|
if (this.resize) {
|
||||||
|
window.removeEventListener('resize', this.resize);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear component
|
// Clear component
|
||||||
if (this.container) this.container.innerHTML = '';
|
if (this.container) this.container.innerHTML = '';
|
||||||
|
|
@ -659,14 +807,21 @@ class ScrollyVideo {
|
||||||
|
|
||||||
updateDebugInfo() {
|
updateDebugInfo() {
|
||||||
scrollyVideoState.generalData.src = this.src;
|
scrollyVideoState.generalData.src = this.src;
|
||||||
scrollyVideoState.generalData.videoPercentage =
|
scrollyVideoState.generalData.videoPercentage = parseFloat(
|
||||||
this.videoPercentage.toFixed(4);
|
this.videoPercentage.toFixed(4)
|
||||||
scrollyVideoState.generalData.frameRate = this.frameRate.toFixed(2);
|
);
|
||||||
scrollyVideoState.generalData.currentTime = this.currentTime.toFixed(4);
|
scrollyVideoState.generalData.frameRate = parseFloat(
|
||||||
scrollyVideoState.generalData.totalTime = this.totalTime.toFixed(4);
|
this.frameRate.toFixed(2)
|
||||||
|
);
|
||||||
|
scrollyVideoState.generalData.currentTime = parseFloat(
|
||||||
|
this.currentTime.toFixed(4)
|
||||||
|
);
|
||||||
|
scrollyVideoState.generalData.totalTime = parseFloat(
|
||||||
|
this.totalTime.toFixed(4)
|
||||||
|
);
|
||||||
scrollyVideoState.usingWebCodecs = this.usingWebCodecs;
|
scrollyVideoState.usingWebCodecs = this.usingWebCodecs;
|
||||||
scrollyVideoState.framesData.currentFrame = this.currentFrame;
|
scrollyVideoState.framesData.currentFrame = this.currentFrame;
|
||||||
scrollyVideoState.framesData.totalFrames = this.frames.length;
|
scrollyVideoState.framesData.totalFrames = this.frames?.length || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default ScrollyVideo;
|
export default ScrollyVideo;
|
||||||
137
src/components/ScrollyVideo/js/mp4box.d.ts
vendored
Normal file
137
src/components/ScrollyVideo/js/mp4box.d.ts
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
declare module 'mp4box' {
|
||||||
|
export interface MP4MediaTrack {
|
||||||
|
id: number;
|
||||||
|
created: Date;
|
||||||
|
modified: Date;
|
||||||
|
movie_duration: number;
|
||||||
|
movie_timescale: number;
|
||||||
|
layer: number;
|
||||||
|
alternate_group: number;
|
||||||
|
volume: number;
|
||||||
|
track_width: number;
|
||||||
|
track_height: number;
|
||||||
|
timescale: number;
|
||||||
|
duration: number;
|
||||||
|
bitrate: number;
|
||||||
|
codec: string;
|
||||||
|
language: string;
|
||||||
|
nb_samples: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MP4VideoData {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MP4VideoTrack extends MP4MediaTrack {
|
||||||
|
video: MP4VideoData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MP4AudioData {
|
||||||
|
sample_rate: number;
|
||||||
|
channel_count: number;
|
||||||
|
sample_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MP4AudioTrack extends MP4MediaTrack {
|
||||||
|
audio: MP4AudioData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MP4Track = MP4VideoTrack | MP4AudioTrack;
|
||||||
|
|
||||||
|
export interface MP4Info {
|
||||||
|
duration: number;
|
||||||
|
timescale: number;
|
||||||
|
fragment_duration: number;
|
||||||
|
isFragmented: boolean;
|
||||||
|
isProgressive: boolean;
|
||||||
|
hasIOD: boolean;
|
||||||
|
brands: string[];
|
||||||
|
created: Date;
|
||||||
|
modified: Date;
|
||||||
|
tracks: MP4Track[];
|
||||||
|
audioTracks: MP4AudioTrack[];
|
||||||
|
videoTracks: MP4VideoTrack[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MP4Sample {
|
||||||
|
alreadyRead: number;
|
||||||
|
chunk_index: number;
|
||||||
|
chunk_run_index: number;
|
||||||
|
cts: number;
|
||||||
|
data: Uint8Array;
|
||||||
|
degradation_priority: number;
|
||||||
|
depends_on: number;
|
||||||
|
description: any;
|
||||||
|
description_index: number;
|
||||||
|
dts: number;
|
||||||
|
duration: number;
|
||||||
|
has_redundancy: number;
|
||||||
|
is_depended_on: number;
|
||||||
|
is_leading: number;
|
||||||
|
is_sync: boolean;
|
||||||
|
number: number;
|
||||||
|
offset: number;
|
||||||
|
size: number;
|
||||||
|
timescale: number;
|
||||||
|
track_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MP4ArrayBuffer = ArrayBuffer & { fileStart: number };
|
||||||
|
|
||||||
|
export class DataStream {
|
||||||
|
static BIG_ENDIAN: boolean;
|
||||||
|
static LITTLE_ENDIAN: boolean;
|
||||||
|
buffer: ArrayBuffer;
|
||||||
|
constructor(
|
||||||
|
arrayBuffer?: ArrayBuffer,
|
||||||
|
byteOffset: number,
|
||||||
|
endianness: boolean
|
||||||
|
): void;
|
||||||
|
// TODO: Complete interface
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Trak {
|
||||||
|
mdia?: {
|
||||||
|
minf?: {
|
||||||
|
stbl?: {
|
||||||
|
stsd?: {
|
||||||
|
entries: {
|
||||||
|
avcC?: {
|
||||||
|
write: (stream: DataStream) => void;
|
||||||
|
};
|
||||||
|
hvcC?: {
|
||||||
|
write: (stream: DataStream) => void;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// TODO: Complete interface
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MP4File {
|
||||||
|
[x: string]: any;
|
||||||
|
onMoovStart?: () => void;
|
||||||
|
onReady?: (info: MP4Info) => void;
|
||||||
|
onError?: (e: string) => void;
|
||||||
|
onSamples?: (id: number, user: any, samples: MP4Sample[]) => any;
|
||||||
|
|
||||||
|
appendBuffer(data: MP4ArrayBuffer): number;
|
||||||
|
start(): void;
|
||||||
|
stop(): void;
|
||||||
|
flush(): void;
|
||||||
|
releaseUsedSamples(trackId: number, sampleNumber: number): void;
|
||||||
|
setExtractionOptions(
|
||||||
|
trackId: number,
|
||||||
|
user?: any,
|
||||||
|
options?: { nbSamples?: number; rapAlignment?: number }
|
||||||
|
): void;
|
||||||
|
getTrackById(trackId: number): Trak;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFile(): MP4File;
|
||||||
|
|
||||||
|
export {};
|
||||||
|
}
|
||||||
36
src/components/ScrollyVideo/js/utils.d.ts
vendored
36
src/components/ScrollyVideo/js/utils.d.ts
vendored
|
|
@ -1,36 +0,0 @@
|
||||||
import type { ScrollyVideoState } from './types';
|
|
||||||
|
|
||||||
type FlattenedScrollyVideoState = {
|
|
||||||
src: string;
|
|
||||||
videoPercentage: number;
|
|
||||||
frameRate: number;
|
|
||||||
currentTime: number;
|
|
||||||
totalTime: number;
|
|
||||||
usingWebCodecs: boolean;
|
|
||||||
codec: string;
|
|
||||||
currentFrame: number;
|
|
||||||
totalFrames: number;
|
|
||||||
isAutoPlaying: boolean;
|
|
||||||
autoplayProgress: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function debounce<T extends (...args: unknown[]) => unknown>(
|
|
||||||
func: T,
|
|
||||||
delay?: number
|
|
||||||
): (...args: Parameters<T>) => void;
|
|
||||||
export function isScrollPositionAtTarget(
|
|
||||||
targetScrollPosition: number | null,
|
|
||||||
threshold?: number
|
|
||||||
): boolean;
|
|
||||||
function constrain(n: number, low: number, high: number): number;
|
|
||||||
export function map(
|
|
||||||
n: number,
|
|
||||||
start1: number,
|
|
||||||
stop1: number,
|
|
||||||
start2: number,
|
|
||||||
stop2: number,
|
|
||||||
withinBounds?: boolean
|
|
||||||
): number;
|
|
||||||
export function flattenObject(
|
|
||||||
obj: ScrollyVideoState
|
|
||||||
): FlattenedScrollyVideoState;
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
export function debounce(func, delay = 0) {
|
|
||||||
let timeoutId;
|
|
||||||
|
|
||||||
return (...args) => {
|
|
||||||
// Clear the previous timeout if it exists
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
// Set a new timeout to call the function later
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
func.apply(this, args);
|
|
||||||
}, delay);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isScrollPositionAtTarget = (
|
|
||||||
targetScrollPosition,
|
|
||||||
threshold = 1
|
|
||||||
) => {
|
|
||||||
const currentScrollPosition = window.pageYOffset;
|
|
||||||
const difference = Math.abs(currentScrollPosition - targetScrollPosition);
|
|
||||||
|
|
||||||
return difference < threshold;
|
|
||||||
};
|
|
||||||
|
|
||||||
function constrain(n, low, high) {
|
|
||||||
return Math.max(Math.min(n, high), low);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function map(n, start1, stop1, start2, stop2, withinBounds = true) {
|
|
||||||
const newval = ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2;
|
|
||||||
if (!withinBounds) {
|
|
||||||
return newval;
|
|
||||||
}
|
|
||||||
if (start2 < stop2) {
|
|
||||||
return constrain(newval, start2, stop2);
|
|
||||||
} else {
|
|
||||||
return constrain(newval, stop2, start2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function flattenObject(obj) {
|
|
||||||
const result = {};
|
|
||||||
|
|
||||||
function flatten(current, property) {
|
|
||||||
if (Object(current) !== current) {
|
|
||||||
result[property] = current;
|
|
||||||
} else if (Array.isArray(current)) {
|
|
||||||
for (let i = 0, l = current.length; i < l; i++) {
|
|
||||||
flatten(current[i], property + '[' + i + ']');
|
|
||||||
if (l === 0) {
|
|
||||||
result[property] = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let isEmpty = true;
|
|
||||||
for (const p in current) {
|
|
||||||
isEmpty = false;
|
|
||||||
flatten(current[p], p);
|
|
||||||
}
|
|
||||||
if (isEmpty && property) {
|
|
||||||
result[property] = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flatten(obj, '');
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
96
src/components/ScrollyVideo/js/utils.ts
Normal file
96
src/components/ScrollyVideo/js/utils.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import type { ScrollyVideoState } from './state.svelte';
|
||||||
|
|
||||||
|
type FlattenedScrollyVideoState = {
|
||||||
|
src: string;
|
||||||
|
videoPercentage: number;
|
||||||
|
frameRate: number;
|
||||||
|
currentTime: number;
|
||||||
|
totalTime: number;
|
||||||
|
usingWebCodecs: boolean;
|
||||||
|
codec: string;
|
||||||
|
currentFrame: number;
|
||||||
|
totalFrames: number;
|
||||||
|
isAutoPlaying: boolean;
|
||||||
|
autoplayProgress: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function debounce<T extends (...args: unknown[]) => void>(
|
||||||
|
func: T,
|
||||||
|
delay = 0
|
||||||
|
) {
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
func(...args);
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isScrollPositionAtTarget = (
|
||||||
|
targetScrollPosition: number,
|
||||||
|
threshold: number = 1
|
||||||
|
) => {
|
||||||
|
const currentScrollPosition = window.pageYOffset;
|
||||||
|
const difference = Math.abs(currentScrollPosition - targetScrollPosition);
|
||||||
|
|
||||||
|
return difference < threshold;
|
||||||
|
};
|
||||||
|
|
||||||
|
function constrain(n: number, low: number, high: number): number {
|
||||||
|
return Math.max(Math.min(n, high), low);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function map(
|
||||||
|
n: number,
|
||||||
|
start1: number,
|
||||||
|
stop1: number,
|
||||||
|
start2: number,
|
||||||
|
stop2: number,
|
||||||
|
withinBounds: boolean = true
|
||||||
|
): number {
|
||||||
|
const newval = ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2;
|
||||||
|
if (!withinBounds) {
|
||||||
|
return newval;
|
||||||
|
}
|
||||||
|
if (start2 < stop2) {
|
||||||
|
return constrain(newval, start2, stop2);
|
||||||
|
} else {
|
||||||
|
return constrain(newval, stop2, start2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenObject(
|
||||||
|
obj: ScrollyVideoState
|
||||||
|
): FlattenedScrollyVideoState {
|
||||||
|
const result: { [key: string]: unknown } = {};
|
||||||
|
|
||||||
|
function flatten(current: string | unknown[] | object, property: string) {
|
||||||
|
if (Object(current) !== current) {
|
||||||
|
result[property] = current;
|
||||||
|
} else if (Array.isArray(current)) {
|
||||||
|
for (let i = 0, l = current.length; i < l; i++) {
|
||||||
|
flatten(current[i], property + '[' + i + ']');
|
||||||
|
if (l === 0) {
|
||||||
|
result[property] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof current === 'object') {
|
||||||
|
let isEmpty = true;
|
||||||
|
for (const p in current) {
|
||||||
|
isEmpty = false;
|
||||||
|
flatten(
|
||||||
|
(current as { [key: string]: string | object | unknown[] })[p],
|
||||||
|
p
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isEmpty && property) {
|
||||||
|
result[property] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flatten(obj, '');
|
||||||
|
return result as FlattenedScrollyVideoState;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
declare function _default(
|
|
||||||
src: string,
|
|
||||||
emitFrame: (frame: ImageBitmap) => void,
|
|
||||||
debug: boolean
|
|
||||||
): Promise<never> | Promise<void>;
|
|
||||||
export default _default;
|
|
||||||
|
|
@ -1,28 +1,36 @@
|
||||||
import * as MP4Box from 'mp4box';
|
import * as MP4Box from 'mp4box';
|
||||||
import { scrollyVideoState } from './state.svelte';
|
import { scrollyVideoState } from './state.svelte';
|
||||||
|
|
||||||
|
interface MP4BoxBuffer extends ArrayBuffer {
|
||||||
|
fileStart: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Taken from https://github.com/w3c/webcodecs/blob/main/samples/mp4-decode/mp4_demuxer.js
|
* Taken from https://github.com/w3c/webcodecs/blob/main/samples/mp4-decode/mp4_demuxer.js
|
||||||
*/
|
*/
|
||||||
class Writer {
|
class Writer {
|
||||||
constructor(size) {
|
data: Uint8Array;
|
||||||
|
idx: number;
|
||||||
|
size: number;
|
||||||
|
|
||||||
|
constructor(size: number) {
|
||||||
this.data = new Uint8Array(size);
|
this.data = new Uint8Array(size);
|
||||||
this.idx = 0;
|
this.idx = 0;
|
||||||
this.size = size;
|
this.size = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
getData() {
|
getData(): Uint8Array {
|
||||||
if (this.idx !== this.size)
|
if (this.idx !== this.size)
|
||||||
throw new Error('Mismatch between size reserved and sized used');
|
throw new Error('Mismatch between size reserved and sized used');
|
||||||
return this.data.slice(0, this.idx);
|
return this.data.slice(0, this.idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeUint8(value) {
|
writeUint8(value: number): void {
|
||||||
this.data.set([value], this.idx);
|
this.data.set([value], this.idx);
|
||||||
this.idx += 1;
|
this.idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeUint16(value) {
|
writeUint16(value: number): void {
|
||||||
const arr = new Uint16Array(1);
|
const arr = new Uint16Array(1);
|
||||||
arr[0] = value;
|
arr[0] = value;
|
||||||
const buffer = new Uint8Array(arr.buffer);
|
const buffer = new Uint8Array(arr.buffer);
|
||||||
|
|
@ -30,7 +38,7 @@ class Writer {
|
||||||
this.idx += 2;
|
this.idx += 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeUint8Array(value) {
|
writeUint8Array(value: number[]): void {
|
||||||
this.data.set(value, this.idx);
|
this.data.set(value, this.idx);
|
||||||
this.idx += value.length;
|
this.idx += value.length;
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +50,17 @@ class Writer {
|
||||||
* @param avccBox
|
* @param avccBox
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
const getExtradata = (avccBox) => {
|
const getExtradata = (avccBox: {
|
||||||
|
SPS: string | unknown[];
|
||||||
|
PPS: string | unknown[];
|
||||||
|
configurationVersion: number;
|
||||||
|
AVCProfileIndication: number;
|
||||||
|
profile_compatibility: number;
|
||||||
|
AVCLevelIndication: number;
|
||||||
|
lengthSizeMinusOne: number;
|
||||||
|
nb_SPS_nalus: number;
|
||||||
|
nb_PPS_nalus: number;
|
||||||
|
}) => {
|
||||||
let i;
|
let i;
|
||||||
let size = 7;
|
let size = 7;
|
||||||
for (i = 0; i < avccBox.SPS.length; i += 1) {
|
for (i = 0; i < avccBox.SPS.length; i += 1) {
|
||||||
|
|
@ -94,11 +112,19 @@ const getExtradata = (avccBox) => {
|
||||||
* @returns {Promise<unknown>}
|
* @returns {Promise<unknown>}
|
||||||
*/
|
*/
|
||||||
const decodeVideo = (
|
const decodeVideo = (
|
||||||
src,
|
src: string,
|
||||||
emitFrame,
|
emitFrame: (frame: ImageBitmap) => void,
|
||||||
{ VideoDecoder, EncodedVideoChunk, debug }
|
{
|
||||||
) =>
|
VideoDecoder,
|
||||||
new Promise((resolve, reject) => {
|
EncodedVideoChunk,
|
||||||
|
debug,
|
||||||
|
}: {
|
||||||
|
VideoDecoder: typeof window.VideoDecoder;
|
||||||
|
EncodedVideoChunk: typeof window.EncodedVideoChunk;
|
||||||
|
debug: boolean;
|
||||||
|
}
|
||||||
|
): Promise<unknown> =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
if (debug) console.info('Decoding video from', src);
|
if (debug) console.info('Decoding video from', src);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -170,16 +196,19 @@ const decodeVideo = (
|
||||||
|
|
||||||
// Fetches the file into arraybuffers
|
// Fetches the file into arraybuffers
|
||||||
fetch(src).then((res) => {
|
fetch(src).then((res) => {
|
||||||
|
if (!res.body) throw new Error('Response body is null');
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
||||||
function appendBuffers({ done, value }) {
|
function appendBuffers(
|
||||||
if (done) {
|
result: ReadableStreamReadResult<Uint8Array>
|
||||||
|
): Promise<void | null> {
|
||||||
|
if (result.done) {
|
||||||
mp4boxfile.flush();
|
mp4boxfile.flush();
|
||||||
return null;
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buf = value.buffer;
|
const buf = result.value.buffer as MP4BoxBuffer;
|
||||||
buf.fileStart = offset;
|
buf.fileStart = offset;
|
||||||
offset += buf.byteLength;
|
offset += buf.byteLength;
|
||||||
mp4boxfile.appendBuffer(buf);
|
mp4boxfile.appendBuffer(buf);
|
||||||
|
|
@ -203,7 +232,11 @@ const decodeVideo = (
|
||||||
* @param debug
|
* @param debug
|
||||||
* @returns {Promise<never>|Promise<void>*}
|
* @returns {Promise<never>|Promise<void>*}
|
||||||
*/
|
*/
|
||||||
export default (src, emitFrame, debug) => {
|
export default (
|
||||||
|
src: string,
|
||||||
|
emitFrame: (frame: ImageBitmap) => void,
|
||||||
|
debug: boolean = false
|
||||||
|
) => {
|
||||||
// If our browser supports WebCodecs natively
|
// If our browser supports WebCodecs natively
|
||||||
if (
|
if (
|
||||||
typeof VideoDecoder === 'function' &&
|
typeof VideoDecoder === 'function' &&
|
||||||
Loading…
Reference in a new issue