468 lines
15 KiB
Text
468 lines
15 KiB
Text
import { Meta } from '@storybook/blocks';
|
|
|
|
import * as ScrollyVideoStories from './ScrollyVideo.stories.svelte';
|
|
|
|
<Meta of={ScrollyVideoStories} />
|
|
|
|
# ScrollyVideo
|
|
|
|
The `ScrollyVideo` component is a powerful tool for creating engaging, interactive video experiences that respond to user scrolling. It allows for precise control over video playback, including the ability to sync video progress with scroll position.
|
|
|
|
The `ScrollyVideo` component is built on top of the [ScrollyVideo.js](https://scrollyvideo.js.org/), and is designed to work seamlessly with Svelte's reactivity model.
|
|
|
|
> For embedded version, toggle the `embedded` prop to **true** and provide video src to `embeddedSrc` prop. If `embeddedSrc` is not used, it will default to using the original `src` prop. If other elements are used in conjunction with the `ScrollyVideo` component, such as `ScrollerBase`, it is recommended to screen record the desktop output using [Scroll Capture](https://chromewebstore.google.com/detail/scroll-capture/egmhoeaacclmanaimofoooiamhpkimkk?hl=en) and use that video instead.
|
|
|
|
> To optimise videos for use with the `ScrollyVideo` component, it is recommended to minimise the file size and ensure that the video is encoded in a format that is widely supported across browsers. Videos encoded at higher frames per second (FPS) are bound to crash on phone devices, so it is recommended to use 24 FPS for most videos (or atleast for the phone devices). Toggle showDebugInfo to check on video encoding information.
|
|
>
|
|
> In case of doubt, fire away this command with the video file in question:
|
|
>
|
|
> ```bash
|
|
> npx ffmpeg -y -i <input_video>.mp4 -c:v libx264 -movflags faststart -crf 20 -r 24 -vf scale=720:-1 -profile:v high -preset veryslow -level:v 4.1 -color_primaries 1 -color_trc 1 -colorspace 1 -an <output_video>.mp4
|
|
> ```
|
|
>
|
|
> This will convert the video to a format that is optimised for web playback, with a resolution of 720p and a frame rate of 24 FPS. Adjust the `-crf` value to control the quality (lower values mean higher quality, with 20 being a good balance).
|
|
> Refer to the [Testing Media Capabilities](https://cconcolato.github.io/media-mime-support/mediacapabilities.html) for more details on the encoding formats to use.
|
|
|
|
## Basic usage
|
|
|
|
To use the `ScrollyVideo` component, simply import it and provide the video source. Set height prop to any valid CSS height value, such as `1200px`, `50vh`, or `500svh` to set the scrolling height.
|
|
|
|
[Demo](?path=/story/components-graphics-scrollyvideo--basic)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import { ScrollyVideo } from '@reuters-graphics/graphics-components';
|
|
</script>
|
|
|
|
<ScrollyVideo
|
|
src={`${assets}/videos/goldengate.mp4`}
|
|
trackScroll={true}
|
|
height="500svh"
|
|
{embedded}
|
|
/>
|
|
```
|
|
|
|
## Mutliple videos
|
|
|
|
It can also be used to display multiple different videos based on the viewport width. This is useful for responsive designs where your subject in the video is not in the center. After Effects can be usd to maintain a crop with a focus on the subject, and the `ScrollyVideo` component can be used to display the appropriate video based on the viewport width.
|
|
|
|
[Demo](?path=/story/components-graphics-scrollyvideo--multiple-videos)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import { ScrollyVideo } from '@reuters-graphics/graphics-components';
|
|
|
|
let width = $state(0);
|
|
</script>
|
|
|
|
<svelte:window bind:innerWidth={width} />
|
|
|
|
{#if width < 600}
|
|
<!-- Aspect ratio 9:16 -->
|
|
<ScrollyVideo
|
|
src={`${assets}/videos/video_sm.mp4`}
|
|
trackScroll={true}
|
|
height="500svh"
|
|
{embedded}
|
|
/>
|
|
{:else if width < 1200}
|
|
<!-- Aspect ratio 1:1 -->
|
|
<ScrollyVideo
|
|
src={`${assets}/videos/video_md.mp4`}
|
|
trackScroll={true}
|
|
height="500svh"
|
|
{embedded}
|
|
/>
|
|
{:else}
|
|
<!-- Aspect ratio 16:9 -->
|
|
<ScrollyVideo
|
|
src={`${assets}/videos/video_lg.mp4`}
|
|
trackScroll={true}
|
|
height="500svh"
|
|
{embedded}
|
|
/>
|
|
{/if}
|
|
```
|
|
|
|
## Autoplay
|
|
|
|
Autoplay can be enabled by setting the `autoplay` prop to `true`. This will start the video playback automatically when the component is mounted. Upon interruption by manual scroll, the video will resume playback on scroll. The height is altered to remaining portion of the video.
|
|
|
|
[Demo](?path=/story/components-graphics-scrollyvideo--autoplay)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import { ScrollyVideo } from '@reuters-graphics/graphics-components';
|
|
</script>
|
|
|
|
<ScrollyVideo
|
|
src={`${assets}/videos/goldengate.mp4`}
|
|
trackScroll={true}
|
|
autoplay={true}
|
|
height="500svh"
|
|
{embedded}
|
|
/>
|
|
```
|
|
|
|
## Inside ScrollerBase
|
|
|
|
The `ScrollyVideo` component can also be used inside the `ScrollerBase` component to create a scroller-based layout. This allows for more complex interactions and layouts, such as combining ai2svelte components with scrolly video content.
|
|
|
|
[Demo](?path=/story/components-graphics-scrollyvideo--inside-scroller-base)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import {
|
|
Headline,
|
|
GraphicBlock,
|
|
ScrollyVideo,
|
|
ScrollerBase,
|
|
} from '@reuters-graphics/graphics-components';
|
|
import AiMap from './ai2svelte/ai-chart.svelte';
|
|
|
|
// ScrollerBase props
|
|
let count = $state(1);
|
|
let index = $state(0);
|
|
let offset = $state(0);
|
|
let progress = $state(0);
|
|
let top = $state(0);
|
|
let threshold = $state(0);
|
|
let bottom = $state(1);
|
|
</script>
|
|
|
|
<div class="scroller-demo-container">
|
|
<ScrollerBase
|
|
{top}
|
|
{threshold}
|
|
{bottom}
|
|
bind:count
|
|
bind:index
|
|
bind:offset
|
|
bind:progress
|
|
query="div.step-foreground-container"
|
|
visible
|
|
>
|
|
{#snippet backgroundSnippet()}
|
|
<!-- Add custom background HTML or component -->
|
|
<div id="progress-bar">
|
|
<p>
|
|
Current step: <strong>{index + 1}/{count}</strong>
|
|
</p>
|
|
<progress value={(index + 1) / count}></progress>
|
|
|
|
<p>Offset in current step</p>
|
|
<progress value={offset}></progress>
|
|
|
|
<p>Total progress</p>
|
|
<progress value={progress}></progress>
|
|
</div>
|
|
|
|
<ScrollyVideo
|
|
src={`src/components/ScrollyVideo/videos/goldengate.mp4`}
|
|
height="100svh"
|
|
trackScroll={false}
|
|
videoPercentage={progress}
|
|
objectFit="cover"
|
|
showDebugInfo
|
|
{embedded}
|
|
/>
|
|
{/snippet}
|
|
{#snippet foregroundSnippet()}
|
|
<!-- Add custom foreground HTML or component -->
|
|
<div class="step-foreground-container">
|
|
<div class="fg-child-container">
|
|
<Headline
|
|
class="custom-headline"
|
|
hed="ScrollyVideo inside ScrollerBase"
|
|
dek="This is a demo of ScrollyVideo inside ScrollerBase component."
|
|
section={'Reuters Graphics'}
|
|
authors={['Jane Doe']}
|
|
publishTime={new Date('2020-01-01').toISOString()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="step-foreground-container"><p>Step 2</p></div>
|
|
<div class="step-foreground-container">
|
|
<div class="fg-child-container">
|
|
<GraphicBlock
|
|
title="Earthquake in Haiti"
|
|
description="The 7.2-magnitude earthquake struck at 8:29 a.m. EST, Aug. 14, 2021."
|
|
notes="Note: A shakemap represents the ground shaking produced by an earthquake."
|
|
>
|
|
<AiMap />
|
|
</GraphicBlock>
|
|
</div>
|
|
</div>
|
|
<div class="step-foreground-container"><p>Step 4</p></div>
|
|
<div class="step-foreground-container"><p>Step 5</p></div>
|
|
{/snippet}
|
|
</ScrollerBase>
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
@use '../../../scss/mixins' as mixins;
|
|
|
|
#progress-bar {
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
position: absolute;
|
|
z-index: 2;
|
|
right: 0;
|
|
padding: 1rem;
|
|
|
|
progress {
|
|
height: 6px;
|
|
background-color: #ff000044; /* Background color of the entire bar */
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
#debugger {
|
|
width: 100%;
|
|
height: 100vh;
|
|
background-color: hotpink;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
z-index: 1;
|
|
|
|
img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
.step-foreground-container {
|
|
height: 100vh;
|
|
width: 100%;
|
|
padding: 1em;
|
|
position: relative;
|
|
border: 1px solid red;
|
|
|
|
.fg-child-container {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
text-align: center;
|
|
width: 100%;
|
|
}
|
|
|
|
p {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
margin: 0;
|
|
font-size: 2em;
|
|
color: white;
|
|
font-family: var(--theme-font-family-sans-serif);
|
|
}
|
|
}
|
|
</style>
|
|
```
|
|
|
|
## Time based Foregrounds
|
|
|
|
The `ScrollyVideo` component can also be used to display different foregrounds based on the current time in the video. This is useful for creating interactive experiences where the content changes as the video progresses. To throw off some ideas, one could add `Headline`, `GraphicBlock`, `FeaturePhoto` or any other component to the foreground based on the current time in the video. This can be achieved by adding `Foreground` components, along with their **startTime** and **endTime**, as a child to the main `ScrollyVideo` component.
|
|
|
|
[Demo](?path=/story/components-graphics-scrollyvideo--time-based-foregrounds)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import { assets } from '$app/paths';
|
|
|
|
import {
|
|
Headline,
|
|
GraphicBlock,
|
|
ScrollyVideo,
|
|
Foreground,
|
|
} from '@reuters-graphics/graphics-components';
|
|
import Annotation1 from './ai2svelte/annotation1.svelte';
|
|
import Annotation2 from './ai2svelte/annotation2.svelte';
|
|
import Annotation3 from './ai2svelte/annotation3.svelte';
|
|
import Annotation4 from './ai2svelte/annotation4.svelte';
|
|
|
|
let width = $state(1);
|
|
</script>
|
|
|
|
<svelte:window bind:innerWidth={width} />
|
|
|
|
{#snippet ScrollForeground()}
|
|
<Foreground startTime={0} endTime={0.3}>
|
|
<Headline
|
|
class="custom-headline"
|
|
hed="ScrollyVideo inside ScrollerBase"
|
|
dek="This is a demo of ScrollyVideo inside ScrollerBase component."
|
|
section={'Reuters Graphics'}
|
|
authors={['Jane Doe']}
|
|
publishTime={new Date('2020-01-01').toISOString()}
|
|
/>
|
|
</Foreground>
|
|
<Foreground startTime={0.3} endTime={2.2}>
|
|
<GraphicBlock title="" description="" width="fluid">
|
|
<Annotation1 />
|
|
</GraphicBlock>
|
|
</Foreground>
|
|
<Foreground startTime={2.2} endTime={3.2}>
|
|
<GraphicBlock title="" description="" width="fluid">
|
|
<Annotation2 />
|
|
</GraphicBlock>
|
|
</Foreground>
|
|
<Foreground startTime={3.2} endTime={4.5}>
|
|
<GraphicBlock title="" description="" width="fluid">
|
|
<Annotation3 />
|
|
</GraphicBlock>
|
|
</Foreground>
|
|
<Foreground startTime={6.5} endTime={8}>
|
|
<GraphicBlock title="" description="" width="fluid">
|
|
<Annotation4 />
|
|
</GraphicBlock>
|
|
</Foreground>
|
|
{/snippet}
|
|
|
|
{#snippet ScrollVideo(height: string, VideoSrc: string)}
|
|
<ScrollyVideo {height} src={VideoSrc} useWebCodecs={true} showDebugInfo>
|
|
{@render ScrollForeground()}
|
|
</ScrollyVideo>
|
|
{/snippet}
|
|
|
|
{#if width < 600}
|
|
{@render ScrollVideo('800svh', `${assets}/videos/sm.mp4`)}
|
|
{:else if width < 1200}
|
|
{@render ScrollVideo('800svh', `${assets}/videos/md.mp4`)}
|
|
{:else}
|
|
{@render ScrollVideo('800svh', `${assets}/videos/lg.mp4`)}
|
|
{/if}
|
|
|
|
<style lang="scss">
|
|
:global(.custom-headline *) {
|
|
color: white;
|
|
}
|
|
|
|
:global(.scrolly-video-foreground) {
|
|
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.85));
|
|
}
|
|
</style>
|
|
```
|
|
|
|
## With Captions
|
|
|
|
The `ScrollyVideo` component can also be used to display captions at specific times in the video. This is useful for providing additional context or information to the viewer. To add captions, you can use the `Foreground` component and pass on the caption as **text** prop.
|
|
|
|
To quickly set it up using ArchieML, you can use the following format on your RNGS doc or Google doc:
|
|
|
|
```yaml
|
|
# ArchieML doc
|
|
[blocks]
|
|
|
|
type: scrolly-video
|
|
id: alps-scrolly
|
|
src: videos/alps.mp4
|
|
height: 800svh
|
|
|
|
# Array of foregrounds
|
|
[.foregrounds]
|
|
startTime: 3
|
|
endTime: 7
|
|
text: "#### The Alps\n\nThe Alps stretch across eight countries: France, Switzerland, Italy, Monaco, Liechtenstein, Austria, Germany, and Slovenia, covering about 1,200 kilometers (750 miles)."
|
|
:end
|
|
|
|
startTime: 9
|
|
endTime: 12
|
|
text: "Mont Blanc, standing at 4,809 meters (15,777 feet), is the highest peak in the Alps and Western Europe, though there's ongoing debate between France and Italy about exactly where the summit lies."
|
|
:end
|
|
|
|
startTime: 16
|
|
endTime: 20
|
|
text: "#### History\n\nThe Alps were formed around **65 million years** ago when the African and Eurasian tectonic plates collided, pushing the land upward.Over 14 million people live in the Alpine region, with tourism supporting approximately 120 million visitors annually."
|
|
:end
|
|
[]
|
|
:end
|
|
|
|
[]
|
|
```
|
|
|
|
Then add the following code to your App.svelte or the component where you want to display the scrolly video:
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import {
|
|
ScrollyVideo,
|
|
Foreground,
|
|
} from '@reuters-graphics/graphics-components';
|
|
import { assets } from '$app/paths';
|
|
</script>
|
|
|
|
{#if block.type == 'scrolly-video'}
|
|
<ScrollyVideo id={block.id} src={block.src} height={block.height}>
|
|
<Foreground startTime={0} endTime={2}>
|
|
<Headline
|
|
class="custom-headline"
|
|
hed={content.hed}
|
|
dek={content.dek}
|
|
section={content.section}
|
|
sectionUrl={content.sectionUrl}
|
|
authors={content.authors}
|
|
publishTime={content.publishedDate}
|
|
updateTime={content.updatedDate}
|
|
/>
|
|
</Foreground>
|
|
{#each block.foregrounds as caption}
|
|
<Foreground
|
|
startTime={parseFloat(caption.startTime)}
|
|
endTime={parseFloat(caption.endTime)}
|
|
text={caption.text}
|
|
/>
|
|
{/each}
|
|
</ScrollyVideo>
|
|
{/if}
|
|
|
|
<style lang="scss">
|
|
:global {
|
|
.custom-headline * {
|
|
color: white;
|
|
text-shadow: 0 0 10px black;
|
|
}
|
|
|
|
#alps-scrolly .foreground-text {
|
|
* {
|
|
color: white;
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
```
|
|
|
|
## 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.
|
|
|
|
```js
|
|
scrollyVideo.setVideoPercentage(0.5, {
|
|
jump: false,
|
|
transitionSpeed: 12,
|
|
easing: d3.easeLinear,
|
|
});
|
|
```
|
|
|
|
This will set the video to 50% progress, with a smooth transition at a playback rate of 12, using a linear easing function.
|