adds embedded version

This commit is contained in:
Sudev Kiyada 2025-07-24 21:50:21 +05:30
parent cb8269e54c
commit b3c9b1dd08
Failed to extract signature
4 changed files with 226 additions and 62 deletions

View file

@ -65,11 +65,9 @@ To show different videos based on the screen width, use the `ScrollerVideo` comp
## Embeds
Scrollytelling does not work in iframes. If the prop `embedded` is set to `true`, the video will be rendered as a regular video instead. By default, the video element has the properties `loop`, `muted`, and `playsinline` and `controls`. To customise the video properties, use the `embeddedProps` prop to render the embed video.
Setting `embedded` will autoplay the whole `ScrollerVideo` component like a video.
To use a different video for the embedded version, pass its source to the `embeddedSrc` prop. If `embeddedSrc` is not provided, the component will use the `src` prop.
> 💡**TIP:** One 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 scrollying through the video and use that video instead.
> 💡**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 scrollying through the video and use that video instead as an HTML video component.
[Demo](?path=/story/components-graphics-scrollervideo--embed)
@ -78,18 +76,24 @@ To use a different video for the embedded version, pass its source to the `embed
import { ScrollerVideo } from '@reuters-graphics/graphics-components';
</script>
<!-- Optionally pass `embeddedSrc` and `embeddedProps` -->
<ScrollerVideo
embedded={true}
src="my-video.mp4"
embeddedSrc="my-video-embedded.mp4"
embeddedProps={{ autoplay: true }}
src={Goldengate}
height="200svh"
trackScroll={false}
showDebugInfo
embedded
embeddedProps={{
delay: 200,
threshold: 0.5, // threshold forplayback
height: '80svh', // height of the video
duration: 5000, // time duration from start to end
}}
/>
```
## Autoplay
The `autoplay` option combines the autoplay and scrollytelling experience. If set to `true`, the video will start playing automatically when the component is mounted, but switch to scrollytelling when the user starts scrolling. The scroll height is calculated based on how much of the video remains, which means that if the user lets the video autoplay to near the end, the user would only have to scroll through a small height to get to the end. If the user lets the video autoplay to the end, there will be no scrolling effect.
The `autoplay` option combines the autoplay and scrollytelling experience. If set to `true`, the video will start playing automatically when the component is mounted, but switch to scrollytelling when the user starts scrolling. The scroll height is calculated based on how much of the video rmains, which means that if the user lets the video autoplay to near the end, the user would only have to scroll through a small height to get to the end. If the user lets the video autoplay to the end, there will be no scrolling effect.
[Demo](?path=/story/components-graphics-scrollervideo--autoplay)

View file

