edited advance used demo docs
This commit is contained in:
parent
0658de887e
commit
7d69c76120
5 changed files with 99 additions and 198 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Component } from 'svelte';
|
import type { Component } from 'svelte';
|
||||||
|
import type { TransitionOptions } from '../../components/ScrollyVideo/ts/ScrollyVideo.ts';
|
||||||
|
import type { ScrollyVideoState } from '../../components/ScrollyVideo/ts/state.svelte.ts';
|
||||||
/**
|
/**
|
||||||
* Used for the list of <option> tags nested in a <select> input.
|
* Used for the list of <option> tags nested in a <select> input.
|
||||||
*/
|
*/
|
||||||
|
|
@ -67,3 +68,50 @@ export type ScrollyVideoForegroundPosition =
|
||||||
| 'center center'
|
| 'center center'
|
||||||
| 'center left'
|
| 'center left'
|
||||||
| 'center right';
|
| 'center right';
|
||||||
|
|
||||||
|
// Complete ScrollyVideo instance interface
|
||||||
|
export interface ScrollyVideoInstance {
|
||||||
|
// Properties
|
||||||
|
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;
|
||||||
|
currentFrame: number;
|
||||||
|
usingWebCodecs: boolean;
|
||||||
|
totalTime: number;
|
||||||
|
transitioningRaf: number | null;
|
||||||
|
componentState: ScrollyVideoState;
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
updateScrollPercentage: ((jump: boolean) => void) | undefined;
|
||||||
|
resize: (() => void) | undefined;
|
||||||
|
setVideoPercentage(percentage: number, options?: TransitionOptions): void;
|
||||||
|
setCoverStyle(el: HTMLElement | HTMLCanvasElement | undefined): void;
|
||||||
|
decodeVideo(): Promise<void>;
|
||||||
|
paintCanvasFrame(frameNum: number): void;
|
||||||
|
transitionToTargetTime(options: TransitionOptions): void;
|
||||||
|
setTargetTimePercent(percentage: number, options?: TransitionOptions): void;
|
||||||
|
setScrollPercent(percentage: number): void;
|
||||||
|
destroy(): void;
|
||||||
|
autoplayScroll(): void;
|
||||||
|
updateDebugInfo(): void;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -385,178 +385,77 @@ The `ScrollyVideo` component can be used inside the [ScrollerBase](?path=/story/
|
||||||
|
|
||||||
## Advanced usecases
|
## Advanced usecases
|
||||||
|
|
||||||
For advanced use cases such as looping a particular section of the video, or jumping to a specific time in the video, you can bind `scrollyVideo` prop and take benefits of methods such as `setVideoPercentage` or bindable methods such as `onReady` and `onChange`. This allows for fine-grained control over the video playback and interaction with the scroll position.
|
Using the methods attached to the bindable prop `scrollyVideo` allows for advanced customisation of the scroll video behaviour. For example, you can create a looping video that plays a specific section of the video repeatedly, or jump to a specific time in the video when the user scrolls to a certain point.
|
||||||
|
|
||||||
|
This code below would make the video smoothly jump to the halfway point of the video. Setting `jump` to `true` will make the video jump to the specified percentage abruptly:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
scrollyVideo.setVideoPercentage(0.5, {
|
scrollyVideo.setVideoPercentage(0.5, {
|
||||||
jump: true,
|
jump: false,
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
This will seek the video to 50% progress at a single instant. Setting `jump` to `false` will make smooth transition to the provided progress value.
|
> **Note**: When using these methods, set `trackScroll` to `false` and pass functions to the `onReady` prop to ensure that the video is ready before calling any methods on it.
|
||||||
|
|
||||||
[Demo](?path=/story/components-graphics-scrollyvideo--advanced)
|
Here is a [demo](?path=/story/components-graphics-scrollyvideo--advanced) that uses `ScrollyVideo` with `ScrollerBase` to make the video jump to the start or the end of the video depending on what step of the scroller the user is on.
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
ScrollyVideo,
|
ScrollyVideo,
|
||||||
ScrollerBase,
|
ScrollerBase,
|
||||||
|
type ScrollyVideoInstance,
|
||||||
} from '@reuters-graphics/graphics-components';
|
} from '@reuters-graphics/graphics-components';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
let progress = $state(0);
|
let scrollyVideo: ScrollyVideoInstance | undefined = $state(undefined);
|
||||||
let scrollyVideo = $state();
|
let animationFrame = $state(0);
|
||||||
let now;
|
let index = $state(0); // index for the current step in ScrollerBase
|
||||||
let then = 0;
|
|
||||||
let time = 0;
|
|
||||||
let currentProgress = 0; // holds progress value for dynamic looping
|
|
||||||
let loopCutoff = 0.33; // value between 0-1 to loop the video by
|
|
||||||
let totalTime = 9 * 1000; // milliseconds
|
|
||||||
|
|
||||||
let animationId;
|
// If ScrollerBase is on index 0, jump to the start of the video.
|
||||||
|
// Otherwise, jump to 1, or 100% (the end), of the video.
|
||||||
// clamps n value between low and high
|
function jumpVideo() {
|
||||||
function constrain(n, low, high) {
|
if (index === 0) {
|
||||||
return Math.max(Math.min(n, high), low);
|
scrollyVideo?.setVideoPercentage(0, {
|
||||||
}
|
jump: false, // Eases the jump
|
||||||
|
});
|
||||||
// maps n value between two ranges
|
|
||||||
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 {
|
} else {
|
||||||
return constrain(newval, stop2, start2);
|
scrollyVideo?.setVideoPercentage(1, {
|
||||||
}
|
jump: false,
|
||||||
}
|
});
|
||||||
|
|
||||||
// loops the video between 0 and loopCutoff
|
|
||||||
function renderVideo() {
|
|
||||||
if (progress < loopCutoff) {
|
|
||||||
now = Date.now();
|
|
||||||
const elapsed = now - then;
|
|
||||||
|
|
||||||
// if (elapsed > fpsInterval) {
|
|
||||||
time += elapsed;
|
|
||||||
currentProgress = map(time, 0, totalTime, 0, 1);
|
|
||||||
scrollyVideo.setVideoPercentage(currentProgress, { jump: true });
|
|
||||||
|
|
||||||
if (currentProgress > loopCutoff) {
|
|
||||||
currentProgress = 0;
|
|
||||||
time = 0;
|
|
||||||
scrollyVideo.setVideoPercentage(0, { jump: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
then = now;
|
|
||||||
// }
|
|
||||||
} else {
|
|
||||||
scrollyVideo.setVideoPercentage(progress, { jump: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
animationId = requestAnimationFrame(renderVideo);
|
animationFrame = requestAnimationFrame(jumpVideo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// initializes video autoplay
|
// Cancel requestAnimationFrame on destroy
|
||||||
// when it's ready to play
|
|
||||||
function initAutoplay() {
|
|
||||||
then = Date.now();
|
|
||||||
renderVideo();
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancel RAF on destroy
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (animationId) {
|
cancelAnimationFrame(animationFrame);
|
||||||
cancelAnimationFrame(animationId);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollerBase bind:progress query="div.step-foreground-container" visible>
|
<ScrollerBase bind:index query="div.step-foreground-container">
|
||||||
|
<!-- ScrollyVideo as background -->
|
||||||
{#snippet backgroundSnippet()}
|
{#snippet backgroundSnippet()}
|
||||||
|
<!-- Pass `jumpVideo` to `onReady` and set `trackScroll` to `false` -->
|
||||||
<ScrollyVideo
|
<ScrollyVideo
|
||||||
bind:scrollyVideo
|
bind:scrollyVideo
|
||||||
src="my-video.mp4"
|
src={Tennis}
|
||||||
height="100svh"
|
height="100svh"
|
||||||
trackScroll={false}
|
trackScroll={false}
|
||||||
showDebugInfo
|
showDebugInfo
|
||||||
onReady={initAutoplay}
|
onReady={jumpVideo}
|
||||||
/>
|
/>
|
||||||
<!-- Only for debugging -->
|
|
||||||
<div id="progress-bar">
|
|
||||||
<p>ScrollerBase progress: {progress.toPrecision(2)}</p>
|
|
||||||
<progress class="mb-4" value={progress}></progress>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
<!-- Simple text foregrounds -->
|
||||||
{#snippet foregroundSnippet()}
|
{#snippet foregroundSnippet()}
|
||||||
<!-- Add custom foreground HTML or component -->
|
|
||||||
<div class="step-foreground-container">
|
<div class="step-foreground-container">
|
||||||
<h3 class="text-center">Step 1</h3>
|
<h3 class="text-center">Index {index}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="step-foreground-container">
|
<div class="step-foreground-container">
|
||||||
<h3 class="text-center">Step 2</h3>
|
<h3 class="text-center">Index {index}</h3>
|
||||||
</div>
|
|
||||||
<div class="step-foreground-container">
|
|
||||||
<h3 class="text-center">Step 3</h3>
|
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ScrollerBase>
|
</ScrollerBase>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use '../../../scss/mixins' as mixins;
|
|
||||||
|
|
||||||
// svelte-scroller-background
|
|
||||||
#progress-bar {
|
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
|
||||||
position: absolute;
|
|
||||||
z-index: 4;
|
|
||||||
right: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
progress {
|
|
||||||
height: 6px;
|
|
||||||
background-color: #ff000044; /* Background color of the entire bar */
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
progress::-webkit-progress-value {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
progress::-webkit-progress-bar {
|
|
||||||
background-color: #444444;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-family: var(--theme-font-family-sans-serif);
|
|
||||||
color: white;
|
|
||||||
font-size: var(--theme-font-size-xs);
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-foreground-container {
|
|
||||||
height: 100vh;
|
|
||||||
width: 50%;
|
|
||||||
padding: 1em;
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
// align center
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,17 @@
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import type { ScrollyVideoInstance } from '../ts/ScrollyVideo';
|
import type { ScrollyVideoInstance } from '../../@types/global.ts';
|
||||||
|
|
||||||
let progress = $state(0);
|
|
||||||
let scrollyVideo: ScrollyVideoInstance | undefined = $state(undefined);
|
let scrollyVideo: ScrollyVideoInstance | undefined = $state(undefined);
|
||||||
let animationFrame = $state(0);
|
let animationFrame = $state(0);
|
||||||
let index = $state(0); // index for the current step in ScrollerBase
|
let index = $state(0); // index for the current step in ScrollerBase
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If ScrollerBase is on index 0, jump to the start of the video.
|
||||||
|
* Otherwise, jump to 1, or 100% (the end), of the video.
|
||||||
|
*/
|
||||||
function jumpVideo() {
|
function jumpVideo() {
|
||||||
// If ScrollerBase is on index 0, jump to the start of the video.
|
|
||||||
// Otherwise, jump to 100% (the end) of the video.
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
scrollyVideo?.setVideoPercentage(0, {
|
scrollyVideo?.setVideoPercentage(0, {
|
||||||
jump: false, // Eases the jump
|
jump: false, // Eases the jump
|
||||||
|
|
@ -34,14 +35,10 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollerBase
|
<ScrollerBase bind:index query="div.step-foreground-container">
|
||||||
bind:progress
|
|
||||||
bind:index
|
|
||||||
query="div.step-foreground-container"
|
|
||||||
visible
|
|
||||||
>
|
|
||||||
<!-- ScrollyVideo as background -->
|
<!-- ScrollyVideo as background -->
|
||||||
{#snippet backgroundSnippet()}
|
{#snippet backgroundSnippet()}
|
||||||
|
<!-- Pass `jumpVideo` to `onReady` and set `trackScroll` to `false` -->
|
||||||
<ScrollyVideo
|
<ScrollyVideo
|
||||||
bind:scrollyVideo
|
bind:scrollyVideo
|
||||||
src={Tennis}
|
src={Tennis}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ interface ScrollyVideoArgs {
|
||||||
resize?: () => void;
|
resize?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TransitionOptions {
|
export interface TransitionOptions {
|
||||||
jump: boolean;
|
jump: boolean;
|
||||||
transitionSpeed?: number;
|
transitionSpeed?: number;
|
||||||
easing?: ((progress: number) => number) | null;
|
easing?: ((progress: number) => number) | null;
|
||||||
|
|
@ -202,8 +202,8 @@ class ScrollyVideo {
|
||||||
transitionSpeed = 8,
|
transitionSpeed = 8,
|
||||||
frameThreshold = 0.1,
|
frameThreshold = 0.1,
|
||||||
useWebCodecs = true,
|
useWebCodecs = true,
|
||||||
onReady = () => { },
|
onReady = () => {},
|
||||||
onChange = (_percentage?: number) => { },
|
onChange = (_percentage?: number) => {},
|
||||||
debug = false,
|
debug = false,
|
||||||
autoplay = false,
|
autoplay = false,
|
||||||
}: ScrollyVideoArgs) {
|
}: ScrollyVideoArgs) {
|
||||||
|
|
@ -741,7 +741,7 @@ class ScrollyVideo {
|
||||||
const hasPassedThreshold =
|
const hasPassedThreshold =
|
||||||
isForwardTransition ?
|
isForwardTransition ?
|
||||||
this.currentTime >= this.targetTime
|
this.currentTime >= this.targetTime
|
||||||
: this.currentTime <= this.targetTime;
|
: this.currentTime <= this.targetTime;
|
||||||
|
|
||||||
if (this.componentState.isAutoPlaying) {
|
if (this.componentState.isAutoPlaying) {
|
||||||
this.componentState.autoplayProgress = parseFloat(
|
this.componentState.autoplayProgress = parseFloat(
|
||||||
|
|
@ -780,7 +780,7 @@ class ScrollyVideo {
|
||||||
isForwardTransition ?
|
isForwardTransition ?
|
||||||
startCurrentTime +
|
startCurrentTime +
|
||||||
easedProgress * Math.abs(distance) * transitionSpeed
|
easedProgress * Math.abs(distance) * transitionSpeed
|
||||||
: startCurrentTime -
|
: startCurrentTime -
|
||||||
easedProgress * Math.abs(distance) * transitionSpeed;
|
easedProgress * Math.abs(distance) * transitionSpeed;
|
||||||
|
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
|
|
@ -869,7 +869,7 @@ class ScrollyVideo {
|
||||||
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 || 0;
|
: 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;
|
||||||
|
|
||||||
|
|
@ -972,50 +972,3 @@ class ScrollyVideo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default ScrollyVideo;
|
export default ScrollyVideo;
|
||||||
|
|
||||||
// Complete ScrollyVideo instance interface
|
|
||||||
export interface ScrollyVideoInstance {
|
|
||||||
// Properties
|
|
||||||
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;
|
|
||||||
currentFrame: number;
|
|
||||||
usingWebCodecs: boolean;
|
|
||||||
totalTime: number;
|
|
||||||
transitioningRaf: number | null;
|
|
||||||
componentState: ScrollyVideoState;
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
updateScrollPercentage: ((jump: boolean) => void) | undefined;
|
|
||||||
resize: (() => void) | undefined;
|
|
||||||
setVideoPercentage(percentage: number, options?: TransitionOptions): void;
|
|
||||||
setCoverStyle(el: HTMLElement | HTMLCanvasElement | undefined): void;
|
|
||||||
decodeVideo(): Promise<void>;
|
|
||||||
paintCanvasFrame(frameNum: number): void;
|
|
||||||
transitionToTargetTime(options: TransitionOptions): void;
|
|
||||||
setTargetTimePercent(percentage: number, options?: TransitionOptions): void;
|
|
||||||
setScrollPercent(percentage: number): void;
|
|
||||||
destroy(): void;
|
|
||||||
autoplayScroll(): void;
|
|
||||||
updateDebugInfo(): void;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -51,4 +51,8 @@ export { default as ToolsHeader } from './components/ToolsHeader/ToolsHeader.sve
|
||||||
export { default as Video } from './components/Video/Video.svelte';
|
export { default as Video } from './components/Video/Video.svelte';
|
||||||
export { default as Visible } from './components/Visible/Visible.svelte';
|
export { default as Visible } from './components/Visible/Visible.svelte';
|
||||||
|
|
||||||
export type { ContainerWidth, HeadlineSize } from './components/@types/global';
|
export type {
|
||||||
|
ContainerWidth,
|
||||||
|
HeadlineSize,
|
||||||
|
ScrollyVideoInstance,
|
||||||
|
} from './components/@types/global';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue