From b3c9b1dd0854b189794b98c0aa084d6643ae0f46 Mon Sep 17 00:00:00 2001 From: Sudev Kiyada Date: Thu, 24 Jul 2025 21:50:21 +0530 Subject: [PATCH] adds embedded version --- .../ScrollerVideo/ScrollerVideo.mdx | 24 ++- .../ScrollerVideo.stories.svelte | 6 +- .../ScrollerVideo/ScrollerVideo.svelte | 173 +++++++++++++----- .../ScrollerVideo/demo/Embedded.svelte | 85 +++++++++ 4 files changed, 226 insertions(+), 62 deletions(-) create mode 100644 src/components/ScrollerVideo/demo/Embedded.svelte diff --git a/src/components/ScrollerVideo/ScrollerVideo.mdx b/src/components/ScrollerVideo/ScrollerVideo.mdx index 802a08eb..ec070891 100644 --- a/src/components/ScrollerVideo/ScrollerVideo.mdx +++ b/src/components/ScrollerVideo/ScrollerVideo.mdx @@ -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'; - ``` ## 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) diff --git a/src/components/ScrollerVideo/ScrollerVideo.stories.svelte b/src/components/ScrollerVideo/ScrollerVideo.stories.svelte index f4f41165..08bae7a3 100644 --- a/src/components/ScrollerVideo/ScrollerVideo.stories.svelte +++ b/src/components/ScrollerVideo/ScrollerVideo.stories.svelte @@ -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 @@ - + /> --> + diff --git a/src/components/ScrollerVideo/ScrollerVideo.svelte b/src/components/ScrollerVideo/ScrollerVideo.svelte index 80f7b613..f272dae1 100644 --- a/src/components/ScrollerVideo/ScrollerVideo.svelte +++ b/src/components/ScrollerVideo/ScrollerVideo.svelte @@ -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(undefined); + let embeddedContainerHeight = $state(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 @@ }); -{#if embedded && (embeddedSrc || restProps.src)} -
- + + +{#snippet supportingElements()} + {#if scrollerVideo} + {#if showDebugInfo && dev} +
+ +
+ {/if} + + + {#if children} + {@render children()} + {/if} + {/if} +{/snippet} + +{#if embedded} +
{ + if (scrollerVideo && embeddedContainer) { + let scrollProgress = + embeddedContainer.scrollTop / embeddedContainerScrollHeight; + scrollerVideo.setVideoPercentage(scrollProgress, { + jump: scrollProgress == 0, + easing: (t) => t, + }); + } + }} + > +
+
+ {@render supportingElements()} +
+
{:else}
- {#if scrollerVideo} - {#if showDebugInfo && dev} -
- -
- {/if} - - - {#if children} - {@render children()} - {/if} - {/if} + {@render supportingElements()}
{/if} @@ -216,4 +284,9 @@ min-height: 100svh; } } + + .embedded-scroller-video-container { + max-height: 100svh; + overflow: hidden; + } diff --git a/src/components/ScrollerVideo/demo/Embedded.svelte b/src/components/ScrollerVideo/demo/Embedded.svelte new file mode 100644 index 00000000..4cc5e076 --- /dev/null +++ b/src/components/ScrollerVideo/demo/Embedded.svelte @@ -0,0 +1,85 @@ + + +
+ + + {#each scrollerVideoBlock.foregrounds as foreground} + + {/each} + + +
+ +