Merge pull request #342 from reuters-graphics/mf-prop-fixes

Documentation, demo, props cleanup
This commit is contained in:
Minami 2025-08-07 08:44:36 -04:00 committed by GitHub
commit 0f39319ae1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 56 additions and 81 deletions

View file

@ -10,9 +10,9 @@ The `ScrollerVideo` component creates interactive video experiences that respond
## Basic demo
To use the `ScrollerVideo` component, import it and provide the video source. Set the `height` prop to any valid CSS height value such as `1200px`, `150vh`, or `500lvh` to set the scroll height.
To use the `ScrollerVideo` component, import it and provide the video source. The scroll height defaults to `200lvh`, but you can adjust this to any valid CSS height value such as `1200px` or `200lvh` with the `height` prop.
> It is advisable to use 'lvh' or 'svh' units instead of 'vh' unit for the height, as these units are more reliable across different devices.
> 💡TIP: Use `lvh` or `svh` units instead of `vh` unit for the height, as [these units](https://www.w3.org/TR/css-values-4/#large-viewport-size) are more reliable on mobile or other devices where elements such as the address bar toggle between being shown and hidden.
[Demo](?path=/story/components-graphics-scrollervideo--demo)
@ -21,6 +21,7 @@ To use the `ScrollerVideo` component, import it and provide the video source. Se
import { ScrollerVideo } from '@reuters-graphics/graphics-components';
</script>
<!-- Optionally set `height` to adjust scroll height -->
<ScrollerVideo src="my-video.mp4" height="500lvh" />
```
@ -69,7 +70,7 @@ To show different videos based on the screen width, use the `ScrollerVideo` comp
## Embeds
Setting `embedded` will autoplay the entire `ScrollerVideo` component like a video.
Setting `embedded` to `true` will turn `ScrollerVideo` into an embeddable version, where the video autoplays when the user scrolls upon it. Optionally, you can control the embed video behaviour by passing `embeddedProps` to control the autoplay `delay`, `threshold` for triggering autoplay, and the `duration` of the video.
> 💡**TIP:** Another way to recreate the ScrollerVideo experience for embeds is to record the desktop screen with [Scroll Capture](https://chromewebstore.google.com/detail/scroll-capture/egmhoeaacclmanaimofoooiamhpkimkk?hl=en) while scrolling through the video and use that video instead as an HTML video component.
@ -78,19 +79,17 @@ Setting `embedded` will autoplay the entire `ScrollerVideo` component like a vid
```svelte
<script lang="ts">
import { ScrollerVideo } from '@reuters-graphics/graphics-components';
let embedded = $state(true); // Set to true to enable embedded mode
</script>
<ScrollerVideo
src={Goldengate}
height="200lvh"
trackScroll={true}
showDebugInfo
src='my-video.mp4'
{embedded}
embeddedProps={{
delay: 200,
threshold: 0.5, // threshold for triggering the autoplay
height: '80lvh', // height of the scroll container
duration: 5000, // time duration from start to end
delay: 200, // Optional: Delay before autoplay starts. Defaults to 200ms.
threshold: 0.5, // Optional: Threshold for triggering the autoplay. Defaults to 0.5.
duration: 5000, // Optional: Defaults to the duration of the video.
}}
/>
```

View file

@ -137,17 +137,7 @@
};
const args = {
trackScroll: true,
height: '500lvh',
showDebugInfo: true,
autoplay: false,
full: true,
sticky: true,
objectFit: 'cover',
transitionSpeed: 8,
frameThreshold: 0.1,
useWebCodecs: true,
lockScroll: true,
};
</script>
@ -168,21 +158,11 @@
</Story>
<Story name="Embed version" exportName="Embed">
<!-- <ScrollerVideo
embedded={true}
src={videoSrc.Goldengate}
embeddedProps={{ autoplay: true }}
/> -->
<Embedded />
</Story>
<Story name="Autoplay" {args}>
<ScrollerVideo
{...args}
src={videoSrc.Goldengate}
useWebCodecs={false}
autoplay={true}
></ScrollerVideo>
<Story name="Autoplay">
<ScrollerVideo {...args} src={videoSrc.Goldengate} autoplay={true} />
</Story>
<Story

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import ScrollerVideo from './ts/ScrollerVideo.js';
import ScrollerVideo from './ts/ScrollerVideo';
import Debug from './Debug.svelte';
import type { Snippet } from 'svelte';
import { setContext } from 'svelte';
@ -15,7 +15,7 @@
/** Bindable instance of ScrollerVideo */
scrollerVideo?: ScrollerVideo;
/** Video source URL */
src?: string;
src: string;
/** Bindable percentage value to control video playback. **Ranges from 0 to 1** */
videoPercentage?: number;
/** Sets the maximum playbackRate for this video */
@ -52,8 +52,6 @@
embeddedProps?: {
/** When to start the playback in terms of the component's position */
threshold?: number;
/** Height of embedded component */
height?: string;
/** Duration of ScrollerVideo experience as a video */
duration?: number;
/** Delay before the playback */
@ -66,7 +64,6 @@
/** Default properties for embedded videos */
const defaultEmbedProps = {
threshold: 0.5,
height: '80lvh',
delay: 200,
};
@ -75,14 +72,15 @@
* Handles instantiation, prop changes, and cleanup.
*/
let {
class: cls = '',
id = '',
src,
scrollerVideo = $bindable(),
videoPercentage,
onReady = $bindable(() => {}),
onChange = $bindable(() => {}),
height = '200lvh',
showDebugInfo = false,
class: cls = '',
id = '',
embedded = false,
embeddedProps,
children,
@ -136,6 +134,7 @@
if (scrollerVideo && scrollerVideo.destroy) scrollerVideo.destroy();
scrollerVideo = new ScrollerVideo({
src,
scrollerVideoContainer,
onReady,
onChange,
@ -246,7 +245,6 @@
{#if embedded}
<div
class="embedded-scroller-video-container"
style="height: {allEmbedProps.height};"
bind:this={embeddedContainer}
bind:clientHeight={embeddedContainerHeight}
onscroll={() => {
@ -260,18 +258,8 @@
}
}}
>
<!-- style needs to be >= 100lvh to allow child element to scroll -->
<!-- 200lvh provides smoother scrolling experience -->
<div
{id}
class="scroller-video-container embedded {cls}"
style="height: 200lvh;"
>
<div
bind:this={scrollerVideoContainer}
data-scroller-container
style="max-height: {allEmbedProps.height};"
>
<div {id} class="scroller-video-container embedded {cls}">
<div bind:this={scrollerVideoContainer} data-scroller-container>
{@render supportingElements()}
</div>
</div>
@ -292,6 +280,11 @@
.scroller-video-container {
width: 100%;
// Needs to be >= 100lvh to allow child element to scroll
// 200lvh provides smoother scrolling experience -->
&.embedded {
height: 200lvh;
}
&:not(.embedded) {
min-height: 100lvh;
}

View file

@ -3,6 +3,7 @@
import ScrollerVideoForeground from '../ScrollerVideoForeground.svelte';
import Goldengate from '../videos/goldengate.mp4';
import BodyText from '../../BodyText/BodyText.svelte';
import Block from '../../Block/Block.svelte';
import type { ContainerWidth } from '../../@types/global';
@ -46,22 +47,24 @@
const dummyText =
'Reprehenderit hamburger pork bresaola, dolore chuck sirloin landjaeger ham hock tempor meatball alcatra nostrud pork belly. Culpa pork belly doner ea jowl, elit deserunt leberkas cow shoulder ham hock dolore.';
const scrollerVideoBlock = content.blocks[0];
let embedded = $state(true);
</script>
<BodyText text={dummyText} />
<BodyText text={dummyText} />
<BodyText text={dummyText} />
<BodyText text={dummyText} />
<BodyText text={dummyText} />
<BodyText text={dummyText} />
<ScrollerVideo
src={Goldengate}
height="200lvh"
trackScroll={true}
showDebugInfo
embedded={true}
class="embedded-demo"
showDebugInfo={true}
{embedded}
embeddedProps={{
threshold: 0.5,
height: '100lvh',
duration: 12000,
delay: 200,
}}
@ -78,3 +81,19 @@
{/each}
</ScrollerVideo>
<BodyText text={dummyText} />
<BodyText text={dummyText} />
<BodyText text={dummyText} />
<BodyText text={dummyText} />
<BodyText text={dummyText} />
<BodyText text={dummyText} />
<style lang="scss">
:global {
.embedded-demo .foreground-text {
h4,
p {
color: white;
}
}
}
</style>

View file

@ -4,7 +4,7 @@ import { debounce, isScrollPositionAtTarget, map, constrain } from './utils';
import { createComponentState, type ScrollerVideoState } from './state.svelte';
interface ScrollerVideoArgs {
src?: string;
src: string;
scrollerVideoContainer: HTMLElement | string;
objectFit?: string;
sticky?: boolean;
@ -192,7 +192,7 @@ class ScrollerVideo {
* @param {ScrollerVideoArgs} args - The arguments for initialization.
*/
constructor({
src = 'https://scrollyvideo.js.org/goldengate.mp4',
src,
scrollerVideoContainer,
objectFit = 'cover',
sticky = true,
@ -202,8 +202,8 @@ class ScrollerVideo {
transitionSpeed = 8,
frameThreshold = 0.1,
useWebCodecs = true,
onReady = () => {},
onChange = (_percentage?: number) => {},
onReady = () => { },
onChange = (_percentage?: number) => { },
debug = false,
autoplay = false,
}: ScrollerVideoArgs) {
@ -240,24 +240,8 @@ class ScrollerVideo {
this.totalTime = 0; // The total time of the video, used for calculating percentage
this.transitioningRaf = null;
this.componentState = createComponentState();
this.componentState.willAutoPlay = autoplay;
// Make sure that we have a DOM
if (typeof document !== 'object') {
console.error('ScrollerVideo must be initiated in a DOM context');
return;
}
// Make sure the basic arguments are set for scrollervideo
if (!scrollerVideoContainer) {
console.error('scrollerVideoContainer must be a valid DOM object');
return;
}
if (!src) {
console.error('Must provide valid video src to ScrollerVideo');
return;
}
// Save the container. If the container is a string we get the element
@ -741,7 +725,7 @@ class ScrollerVideo {
const hasPassedThreshold =
isForwardTransition ?
this.currentTime >= this.targetTime
: this.currentTime <= this.targetTime;
: this.currentTime <= this.targetTime;
if (this.componentState.isAutoPlaying) {
this.componentState.autoplayProgress = parseFloat(
@ -780,7 +764,7 @@ class ScrollerVideo {
isForwardTransition ?
startCurrentTime +
easedProgress * Math.abs(distance) * transitionSpeed
: startCurrentTime -
: startCurrentTime -
easedProgress * Math.abs(distance) * transitionSpeed;
if (this.canvas) {
@ -869,7 +853,7 @@ class ScrollerVideo {
const targetDuration =
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
this.targetTime = Math.max(Math.min(percentage, 1), 0) * targetDuration;