@ -4,6 +4,7 @@
import WithScrollerBase from './demo/WithScrollerBase.svelte';
import WithAi2svelteForegrounds from './demo/WithAi2svelteForegrounds.svelte';
import WithTextForegrounds from './demo/WithTextForegrounds.svelte';
import Embedded from './demo/Embedded.svelte';
const { Story } = defineMeta({
title: 'Components/Graphics/ScrollerVideo',
@ -167,11 +168,12 @@
</Story>
<Story name="Embed version" exportName="Embed">
<ScrollerVideo
<!-- <ScrollerVideo
embedded={true}
src={videoSrc.Goldengate}
embeddedProps={{ autoplay: true }}
/>
/> -->
<Embedded />
</Story>
<Story name="Autoplay" {args}>

View file

@ -5,6 +5,8 @@
import type { Snippet } from 'svelte';
import { setContext } from 'svelte';
import { dev } from '$app/environment';
import { Tween } from 'svelte/motion';
import { linear } from 'svelte/easing';
interface Props {
/** CSS class for scroller container */
@ -47,24 +49,16 @@
autoplay?: boolean;
/** Variable to control component rendering on embed page */
embedded?: boolean;
/** Source for the embedded video. If not provided, defaults to `src` */
embeddedSrc?: string;
/** Additional properties for embedded videos */
embeddedProps?: {
/** Whether the video should autoplay */
autoplay?: boolean;
/** Whether the video should loop */
loop?: boolean;
/** Whether the video should be muted */
muted?: boolean;
/** Whether the video should play inline */
playsinline?: boolean;
/** Whether the video should have controls */
controls?: boolean;
/** Poster image for the embedded video */
poster?: string;
/** Preload setting for the embedded video: 'none' | 'metadata' | 'auto' */
preload?: 'none' | 'metadata' | 'auto';
/** 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 */
delay?: number;
};
/** Children render function */
children?: Snippet;
@ -72,13 +66,10 @@
/** Default properties for embedded videos */
const defaultEmbedProps = {
autoplay: false,
loop: false,
muted: true,
playsinline: true,
controls: true,
poster: '',
preload: 'auto' as 'auto' | 'metadata' | 'none',
threshold: 0.5,
height: '80svh',
duration: 5000,
delay: 200,
};
/**
@ -95,7 +86,6 @@
class: cls = '',
id = '',
embedded = false,
embeddedSrc,
embeddedProps,
children,
...restProps
@ -117,6 +107,29 @@
...embeddedProps,
};
// Holds regular scroller video component
// and scrolls automatically for embedded version
let embeddedContainer = $state<HTMLDivElement | undefined>(undefined);
let embeddedContainerHeight = $state<number | undefined>(undefined);
let embeddedContainerScrollHeight: number = $derived.by(() => {
let scrollHeight = 1;
if (embeddedContainer && embeddedContainerHeight) {
scrollHeight = embeddedContainer.scrollHeight - embeddedContainerHeight;
}
return scrollHeight;
});
const embeddedContainerScrollY = new Tween(0, {
duration: allEmbedProps.duration,
delay: allEmbedProps.delay,
easing: (t) => t,
});
$effect(() => {
if (embeddedContainer) {
embeddedContainer.scrollTop = embeddedContainerScrollY.current;
}
});
$effect(() => {
if (scrollerVideoContainer) {
if (JSON.stringify(restProps) !== lastPropsString) {
@ -130,6 +143,39 @@
...restProps,
});
// if embedded prop is set,
// play the video when it crosses the threshold
// and reset it to zero when it crosses the threshold in opposite direction
if (embedded) {
const updatedOnReady = () => {
// add user defined onReady
onReady();
window?.addEventListener('scroll', (e: Event) => {
if (
embeddedContainer &&
embeddedContainer.getBoundingClientRect().top <
window.innerHeight * allEmbedProps.threshold
) {
if (embeddedContainerScrollY.current == 0) {
embeddedContainerScrollY.target =
embeddedContainerScrollHeight;
}
} else if (
embeddedContainer &&
embeddedContainer.getBoundingClientRect().top >
window.innerHeight * allEmbedProps.threshold
) {
if (embeddedContainerScrollY.current > 0) {
embeddedContainerScrollY.set(0, { duration: 0 });
}
}
});
};
scrollerVideo.onReady = updatedOnReady;
}
// pass on component state to child components
// this controls fade in and out of foregrounds
setContext('scrollerVideoState', scrollerVideo.componentState);
@ -170,20 +216,53 @@
});
</script>
{#if embedded && (embeddedSrc || restProps.src)}
<div class="scroller-video-container embedded">
<video
class="scroller-video-embedded"
src={embeddedSrc || restProps.src}
autoplay={allEmbedProps.autoplay}
loop={allEmbedProps.loop}
muted={allEmbedProps.muted}
playsinline={allEmbedProps.playsinline}
controls={allEmbedProps.controls}
poster={allEmbedProps.poster}
preload={embeddedProps?.preload || defaultEmbedProps.preload}
style="width: 100%;"
></video>
<!-- snippet to avoid redundancy between regular and embedded versions -->
<!-- renders Debug component and children foregrounds -->
{#snippet supportingElements()}
{#if scrollerVideo}
{#if showDebugInfo && dev}
<div class="debug-info">
<Debug componentState={scrollerVideo.componentState} />
</div>
{/if}
<!-- renders foregrounds -->
{#if children}
{@render children()}
{/if}
{/if}
{/snippet}
{#if embedded}
<div
class="embedded-scroller-video-container"
style="height: {allEmbedProps.height};"
bind:this={embeddedContainer}
bind:clientHeight={embeddedContainerHeight}
onscroll={() => {
if (scrollerVideo && embeddedContainer) {
let scrollProgress =
embeddedContainer.scrollTop / embeddedContainerScrollHeight;
scrollerVideo.setVideoPercentage(scrollProgress, {
jump: scrollProgress == 0,
easing: (t) => t,
});
}
}}
>
<div
{id}
class="scroller-video-container embedded {cls}"
style="height: 200svh;"
>
<div
bind:this={scrollerVideoContainer}
data-scroller-container
style="max-height: {allEmbedProps.height};"
>
{@render supportingElements()}
</div>
</div>
</div>
{:else}
<div
@ -192,18 +271,7 @@
style="height: {heightChange}"
>
<div bind:this={scrollerVideoContainer} data-scroller-container>
{#if scrollerVideo}
{#if showDebugInfo && dev}
<div class="debug-info">
<Debug componentState={scrollerVideo.componentState} />
</div>
{/if}
<!-- renders foregrounds -->
{#if children}
{@render children()}
{/if}
{/if}
{@render supportingElements()}
</div>
</div>
{/if}
@ -216,4 +284,9 @@
min-height: 100svh;
}
}
.embedded-scroller-video-container {
max-height: 100svh;
overflow: hidden;
}
</style>

View file

@ -0,0 +1,85 @@
<script lang="ts">
import ScrollerVideo from '../ScrollerVideo.svelte';
import ScrollerVideoForeground from '../ScrollerVideoForeground.svelte';
import Goldengate from '../videos/goldengate.mp4';
import type { ContainerWidth } from '../../@types/global';
const content = {
blocks: [
{
type: 'scroller-video',
id: 'goldengate-scroller',
src: '../videos/goldengate.mp4',
height: '200svh',
foregrounds: [
{
startTime: '0',
endTime: '2',
width: 'normal',
position: 'bottom center',
backgroundColour: 'rgba(0, 0, 0, 0.8)',
text: '#### Golden Gate Bridge\n\nThe Golden Gate Bridge took over 4 years to build (1933-1937) and was the longest suspension bridge in the world at the time of its completion, spanning 4,200 feet between its towers.',
},
{
startTime: '4',
endTime: '7',
width: 'normal',
position: 'bottom center',
backgroundColour: 'rgba(0, 0, 0, 0.8)',
text: "The bridge's iconic International Orange color was chosen partly for visibility in San Francisco's frequent fog. The paint job requires constant maintenance, with a dedicated crew painting the bridge year-round to protect it from rust and corrosion.",
},
{
startTime: '8',
endTime: '11',
width: 'normal',
position: 'bottom center',
backgroundColour: 'rgba(0, 0, 0, 0.8)',
text: '#### Engineering Marvel\n\nThe Golden Gate Bridge sways up to **27 feet** sideways in strong winds and can handle winds up to 100 mph. On foggy days, the bridge can collect enough moisture to drip like rain, and it has been struck by ships only once in its history.',
},
],
},
],
};
const scrollerVideoBlock = content.blocks[0];
</script>
<div class="dummy"></div>
<ScrollerVideo
src={Goldengate}
height="200svh"
trackScroll={false}
showDebugInfo
embedded
embeddedProps={{
threshold: 0.5,
height: '80svh',
duration: 5000,
delay: 200,
}}
>
{#each scrollerVideoBlock.foregrounds as foreground}
<ScrollerVideoForeground
startTime={parseFloat(foreground.startTime)}
endTime={parseFloat(foreground.endTime)}
width={foreground.width as ContainerWidth}
position={foreground.position}
backgroundColour={foreground.backgroundColour}
text={foreground.text}
/>
{/each}
</ScrollerVideo>
<div class="dummy"></div>
<style lang="scss">
@use '../../../scss/mixins' as mixins;
.dummy {
width: 100%;
height: 80svh;
background-color: gainsboro;
margin: 10svh 0;
}
</style>