adds embedded version
This commit is contained in:
parent
cb8269e54c
commit
b3c9b1dd08
4 changed files with 226 additions and 62 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
85
src/components/ScrollerVideo/demo/Embedded.svelte
Normal file
85
src/components/ScrollerVideo/demo/Embedded.svelte
Normal 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>
|
||||
Loading…
Reference in a new issue