commit
d01a0bba9d
22 changed files with 717 additions and 741 deletions
|
|
@ -69,7 +69,7 @@ export type ScrollerVideoForegroundPosition =
|
||||||
| 'center left'
|
| 'center left'
|
||||||
| 'center right';
|
| 'center right';
|
||||||
|
|
||||||
export type ScrollerLottieForegroundPosition =
|
export type LottieForegroundPosition =
|
||||||
| 'top center'
|
| 'top center'
|
||||||
| 'top left'
|
| 'top left'
|
||||||
| 'top right'
|
| 'top right'
|
||||||
|
|
|
||||||
|
|
@ -167,8 +167,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap');
|
|
||||||
|
|
||||||
.debug-info {
|
.debug-info {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -207,7 +205,7 @@
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: 'Geist Mono', monospace;
|
font-family: var(--theme-font-family-monospace);
|
||||||
color: white;
|
color: white;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -235,7 +233,7 @@
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: var(--theme-font-size-xxs);
|
font-size: var(--theme-font-size-xxs);
|
||||||
font-family: 'Geist Mono', monospace;
|
font-family: var(--theme-font-family-monospace);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
373
src/components/Lottie/Lottie.mdx
Normal file
373
src/components/Lottie/Lottie.mdx
Normal file
|
|
@ -0,0 +1,373 @@
|
||||||
|
import { Meta } from '@storybook/blocks';
|
||||||
|
|
||||||
|
import * as LottieStories from './Lottie.stories.svelte';
|
||||||
|
import CompositionMarkerImage from './assets/marker.jpg?url';
|
||||||
|
|
||||||
|
<Meta of={LottieStories} />
|
||||||
|
|
||||||
|
# Lottie
|
||||||
|
|
||||||
|
The `Lottie` component uses the [dotLottie-web](https://developers.lottiefiles.com/docs/dotlottie-player/dotlottie-web/) library to render Lottie animations.
|
||||||
|
|
||||||
|
## How to prepare Lottie files
|
||||||
|
|
||||||
|
[LottieFiles](https://lottiefiles.com/) is the official platform for creating and editing Lottie animations. The free version of LottieFiles has limited features, so [Bodymovin](https://exchange.adobe.com/apps/cc/12557/bodymovin) remains a popular, free way to export Lottie animations as JSON files.
|
||||||
|
|
||||||
|
[dotLottie](https://dotlottie.io/) is another common format for Lottie files. This format bundles the Lottie JSON file and any associated assets, such as images and fonts, into a single compressed file with the extension `.lottie`.
|
||||||
|
|
||||||
|
This `Lottie` component is flexible and supports both `dotLottie` and JSON Lottie files. For best performance it is recommended that you convert your Lottie JSON file into a `.zip` file by following these steps:
|
||||||
|
|
||||||
|
1. Export your Lottie animation as a JSON file using [Bodymovin](https://exchange.adobe.com/apps/cc/12557/bodymovin) or another Lottie exporter.
|
||||||
|
2. Use the [LottieFiles converter](https://lottiefiles.com/tools/lottie-to-dotlottie) to convert the JSON file into a `.lottie` file.
|
||||||
|
3. Change the file extension to `.zip` from `.lottie`. This ensures full compatibility with the Reuters graphics publisher while maintaining the benefits of dotLottie format's compression and optimisation.
|
||||||
|
|
||||||
|
## Basic demo
|
||||||
|
|
||||||
|
To use the `Lottie` component, import it and provide the Lottie animation source. The height of the container defaults to `100lvh`, but you can adjust this to any valid CSS height value such as `1200px` or `200lvh` with the `height` prop.
|
||||||
|
|
||||||
|
**Use `lvh` or `svh` units instead of `vh`** as [these units](https://www.w3.org/TR/css-values-4/#large-viewport-size) are more reliable on mobile and other devices where elements such as the address bar appear and disappear and affect the height.
|
||||||
|
|
||||||
|
If importing the Lottie file directly into a Svelte component, make sure to append **?url** to the import statement (see example below). This ensures that the file is treated as a URL.
|
||||||
|
|
||||||
|
> 💡TIP: Set `showDebugInfo` prop to `true` to display information about the component state.
|
||||||
|
|
||||||
|
[Demo](?path=/story/components-graphics-scrollerlottie--demo)
|
||||||
|
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Lottie } from '@reuters-graphics/graphics-components';
|
||||||
|
|
||||||
|
// Import Lottie file
|
||||||
|
import MyLottie from './lottie/my-lottie.zip?url'; // Append **?url** to the file path
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Lottie src={MyLottie} autoplay={true} showDebugInfo={true} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using with ArchieML
|
||||||
|
|
||||||
|
If you are using `Lottie` with ArchieML, store your Lottie zip file in the `src/statics/lottie/` folder.
|
||||||
|
|
||||||
|
With the graphics kit, you'll likely get your text and prop values from an ArchieML doc...
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ArchieML doc
|
||||||
|
[blocks]
|
||||||
|
type: lottie
|
||||||
|
|
||||||
|
# Lottie file stored in `src/statics/lottie/` folder
|
||||||
|
src: lottie/LottieFile.zip
|
||||||
|
autoplay: true
|
||||||
|
loop: true
|
||||||
|
showDebugInfo: true
|
||||||
|
[]
|
||||||
|
```
|
||||||
|
|
||||||
|
... which you'll parse out of a ArchieML block object before passing to the `Lottie` component.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Lottie } from '@reuters-graphics/graphics-components';
|
||||||
|
|
||||||
|
// Graphics kit only
|
||||||
|
import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
|
||||||
|
import { truthy } from '$utils/propValidators'; // 👈 If using in the graphics kit...
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each content.blocks as block}
|
||||||
|
<!-- Inside the content.blocks for loop... -->
|
||||||
|
{#if block.type == 'lottie'}
|
||||||
|
<Lottie
|
||||||
|
src={`${assets}/${block.src}`}
|
||||||
|
autoplay={truthy(block.autoplay)}
|
||||||
|
loop={truthy(block.loop)}
|
||||||
|
showDebugInfo={truthy(block.showDebugInfo)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Playing a segment
|
||||||
|
|
||||||
|
The `Lottie` component can play a specific segment of the Lottie animation using the `segment` prop. The `segment` prop expects an array of two numbers representing the start and end frames of the segment.
|
||||||
|
|
||||||
|
[Demo](?path=/story/components-graphics-scrollerlottie--segment)
|
||||||
|
|
||||||
|
With the graphics kit, you'll likely get your text and prop values from an ArchieML doc...
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ArchieML doc
|
||||||
|
[blocks]
|
||||||
|
type: lottie
|
||||||
|
showDebugInfo: true
|
||||||
|
loop: true
|
||||||
|
|
||||||
|
# Optionally, set playback speed
|
||||||
|
speed: 0.5
|
||||||
|
|
||||||
|
# Lottie file stored in `src/statics/lottie/` folder
|
||||||
|
src: lottie/LottieFile.zip
|
||||||
|
[.segment]
|
||||||
|
start: 0
|
||||||
|
end: 20
|
||||||
|
[]
|
||||||
|
[]
|
||||||
|
```
|
||||||
|
|
||||||
|
... which you'll parse out of a ArchieML block object before passing to the `Lottie` component.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Lottie } from '@reuters-graphics/graphics-components';
|
||||||
|
|
||||||
|
// Graphics kit only
|
||||||
|
import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
|
||||||
|
import { truthy } from '$utils/propValidators'; // 👈 If using in the graphics kit...
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each content.blocks as block}
|
||||||
|
<!-- Inside the content.blocks for loop... -->
|
||||||
|
{#if block.type == 'lottie'}
|
||||||
|
<Lottie
|
||||||
|
src={`${assets}/${block.src}`}
|
||||||
|
segment={[parseInt(block.segment.start), parseInt(block.segment.end)]}
|
||||||
|
showDebugInfo={truthy(block.showDebugInfo)}
|
||||||
|
loop={truthy(block.loop)}
|
||||||
|
speed={parseInt(block.speed)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Markers
|
||||||
|
|
||||||
|
The `Lottie` component can also play a specific portion of the Lottie animation using markers set in [AfterEffects](https://helpx.adobe.com/in/after-effects/using/layer-markers-composition-markers.html).
|
||||||
|
|
||||||
|
The list of available markers, which can be passed into the `marker` prop, can be found in the debug info box that appears when `showDebugInfo` is set to `true`.
|
||||||
|
|
||||||
|
When setting markers in AfterEffects, ensure that the **Comment** section of the Composition Marker contains only the name of your marker:
|
||||||
|
|
||||||
|
<img src={CompositionMarkerImage} alt="Composition Marker Dialog" />
|
||||||
|
|
||||||
|
[Demo](?path=/story/components-graphics-scrollerlottie--marker)
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Lottie } from '@reuters-graphics/graphics-components';
|
||||||
|
|
||||||
|
// Import Lottie file
|
||||||
|
import MyLottie from './lottie/my-lottie.zip?url'; // Append **?url** to the file path
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Lottie src={MyLottie} marker="myMarker"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Switching themes
|
||||||
|
|
||||||
|
[Lottie Creator](https://lottiefiles.com/theming) allows you to define multiple colour themes for your animation. You can switch between these themes using the `theme` prop.
|
||||||
|
|
||||||
|
Available themes can be found in the debug info when the `showDebugInfo` prop is set to `true`.
|
||||||
|
|
||||||
|
You can set multiple themes and switch between them dynamically -- for example, based on the `progress` of the animation.
|
||||||
|
|
||||||
|
[Demo](?path=/story/components-graphics-scrollerlottie--themes)
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Lottie } from '@reuters-graphics/graphics-components';
|
||||||
|
import MyLottie from './lottie/my-lottie.zip?url';
|
||||||
|
|
||||||
|
// Set a bindable `progress` variable to pass into `<Lottie />`
|
||||||
|
let progress = $state(0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Lottie
|
||||||
|
src={MyLottie}
|
||||||
|
bind:progress
|
||||||
|
themeId={progress < 0.33 ? 'water'
|
||||||
|
: progress < 0.66 ? 'air'
|
||||||
|
: 'earth'}
|
||||||
|
autoplay
|
||||||
|
showDebugInfo
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using with `ScrollerBase`
|
||||||
|
|
||||||
|
The `Lottie` component can be used with the `ScrollerBase` component to create a more complex scrolling experience. `ScrollerBase` provides a scrollable container sets the `Lottie` component as a background.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Lottie, ScrollerBase } from '@reuters-graphics/graphics-components';
|
||||||
|
import MyLottie from './lottie/my-lottie.zip?url';
|
||||||
|
|
||||||
|
// Pass `progress` as `videoPercentage` to Lottie
|
||||||
|
let progress = $state(0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ScrollerBase bind:progress query="div.step-foreground-container">
|
||||||
|
{#snippet backgroundSnippet()}
|
||||||
|
<!-- Pass bindable prop `progress` as `progress` -->
|
||||||
|
<Lottie src={MyLottie} {progress} showDebugInfo />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet foregroundSnippet()}
|
||||||
|
<div class="step-foreground-container">
|
||||||
|
<h3 class="text-center">Step 1</h3>
|
||||||
|
</div>
|
||||||
|
<div class="step-foreground-container">
|
||||||
|
<h3 class="text-center">Step 2</h3>
|
||||||
|
</div>
|
||||||
|
<div class="step-foreground-container">
|
||||||
|
<h3 class="text-center">Step 3</h3>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</ScrollerBase>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.step-foreground-container {
|
||||||
|
height: 100lvh;
|
||||||
|
width: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
background-color: antiquewhite;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
## With foregrounds
|
||||||
|
|
||||||
|
The `Lottie` component can also be used with the `LottieForeground` component to display foreground elements at specific times in the animation.
|
||||||
|
|
||||||
|
[Demo](?path=/story/components-graphics-scrollerlottie--with-foregrounds).
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Lottie, ScrollerBase } from '@reuters-graphics/graphics-components';
|
||||||
|
import MyLottie from './lottie/my-lottie.zip?url';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Lottie src={MyLottie} autoplay >
|
||||||
|
|
||||||
|
<!-- Foreground 1: Headline component as foreground -->
|
||||||
|
<LottieForeground
|
||||||
|
startFrame={0}
|
||||||
|
endFrame={50}
|
||||||
|
position="center center"
|
||||||
|
backgroundColour="rgba(0, 0, 0)"
|
||||||
|
>
|
||||||
|
<div class="headline-container">
|
||||||
|
<Theme base="dark">
|
||||||
|
<Headline
|
||||||
|
hed="Headline"
|
||||||
|
dek="This is an example of using a Svelte component as the foreground."
|
||||||
|
authors={['Jane Doe', 'John Doe']}
|
||||||
|
/>
|
||||||
|
</Theme>
|
||||||
|
</div>
|
||||||
|
</LottieForeground>
|
||||||
|
|
||||||
|
<!-- Foreground 2: Text only -->
|
||||||
|
<LottieForeground
|
||||||
|
startFrame={50}
|
||||||
|
endFrame={100}
|
||||||
|
text="Foreground caption between frames 50 and 100."
|
||||||
|
position="bottom center"
|
||||||
|
backgroundColour="rgba(0, 0, 0)"
|
||||||
|
width="wide"
|
||||||
|
/>
|
||||||
|
</Lottie>
|
||||||
|
```
|
||||||
|
### Using with ArchieML
|
||||||
|
With the graphics kit, you'll likely get your text and prop values from an ArchieML doc...
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ArchieML doc
|
||||||
|
[blocks]
|
||||||
|
type: lottie
|
||||||
|
|
||||||
|
# Lottie file stored in `src/statics/lottie/` folder
|
||||||
|
src: lottie/LottieFile.zip
|
||||||
|
|
||||||
|
# Array of foregrounds
|
||||||
|
[.foregrounds]
|
||||||
|
|
||||||
|
# Foreground 1: Headline component
|
||||||
|
startFrame: 0 # When in the animation to start showing the foreground
|
||||||
|
endFrame: 50 # When to stop showing the foreground
|
||||||
|
|
||||||
|
# Set foreground type
|
||||||
|
type: component
|
||||||
|
|
||||||
|
# Set props to pass into `LottieForeground`
|
||||||
|
{.foregroundProps}
|
||||||
|
componentName: Headline
|
||||||
|
hed: Headline
|
||||||
|
dek: Some deck text
|
||||||
|
[.authors]
|
||||||
|
* Jane Doe
|
||||||
|
* John Smith
|
||||||
|
[]
|
||||||
|
{}
|
||||||
|
|
||||||
|
# Foreground 2: Text only
|
||||||
|
startFrame: 50
|
||||||
|
endFrame: 100
|
||||||
|
|
||||||
|
# Set foreground type
|
||||||
|
type: text
|
||||||
|
|
||||||
|
# If the foreground type is `text`, set text prop here
|
||||||
|
{.foregroundProps}
|
||||||
|
text: Some text for the foreground
|
||||||
|
{}
|
||||||
|
|
||||||
|
[]
|
||||||
|
```
|
||||||
|
|
||||||
|
... which you'll parse out of a ArchieML block object before passing to the `Lottie` component.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Lottie,
|
||||||
|
LottieForeground,
|
||||||
|
} from '@reuters-graphics/graphics-components';
|
||||||
|
import { assets } from '$app/paths';
|
||||||
|
|
||||||
|
// Make an object of components to use as foregrounds
|
||||||
|
const Components = $state({
|
||||||
|
Headline,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each content.blocks as block}
|
||||||
|
<!-- Inside the content.blocks for loop... -->
|
||||||
|
{#if block.type == 'lottie'}
|
||||||
|
<Lottie src={`${assets}/${block.src}`} >
|
||||||
|
{#each block.foregrounds as foreground}
|
||||||
|
{#if foreground.type == 'text'}
|
||||||
|
<LottieForeground
|
||||||
|
startFrame={parseInt(foreground.startFrame)}
|
||||||
|
endFrame={parseInt(foreground.endFrame)}
|
||||||
|
text={foreground.foregroundProps.text}
|
||||||
|
/>
|
||||||
|
{:else if foreground.type == 'component'}
|
||||||
|
{@const Component =
|
||||||
|
Components[foreground.foregroundProps.componentName]}
|
||||||
|
<LottieForeground
|
||||||
|
startFrame={parseInt(foreground.startFrame)}
|
||||||
|
endFrame={parseInt(foreground.endFrame)}
|
||||||
|
>
|
||||||
|
<Component {...foreground.foregroundProps} />
|
||||||
|
</LottieForeground>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</Lottie>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
```
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
import ScrollerLottie from './ScrollerLottie.svelte';
|
|
||||||
import ScrollerLottieForeground from './ScrollerLottieForeground.svelte';
|
// Components
|
||||||
|
import Lottie from './Lottie.svelte';
|
||||||
|
import LottieForeground from './LottieForeground.svelte';
|
||||||
import Headline from '../Headline/Headline.svelte';
|
import Headline from '../Headline/Headline.svelte';
|
||||||
import Theme from '../Theme/Theme.svelte';
|
import Theme from '../Theme/Theme.svelte';
|
||||||
|
|
||||||
import MarkerSample from './data/markerSample.lottie?url';
|
// Denmo Lottie file
|
||||||
import ForegroundSample from './data/foregroundSample.lottie?url';
|
import DemoLottie from './lottie/demo.lottie?url';
|
||||||
import ThemesSample from './data/themesLottie.lottie?url';
|
import MarkerSample from './lottie/markerSample.lottie?url';
|
||||||
|
import ForegroundSample from './lottie/foregroundSample.lottie?url';
|
||||||
|
import ThemesSample from './lottie/themesLottie.lottie?url';
|
||||||
import WithScrollerBase from './demo/withScrollerBase.svelte';
|
import WithScrollerBase from './demo/withScrollerBase.svelte';
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
const { Story } = defineMeta({
|
||||||
title: 'Components/Graphics/ScrollerLottie',
|
title: 'Components/Multimedia/Lottie',
|
||||||
component: ScrollerLottie,
|
component: Lottie,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
data: {
|
data: {
|
||||||
table: {
|
table: {
|
||||||
|
|
@ -36,12 +40,23 @@
|
||||||
let progress = $state(0);
|
let progress = $state(0);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Story name="Basic">
|
<Story name="Demo">
|
||||||
<ScrollerLottie autoplay loop showDebugInfo />
|
<Lottie src={DemoLottie} autoplay={true} showDebugInfo={true} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Segment">
|
||||||
|
<Lottie
|
||||||
|
src={DemoLottie}
|
||||||
|
autoplay
|
||||||
|
loop
|
||||||
|
showDebugInfo
|
||||||
|
segment={[0, 20]}
|
||||||
|
speed={0.5}
|
||||||
|
/>
|
||||||
</Story>
|
</Story>
|
||||||
|
|
||||||
<Story name="Marker">
|
<Story name="Marker">
|
||||||
<ScrollerLottie
|
<Lottie
|
||||||
src={MarkerSample}
|
src={MarkerSample}
|
||||||
showDebugInfo
|
showDebugInfo
|
||||||
autoplay
|
autoplay
|
||||||
|
|
@ -51,12 +66,8 @@
|
||||||
/>
|
/>
|
||||||
</Story>
|
</Story>
|
||||||
|
|
||||||
<Story name="Segment">
|
|
||||||
<ScrollerLottie autoplay loop showDebugInfo segment={[0, 20]} speed={0.5} />
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story name="Themes">
|
<Story name="Themes">
|
||||||
<ScrollerLottie
|
<Lottie
|
||||||
src={ThemesSample}
|
src={ThemesSample}
|
||||||
showDebugInfo
|
showDebugInfo
|
||||||
autoplay
|
autoplay
|
||||||
|
|
@ -72,45 +83,38 @@
|
||||||
</Story>
|
</Story>
|
||||||
|
|
||||||
<Story name="With foregrounds">
|
<Story name="With foregrounds">
|
||||||
<ScrollerLottie
|
<Lottie src={ForegroundSample} autoplay>
|
||||||
src={ForegroundSample}
|
<LottieForeground
|
||||||
showDebugInfo
|
|
||||||
autoplay
|
|
||||||
speed={0.5}
|
|
||||||
loop
|
|
||||||
mode="bounce"
|
|
||||||
>
|
|
||||||
<ScrollerLottieForeground
|
|
||||||
startFrame={50}
|
|
||||||
endFrame={100}
|
|
||||||
text="Foreground caption between frames 50 and 100."
|
|
||||||
position="bottom center"
|
|
||||||
backgroundColour="rgba(0, 0, 0)"
|
|
||||||
width="normal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ScrollerLottieForeground
|
|
||||||
startFrame={0}
|
startFrame={0}
|
||||||
endFrame={50}
|
endFrame={50}
|
||||||
position="center center"
|
position="center center"
|
||||||
backgroundColour="rgba(0, 0, 0)"
|
backgroundColour="rgba(0, 0, 0)"
|
||||||
width="normal"
|
|
||||||
>
|
>
|
||||||
<Theme base="dark">
|
<div class="headline-container">
|
||||||
<Headline
|
<Theme base="dark">
|
||||||
hed="ScrollerLottie with foreground component"
|
<Headline
|
||||||
dek="This is an example of using a Svelte component as the foreground."
|
hed="Headline"
|
||||||
width="normal"
|
dek="This is an example of using a Svelte component as the foreground."
|
||||||
authors={['Jane Doe', 'John Doe']}
|
authors={['Jane Doe', 'John Doe']}
|
||||||
/>
|
/>
|
||||||
</Theme>
|
</Theme>
|
||||||
</ScrollerLottieForeground>
|
</div>
|
||||||
</ScrollerLottie>
|
</LottieForeground>
|
||||||
|
|
||||||
|
<LottieForeground
|
||||||
|
startFrame={50}
|
||||||
|
endFrame={100}
|
||||||
|
text="Foreground caption between frames 50ms and 100ms."
|
||||||
|
position="bottom center"
|
||||||
|
backgroundColour="rgba(0, 0, 0)"
|
||||||
|
width="wide"
|
||||||
|
/>
|
||||||
|
</Lottie>
|
||||||
</Story>
|
</Story>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
:global {
|
:global {
|
||||||
.scroller-lottie-foreground {
|
.lottie-foreground-container {
|
||||||
header {
|
header {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
|
@ -123,4 +127,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headline-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,57 +1,25 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
// Libraries & utils
|
||||||
type Config,
|
|
||||||
type DotLottie as DotLottieType,
|
|
||||||
} from '@lottiefiles/dotlottie-web';
|
|
||||||
import { DotLottie } from '@lottiefiles/dotlottie-web';
|
|
||||||
import Block from '../Block/Block.svelte';
|
|
||||||
import type { ContainerWidth } from '../@types/global';
|
|
||||||
import { createLottieState, type LottieState } from './ts/lottieState.svelte';
|
|
||||||
import { onDestroy, onMount, setContext } from 'svelte';
|
import { onDestroy, onMount, setContext } from 'svelte';
|
||||||
|
import { DotLottie } from '@lottiefiles/dotlottie-web';
|
||||||
|
import { createLottieState } from './ts/lottieState.svelte';
|
||||||
import { isEqual } from 'es-toolkit';
|
import { isEqual } from 'es-toolkit';
|
||||||
import Debug from './Debug.svelte';
|
import {
|
||||||
import { map } from './ts/utils';
|
syncLottieState,
|
||||||
|
getMarkerRange,
|
||||||
|
calculateTargetFrame,
|
||||||
|
isReverseMode,
|
||||||
|
createRenderConfig,
|
||||||
|
isNullish,
|
||||||
|
} from './ts/utils';
|
||||||
import { Tween } from 'svelte/motion';
|
import { Tween } from 'svelte/motion';
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import WASM from './data/dotlottie-player.wasm?url';
|
|
||||||
import DefaultLottie from './data/defaultLottie.lottie?url';
|
|
||||||
|
|
||||||
type DotlottieProps = {
|
// Components
|
||||||
autoplay?: Config['autoplay'];
|
import Debug from './Debug.svelte';
|
||||||
backgroundColor?: Config['backgroundColor'];
|
import WASM from './lottie/dotlottie-player.wasm?url';
|
||||||
data?: Config['data'];
|
|
||||||
loop?: Config['loop'];
|
|
||||||
mode?: Config['mode'];
|
|
||||||
renderConfig?: Config['renderConfig'];
|
|
||||||
segment?: Config['segment'];
|
|
||||||
speed?: Config['speed'];
|
|
||||||
src?: Config['src'];
|
|
||||||
useFrameInterpolation?: Config['useFrameInterpolation'];
|
|
||||||
marker?: Config['marker'] | undefined;
|
|
||||||
layout?: Config['layout'];
|
|
||||||
animationId?: Config['animationId'];
|
|
||||||
themeId?: Config['themeId'];
|
|
||||||
playOnHover?: boolean;
|
|
||||||
themeData?: string;
|
|
||||||
dotLottieRefCallback?: (dotLottie: DotLottieType) => void;
|
|
||||||
onLoad?: () => void;
|
|
||||||
onRender?: () => void;
|
|
||||||
onComplete?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = DotlottieProps & {
|
// Types
|
||||||
// Additional properties can be added here if needed
|
import type { Props } from './ts/types';
|
||||||
lottiePlayer?: DotLottieType | undefined;
|
|
||||||
showDebugInfo?: boolean;
|
|
||||||
width?: ContainerWidth;
|
|
||||||
height?: string;
|
|
||||||
lottieState?: LottieState;
|
|
||||||
progress?: number;
|
|
||||||
tweenDuration?: number;
|
|
||||||
easing?: (t: number) => number;
|
|
||||||
/** Children render function */
|
|
||||||
children?: Snippet;
|
|
||||||
};
|
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
let canvasWidth: number = $state(1);
|
let canvasWidth: number = $state(1);
|
||||||
|
|
@ -66,22 +34,21 @@
|
||||||
autoplay = false,
|
autoplay = false,
|
||||||
loop = false,
|
loop = false,
|
||||||
mode = 'forward',
|
mode = 'forward',
|
||||||
src = DefaultLottie,
|
src,
|
||||||
speed = 1,
|
speed = 1,
|
||||||
data = undefined,
|
data = undefined,
|
||||||
backgroundColor = '#ffffff',
|
backgroundColor = '#ffffff',
|
||||||
segment = undefined,
|
segment,
|
||||||
renderConfig = undefined,
|
renderConfig,
|
||||||
dotLottieRefCallback = () => {},
|
dotLottieRefCallback = () => {},
|
||||||
useFrameInterpolation = true,
|
useFrameInterpolation = true,
|
||||||
themeId = '',
|
themeId = '',
|
||||||
themeData = '',
|
themeData = '',
|
||||||
playOnHover = false,
|
playOnHover = false,
|
||||||
marker = undefined,
|
marker,
|
||||||
layout = { fit: 'contain', align: [0.5, 0.5] },
|
layout = { fit: 'contain', align: [0.5, 0.5] },
|
||||||
animationId = '',
|
animationId = '',
|
||||||
lottiePlayer = $bindable(undefined),
|
lottiePlayer = $bindable(undefined),
|
||||||
width = 'widest',
|
|
||||||
height = '100lvh',
|
height = '100lvh',
|
||||||
showDebugInfo = false,
|
showDebugInfo = false,
|
||||||
lottieState = createLottieState(),
|
lottieState = createLottieState(),
|
||||||
|
|
@ -111,7 +78,7 @@
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (marker == '' || marker == null || marker == undefined) {
|
if (isNullish(marker)) {
|
||||||
start = segment ? segment[0] : 0;
|
start = segment ? segment[0] : 0;
|
||||||
end = segment ? segment[1] : lottiePlayer.totalFrames - 1;
|
end = segment ? segment[1] : lottiePlayer.totalFrames - 1;
|
||||||
}
|
}
|
||||||
|
|
@ -129,109 +96,32 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRenderEvent() {
|
function onRenderEvent() {
|
||||||
const keys = [
|
|
||||||
'currentFrame',
|
|
||||||
'totalFrames',
|
|
||||||
'duration',
|
|
||||||
'loop',
|
|
||||||
'speed',
|
|
||||||
'loopCount',
|
|
||||||
'mode',
|
|
||||||
'isPaused',
|
|
||||||
'isPlaying',
|
|
||||||
'isStopped',
|
|
||||||
'isLoaded',
|
|
||||||
'isFrozen',
|
|
||||||
'segment',
|
|
||||||
'autoplay',
|
|
||||||
'layout',
|
|
||||||
'activeThemeId',
|
|
||||||
'marker',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (lottiePlayer && lottieState) {
|
if (lottiePlayer && lottieState) {
|
||||||
keys.forEach((key) => {
|
syncLottieState(lottiePlayer, lottieState);
|
||||||
switch (key) {
|
|
||||||
case 'currentFrame':
|
|
||||||
lottieState.currentFrame = lottiePlayer!.currentFrame;
|
|
||||||
break;
|
|
||||||
case 'totalFrames':
|
|
||||||
lottieState.totalFrames = lottiePlayer!.totalFrames;
|
|
||||||
break;
|
|
||||||
case 'duration':
|
|
||||||
lottieState.duration = lottiePlayer!.duration;
|
|
||||||
break;
|
|
||||||
case 'loop':
|
|
||||||
lottieState.loop = lottiePlayer!.loop;
|
|
||||||
break;
|
|
||||||
case 'speed':
|
|
||||||
lottieState.speed = lottiePlayer!.speed;
|
|
||||||
break;
|
|
||||||
case 'loopCount':
|
|
||||||
lottieState.loopCount = lottiePlayer!.loopCount;
|
|
||||||
break;
|
|
||||||
case 'mode':
|
|
||||||
lottieState.mode = lottiePlayer!.mode;
|
|
||||||
break;
|
|
||||||
case 'isPaused':
|
|
||||||
lottieState.isPaused = lottiePlayer!.isPaused;
|
|
||||||
break;
|
|
||||||
case 'isPlaying':
|
|
||||||
lottieState.isPlaying = lottiePlayer!.isPlaying;
|
|
||||||
break;
|
|
||||||
case 'isStopped':
|
|
||||||
lottieState.isStopped = lottiePlayer!.isStopped;
|
|
||||||
break;
|
|
||||||
case 'isLoaded':
|
|
||||||
lottieState.isLoaded = lottiePlayer!.isLoaded;
|
|
||||||
break;
|
|
||||||
case 'isFrozen':
|
|
||||||
lottieState.isFrozen = lottiePlayer!.isFrozen;
|
|
||||||
break;
|
|
||||||
case 'segment':
|
|
||||||
lottieState.segment = lottiePlayer!.segment ?? null;
|
|
||||||
break;
|
|
||||||
case 'autoplay':
|
|
||||||
lottieState.autoplay = lottiePlayer!.autoplay ?? false;
|
|
||||||
break;
|
|
||||||
case 'layout':
|
|
||||||
lottieState.layout = lottiePlayer!.layout ?? null;
|
|
||||||
break;
|
|
||||||
case 'activeThemeId':
|
|
||||||
lottieState.activeThemeId = lottiePlayer!.activeThemeId ?? null;
|
|
||||||
break;
|
|
||||||
case 'marker':
|
|
||||||
lottieState.marker = lottiePlayer!.marker ?? undefined;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
progress = (lottiePlayer.currentFrame + 1) / lottiePlayer.totalFrames;
|
progress = (lottiePlayer.currentFrame + 1) / lottiePlayer.totalFrames;
|
||||||
lottieState.progress = progress;
|
lottieState.progress = progress;
|
||||||
onRender(); // call user-defined onRender function
|
onRender();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoverHandler = (event: MouseEvent) => {
|
function handleMouseEnter() {
|
||||||
if (!playOnHover || !lottiePlayer?.isLoaded) return;
|
if (playOnHover && lottiePlayer?.isLoaded) {
|
||||||
if (event.type === 'mouseenter') {
|
lottiePlayer.play();
|
||||||
lottiePlayer?.play();
|
|
||||||
} else if (event.type === 'mouseleave') {
|
|
||||||
lottiePlayer?.pause();
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
if (playOnHover && lottiePlayer?.isLoaded) {
|
||||||
|
lottiePlayer.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const shouldAutoplay = autoplay && !playOnHover;
|
const shouldAutoplay = autoplay && !playOnHover;
|
||||||
|
|
||||||
progressTween = new Tween(0, { duration: tweenDuration, easing: easing });
|
progressTween = new Tween(0, { duration: tweenDuration, easing: easing });
|
||||||
|
|
||||||
const _renderConfig = {
|
const _renderConfig = createRenderConfig();
|
||||||
autoResize: true,
|
|
||||||
devicePixelRatio:
|
|
||||||
window.devicePixelRatio > 1 ? window.devicePixelRatio * 0.75 : 1,
|
|
||||||
freezeOnOffscreen: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
lottiePlayer = new DotLottie({
|
lottiePlayer = new DotLottie({
|
||||||
canvas,
|
canvas,
|
||||||
|
|
@ -259,12 +149,7 @@
|
||||||
dotLottieRefCallback(lottiePlayer);
|
dotLottieRefCallback(lottiePlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.addEventListener('mouseenter', hoverHandler);
|
|
||||||
canvas.addEventListener('mouseleave', hoverHandler);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
canvas.removeEventListener('mouseenter', hoverHandler);
|
|
||||||
canvas.removeEventListener('mouseleave', hoverHandler);
|
|
||||||
lottiePlayer?.destroy();
|
lottiePlayer?.destroy();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -289,20 +174,11 @@
|
||||||
lottiePlayer?.unfreeze();
|
lottiePlayer?.unfreeze();
|
||||||
lottieState.isFrozen = false;
|
lottieState.isFrozen = false;
|
||||||
}
|
}
|
||||||
const targetFrame = map(
|
const targetFrame = calculateTargetFrame(progress, mode, start, end);
|
||||||
mode == 'reverse' || mode == 'reverse-bounce' ?
|
|
||||||
1 - progress
|
|
||||||
: progress,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
);
|
|
||||||
progressTween.target = targetFrame;
|
progressTween.target = targetFrame;
|
||||||
// lottiePlayer.setFrame(targetFrame);
|
// lottiePlayer.setFrame(targetFrame);
|
||||||
} else if ((progress < 0 || progress > 1) && !lottieState.isFrozen) {
|
} else if ((progress < 0 || progress > 1) && !lottieState.isFrozen) {
|
||||||
// lottiePlayer.setFrame(progress < 0 ? start : end);
|
if (isReverseMode(mode)) {
|
||||||
if (mode == 'reverse' || mode == 'reverse-bounce') {
|
|
||||||
progressTween.target = progress < 0 ? end : start;
|
progressTween.target = progress < 0 ? end : start;
|
||||||
} else {
|
} else {
|
||||||
progressTween.target = progress < 0 ? start : end;
|
progressTween.target = progress < 0 ? start : end;
|
||||||
|
|
@ -334,22 +210,11 @@
|
||||||
// Handles marker change
|
// Handles marker change
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (lottieState.isLoaded && lottiePlayer?.marker !== marker) {
|
if (lottieState.isLoaded && lottiePlayer?.marker !== marker) {
|
||||||
if (typeof marker === 'string') {
|
if (typeof marker === 'string' && lottiePlayer) {
|
||||||
lottiePlayer?.setMarker(marker);
|
lottiePlayer.setMarker(marker);
|
||||||
|
[start, end] = getMarkerRange(lottiePlayer, marker);
|
||||||
start =
|
lottieState.marker = marker;
|
||||||
lottiePlayer?.markers().find((m) => m.name === marker)?.time ?? 0;
|
} else if (isNullish(marker)) {
|
||||||
end =
|
|
||||||
start +
|
|
||||||
(lottiePlayer?.markers().find((m) => m.name === marker)?.duration ??
|
|
||||||
0);
|
|
||||||
|
|
||||||
// change lottieState marker because
|
|
||||||
// onRender fires before this
|
|
||||||
if (lottieState) {
|
|
||||||
lottieState.marker = marker;
|
|
||||||
}
|
|
||||||
} else if (marker === null || marker === undefined) {
|
|
||||||
lottiePlayer?.setMarker('');
|
lottiePlayer?.setMarker('');
|
||||||
} else {
|
} else {
|
||||||
console.warn('Invalid marker type:', marker);
|
console.warn('Invalid marker type:', marker);
|
||||||
|
|
@ -511,7 +376,7 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Block {width} class="lottie-block">
|
<div class="lottie-block">
|
||||||
{#if showDebugInfo && lottiePlayer}
|
{#if showDebugInfo && lottiePlayer}
|
||||||
<Debug componentState={lottieState} />
|
<Debug componentState={lottieState} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -521,13 +386,15 @@
|
||||||
bind:this={canvas}
|
bind:this={canvas}
|
||||||
bind:clientWidth={canvasWidth}
|
bind:clientWidth={canvasWidth}
|
||||||
bind:clientHeight={canvasHeight}
|
bind:clientHeight={canvasHeight}
|
||||||
|
onmouseenter={handleMouseEnter}
|
||||||
|
onmouseleave={handleMouseLeave}
|
||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if children}
|
{#if children}
|
||||||
{@render children?.()}
|
{@render children()}
|
||||||
{/if}
|
{/if}
|
||||||
</Block>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
:global(.lottie-block) {
|
:global(.lottie-block) {
|
||||||
|
|
@ -5,11 +5,11 @@
|
||||||
import { Markdown } from '@reuters-graphics/svelte-markdown';
|
import { Markdown } from '@reuters-graphics/svelte-markdown';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import type { Component, Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { LottieState } from './ts/lottieState.svelte';
|
import type { LottieState } from './ts/lottieState.svelte';
|
||||||
import type {
|
import type {
|
||||||
ContainerWidth,
|
ContainerWidth,
|
||||||
ScrollerLottieForegroundPosition,
|
LottieForegroundPosition,
|
||||||
} from '../@types/global';
|
} from '../@types/global';
|
||||||
|
|
||||||
interface ForegroundProps {
|
interface ForegroundProps {
|
||||||
|
|
@ -20,14 +20,13 @@
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
backgroundColour?: string;
|
backgroundColour?: string;
|
||||||
width?: ContainerWidth;
|
width?: ContainerWidth;
|
||||||
position?: ScrollerLottieForegroundPosition | string;
|
position?: LottieForegroundPosition | string;
|
||||||
text?: string;
|
text?: string;
|
||||||
Foreground?: Component;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
id = '',
|
id,
|
||||||
class: cls = '',
|
class: cls,
|
||||||
startFrame = 0,
|
startFrame = 0,
|
||||||
endFrame = 10,
|
endFrame = 10,
|
||||||
children,
|
children,
|
||||||
|
|
@ -35,7 +34,6 @@
|
||||||
width = 'normal',
|
width = 'normal',
|
||||||
position = 'center center',
|
position = 'center center',
|
||||||
text,
|
text,
|
||||||
Foreground,
|
|
||||||
}: ForegroundProps = $props();
|
}: ForegroundProps = $props();
|
||||||
|
|
||||||
let componentState: LottieState | null = $state(getContext('lottieState'));
|
let componentState: LottieState | null = $state(getContext('lottieState'));
|
||||||
|
|
@ -45,19 +43,16 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Block class={`scroller-lottie-foreground ${cls}`} {id}>
|
<div class="lottie-foreground-container {cls}" {id}>
|
||||||
{#if componentState?.currentFrame && componentState.currentFrame >= startFrame && componentState.currentFrame <= endFrame}
|
{#if componentState?.currentFrame && componentState.currentFrame >= startFrame && componentState.currentFrame <= endFrame}
|
||||||
<div
|
<div
|
||||||
class="scroller-foreground"
|
class="lottie-foreground"
|
||||||
in:fade={{ delay: 100, duration: 200 }}
|
in:fade={{ delay: 100, duration: 200 }}
|
||||||
out:fade={{ delay: 0, duration: 100 }}
|
out:fade={{ delay: 0, duration: 100 }}
|
||||||
>
|
>
|
||||||
<!-- Text blurb foreground -->
|
<!-- Text blurb foreground -->
|
||||||
{#if text}
|
{#if text}
|
||||||
<Block
|
<Block class="lottie-foreground-block {position.split(' ')[1]}" {width}>
|
||||||
class="scroller-lottie-foreground-text {position.split(' ')[1]}"
|
|
||||||
{width}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style="background-color: {backgroundColour};"
|
style="background-color: {backgroundColour};"
|
||||||
class="foreground-text {position.split(' ')[0]}"
|
class="foreground-text {position.split(' ')[0]}"
|
||||||
|
|
@ -67,60 +62,38 @@
|
||||||
</Block>
|
</Block>
|
||||||
<!-- Render children snippet -->
|
<!-- Render children snippet -->
|
||||||
{:else if children}
|
{:else if children}
|
||||||
<div class="scroller-lottie-foreground-item">
|
{@render children()}
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
<!-- Render Foreground component -->
|
|
||||||
{:else if Foreground}
|
|
||||||
<div class="scroller-lottie-foreground-item">
|
|
||||||
<Block width="fluid">
|
|
||||||
<Foreground />
|
|
||||||
</Block>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Block>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use './../../scss/mixins' as mixins;
|
@use '../../scss/mixins' as mixins;
|
||||||
|
|
||||||
.scroller-foreground {
|
.lottie-foreground-container {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.scroller-lottie-foreground) {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
inset: 0;
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
|
||||||
|
|
||||||
.scroller-lottie-foreground-item {
|
.lottie-foreground {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
}
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global {
|
:global {
|
||||||
.scroller-lottie-foreground-text {
|
.lottie-foreground-block {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: calc(mixins.$column-width-normal * 0.9);
|
max-width: calc(mixins.$column-width-normal * 0.9);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
&.center {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
@ -128,10 +101,12 @@
|
||||||
&.left {
|
&.left {
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.right {
|
&.right {
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
&.center {
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
@ -139,28 +114,28 @@
|
||||||
|
|
||||||
.foreground-text {
|
.foreground-text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
background-color: white;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-radius: 0.25rem;
|
||||||
@include mixins.fpy-5;
|
@include mixins.fpy-5;
|
||||||
@include mixins.fpx-4;
|
@include mixins.fpx-4;
|
||||||
@include mixins.fm-0;
|
@include mixins.fm-0;
|
||||||
|
|
||||||
:global(*) {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.center {
|
&.center {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.top {
|
&.top {
|
||||||
top: 0;
|
top: 0;
|
||||||
transform: translate(-50%, 50%);
|
transform: translate(-50%, 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bottom {
|
&.bottom {
|
||||||
top: 100%;
|
top: 100%;
|
||||||
transform: translate(-50%, -150%);
|
transform: translate(-50%, -150%);
|
||||||
BIN
src/components/Lottie/assets/marker.jpg
Normal file
BIN
src/components/Lottie/assets/marker.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
51
src/components/Lottie/demo/withScrollerBase.svelte
Normal file
51
src/components/Lottie/demo/withScrollerBase.svelte
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import ScrollerBase from '../../ScrollerBase/ScrollerBase.svelte';
|
||||||
|
import Lottie from '../Lottie.svelte';
|
||||||
|
import LottieSample from '../lottie/themesLottie.lottie?url';
|
||||||
|
|
||||||
|
// Optional: Bind your own variables to use them in your code.
|
||||||
|
let count = $state(1);
|
||||||
|
let index = $state(0);
|
||||||
|
let offset = $state(0);
|
||||||
|
let progress = $state(0);
|
||||||
|
let top = $state(0);
|
||||||
|
let threshold = $state(0.5);
|
||||||
|
let bottom = $state(1);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ScrollerBase
|
||||||
|
{top}
|
||||||
|
{threshold}
|
||||||
|
{bottom}
|
||||||
|
bind:count
|
||||||
|
bind:index
|
||||||
|
bind:offset
|
||||||
|
bind:progress
|
||||||
|
query="div.step-foreground-container"
|
||||||
|
>
|
||||||
|
{#snippet backgroundSnippet()}
|
||||||
|
<Lottie src={LottieSample} {progress} showDebugInfo />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet foregroundSnippet()}
|
||||||
|
<div class="step-foreground-container"><h3>Step 1</h3></div>
|
||||||
|
<div class="step-foreground-container"><h3>Step 2</h3></div>
|
||||||
|
<div class="step-foreground-container"><h3>Step 3</h3></div>
|
||||||
|
<div class="step-foreground-container"><h3>Step 4</h3></div>
|
||||||
|
<div class="step-foreground-container"><h3>Step 5</h3></div>
|
||||||
|
{/snippet}
|
||||||
|
</ScrollerBase>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '../../../scss/mixins' as mixins;
|
||||||
|
|
||||||
|
.step-foreground-container {
|
||||||
|
height: 100lvh;
|
||||||
|
width: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
background-color: antiquewhite;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
44
src/components/Lottie/ts/types.ts
Normal file
44
src/components/Lottie/ts/types.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import {
|
||||||
|
type Config,
|
||||||
|
type DotLottie as DotLottieType,
|
||||||
|
} from '@lottiefiles/dotlottie-web';
|
||||||
|
import { type LottieState } from './lottieState.svelte';
|
||||||
|
|
||||||
|
type DotlottieProps = {
|
||||||
|
autoplay?: Config['autoplay'];
|
||||||
|
backgroundColor?: Config['backgroundColor'];
|
||||||
|
data?: Config['data'];
|
||||||
|
loop?: Config['loop'];
|
||||||
|
mode?: Config['mode'];
|
||||||
|
renderConfig?: Config['renderConfig'];
|
||||||
|
segment?: Config['segment'];
|
||||||
|
speed?: Config['speed'];
|
||||||
|
src: Config['src'];
|
||||||
|
useFrameInterpolation?: Config['useFrameInterpolation'];
|
||||||
|
marker?: Config['marker'] | undefined;
|
||||||
|
layout?: Config['layout'];
|
||||||
|
animationId?: Config['animationId'];
|
||||||
|
themeId?: Config['themeId'];
|
||||||
|
playOnHover?: boolean;
|
||||||
|
themeData?: string;
|
||||||
|
dotLottieRefCallback?: (dotLottie: DotLottieType) => void;
|
||||||
|
onLoad?: () => void;
|
||||||
|
onRender?: () => void;
|
||||||
|
onComplete?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = DotlottieProps & {
|
||||||
|
// Additional properties can be added here if needed
|
||||||
|
lottiePlayer?: DotLottieType | undefined;
|
||||||
|
showDebugInfo?: boolean;
|
||||||
|
height?: string;
|
||||||
|
lottieState?: LottieState;
|
||||||
|
progress?: number;
|
||||||
|
tweenDuration?: number;
|
||||||
|
easing?: (t: number) => number;
|
||||||
|
/** Children render function */
|
||||||
|
children?: Snippet;
|
||||||
|
};
|
||||||
111
src/components/Lottie/ts/utils.ts
Normal file
111
src/components/Lottie/ts/utils.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import type { DotLottie } from '@lottiefiles/dotlottie-web';
|
||||||
|
import type { LottieState } from './lottieState.svelte';
|
||||||
|
|
||||||
|
function constrain(n: number, low: number, high: number) {
|
||||||
|
return Math.max(Math.min(n, high), low);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function map(
|
||||||
|
n: number,
|
||||||
|
start1: number,
|
||||||
|
stop1: number,
|
||||||
|
start2: number,
|
||||||
|
stop2: number,
|
||||||
|
withinBounds: boolean = true
|
||||||
|
) {
|
||||||
|
const newval = ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2;
|
||||||
|
if (!withinBounds) {
|
||||||
|
return newval;
|
||||||
|
}
|
||||||
|
if (start2 < stop2) {
|
||||||
|
return constrain(newval, start2, stop2);
|
||||||
|
} else {
|
||||||
|
return constrain(newval, stop2, start2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs the lottie player state with the component's lottie state
|
||||||
|
*/
|
||||||
|
export function syncLottieState(
|
||||||
|
lottiePlayer: DotLottie,
|
||||||
|
lottieState: LottieState
|
||||||
|
) {
|
||||||
|
lottieState.currentFrame = lottiePlayer.currentFrame;
|
||||||
|
lottieState.totalFrames = lottiePlayer.totalFrames;
|
||||||
|
lottieState.duration = lottiePlayer.duration;
|
||||||
|
lottieState.loop = lottiePlayer.loop;
|
||||||
|
lottieState.speed = lottiePlayer.speed;
|
||||||
|
lottieState.loopCount = lottiePlayer.loopCount;
|
||||||
|
lottieState.mode = lottiePlayer.mode;
|
||||||
|
lottieState.isPaused = lottiePlayer.isPaused;
|
||||||
|
lottieState.isPlaying = lottiePlayer.isPlaying;
|
||||||
|
lottieState.isStopped = lottiePlayer.isStopped;
|
||||||
|
lottieState.isLoaded = lottiePlayer.isLoaded;
|
||||||
|
lottieState.isFrozen = lottiePlayer.isFrozen;
|
||||||
|
lottieState.segment = lottiePlayer.segment ?? null;
|
||||||
|
lottieState.autoplay = lottiePlayer.autoplay ?? false;
|
||||||
|
lottieState.layout = lottiePlayer.layout ?? null;
|
||||||
|
lottieState.activeThemeId = lottiePlayer.activeThemeId ?? null;
|
||||||
|
lottieState.marker = lottiePlayer.marker ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets marker info by name
|
||||||
|
*/
|
||||||
|
export function getMarkerByName(lottiePlayer: DotLottie, markerName: string) {
|
||||||
|
return lottiePlayer.markers().find((m) => m.name === markerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the start and end frames for a marker
|
||||||
|
*/
|
||||||
|
export function getMarkerRange(
|
||||||
|
lottiePlayer: DotLottie,
|
||||||
|
markerName: string
|
||||||
|
): [number, number] {
|
||||||
|
const marker = getMarkerByName(lottiePlayer, markerName);
|
||||||
|
const start = marker?.time ?? 0;
|
||||||
|
const end = start + (marker?.duration ?? 0);
|
||||||
|
return [start, end];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates target frame based on progress and mode
|
||||||
|
*/
|
||||||
|
export function calculateTargetFrame(
|
||||||
|
progress: number,
|
||||||
|
mode: string,
|
||||||
|
start: number,
|
||||||
|
end: number
|
||||||
|
): number {
|
||||||
|
const adjustedProgress =
|
||||||
|
mode === 'reverse' || mode === 'reverse-bounce' ? 1 - progress : progress;
|
||||||
|
return map(adjustedProgress, 0, 1, start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if mode is reverse
|
||||||
|
*/
|
||||||
|
export function isReverseMode(mode: string): boolean {
|
||||||
|
return mode === 'reverse' || mode === 'reverse-bounce';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates render config with optimized defaults
|
||||||
|
*/
|
||||||
|
export function createRenderConfig() {
|
||||||
|
return {
|
||||||
|
autoResize: true,
|
||||||
|
devicePixelRatio:
|
||||||
|
window.devicePixelRatio > 1 ? window.devicePixelRatio * 0.75 : 1,
|
||||||
|
freezeOnOffscreen: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a value is null or undefined (empty marker check)
|
||||||
|
*/
|
||||||
|
export function isNullish(value: any): boolean {
|
||||||
|
return value === null || value === undefined || value === '';
|
||||||
|
}
|
||||||
|
|
@ -1,363 +0,0 @@
|
||||||
import { Meta } from '@storybook/blocks';
|
|
||||||
|
|
||||||
import * as ScrollerLottieStories from './ScrollerLottie.stories.svelte';
|
|
||||||
import CompositionMarkerImage from './assets/marker.png?url';
|
|
||||||
|
|
||||||
<Meta of={ScrollerLottieStories} />
|
|
||||||
|
|
||||||
# ScrollerLottie
|
|
||||||
|
|
||||||
The `ScrollerLottie` component plays Lottie animations. It uses the [dotLottie-web](https://developers.lottiefiles.com/docs/dotlottie-player/dotlottie-web/) library to render the animations.
|
|
||||||
|
|
||||||
## How to use .lottie files
|
|
||||||
|
|
||||||
LottieFiles is the official platform for creating and editing Lottie animations, and exporting them in the dotLottie format for smaller file sizes. The free version of LottieFiles has limited features, so [Bodymovin](https://exchange.adobe.com/apps/cc/12557/bodymovin) remains a popular, free way to export animations as JSON files. You can use the [LottieFiles converter](https://lottiefiles.com/tools/lottie-to-dotlottie) to convert JSON files to dotLottie or optimized dotLottie formats. This component is flexible and supports both dotLottie and JSON animation files.
|
|
||||||
|
|
||||||
> 🚧NOTE: For optimal compatibility with graphics-publisher, export your JSON files as optimized dotLottie format and rename the file extension to `*.zip`. This approach ensures full publisher support while maintaining the benefits of the dotLottie format's compression and optimization.
|
|
||||||
|
|
||||||
## Basic demo
|
|
||||||
|
|
||||||
To use the `ScrollerLottie` component, import it and provide the animation source. The height defaults to `100lvh`, but you can adjust this to any valid CSS height value such as `1200px` or `200lvh` with the `height` prop.
|
|
||||||
|
|
||||||
The .lottie or .json file should be placed at the same level as the component file. If using it inside `App.svelte`, create a `data` folder and place all the animation files inside. Make sure to append **?url** to the import statement when importing an animation file, as shown in the example below. This ensures that the file is treated as a URL.
|
|
||||||
|
|
||||||
> 💡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.
|
|
||||||
|
|
||||||
> 💡TIP: Use showDebugInfo prop to display additional information about the component state.
|
|
||||||
|
|
||||||
[Demo](?path=/story/components-graphics-scrollerlottie--basic)
|
|
||||||
|
|
||||||
With the graphics kit, you'll likely get your text and prop values from an ArchieML doc...
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ArchieML doc
|
|
||||||
[blocks]
|
|
||||||
type: lottie
|
|
||||||
src: LottieFile.zip
|
|
||||||
:end
|
|
||||||
[]
|
|
||||||
```
|
|
||||||
|
|
||||||
... which you'll parse out of a ArchieML block object before passing to the `ScrollerLottie` component.
|
|
||||||
|
|
||||||
```svelte
|
|
||||||
<script lang="ts">
|
|
||||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
|
||||||
import { assets } from '$app/paths';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#each content.blocks as block}
|
|
||||||
<!-- Inside the content.blocks for loop... -->
|
|
||||||
{#if block.type == 'lottie'}
|
|
||||||
<ScrollerLottie
|
|
||||||
src={`${assets}/animations/${block.src}`}
|
|
||||||
autoplay
|
|
||||||
loop
|
|
||||||
showDebugInfo
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Playing a marker
|
|
||||||
|
|
||||||
It is possible to play a specific portion of the animation using markers. Markers can be set in [AfterEffects](https://helpx.adobe.com/in/after-effects/using/layer-markers-composition-markers.html) to define separate portions of the animation. A specific marker can be played by using the `marker` prop.
|
|
||||||
|
|
||||||
The list of available markers can be found in the debug info when `showDebugInfo` prop is enabled.
|
|
||||||
|
|
||||||
> 💡NOTE: The **Comment** section of the Composition Marker dialog should only contain the name of your marker.
|
|
||||||
|
|
||||||
<img src={CompositionMarkerImage} alt="Composition Marker Dialog" />
|
|
||||||
|
|
||||||
[Demo](?path=/story/components-graphics-scrollerlottie--marker)
|
|
||||||
|
|
||||||
With the graphics kit, you'll likely get your text and prop values from an ArchieML doc...
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ArchieML doc
|
|
||||||
[blocks]
|
|
||||||
type: lottie
|
|
||||||
src: LottieFile.zip
|
|
||||||
marker: myMarker
|
|
||||||
:end
|
|
||||||
[]
|
|
||||||
```
|
|
||||||
|
|
||||||
... which you'll parse out of a ArchieML block object before passing to the `ScrollerLottie` component.
|
|
||||||
|
|
||||||
```svelte
|
|
||||||
<script lang="ts">
|
|
||||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
|
||||||
import { assets } from '$app/paths';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#each content.blocks as block}
|
|
||||||
<!-- Inside the content.blocks for loop... -->
|
|
||||||
{#if block.type == 'lottie'}
|
|
||||||
<ScrollerLottie
|
|
||||||
src={`${assets}/animations/${block.src}`}
|
|
||||||
marker={block.marker}
|
|
||||||
autoplay
|
|
||||||
loop
|
|
||||||
showDebugInfo
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Playing a segment
|
|
||||||
|
|
||||||
Just like markers, it is also possible to play a specific segment of the animation using the `segment` prop. The `segment` prop expects an array of two numbers representing the start and end frames of the segment.
|
|
||||||
|
|
||||||
[Demo](?path=/story/components-graphics-scrollerlottie--segment)
|
|
||||||
|
|
||||||
With the graphics kit, you'll likely get your text and prop values from an ArchieML doc...
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ArchieML doc
|
|
||||||
[blocks]
|
|
||||||
type: lottie
|
|
||||||
src: LottieFile.zip
|
|
||||||
[.segment]
|
|
||||||
start: 0
|
|
||||||
end: 20
|
|
||||||
[]
|
|
||||||
:end
|
|
||||||
[]
|
|
||||||
```
|
|
||||||
|
|
||||||
... which you'll parse out of a ArchieML block object before passing to the `ScrollerLottie` component.
|
|
||||||
|
|
||||||
```svelte
|
|
||||||
<script lang="ts">
|
|
||||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
|
||||||
import { assets } from '$app/paths';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#each content.blocks as block}
|
|
||||||
<!-- Inside the content.blocks for loop... -->
|
|
||||||
{#if block.type == 'lottie'}
|
|
||||||
<ScrollerLottie
|
|
||||||
src={`${assets}/animations/${block.src}`}
|
|
||||||
segment={[block.segment.start, block.segment.end]}
|
|
||||||
autoplay
|
|
||||||
loop
|
|
||||||
showDebugInfo
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Switching themes
|
|
||||||
|
|
||||||
[Lottie Creator](https://lottiefiles.com/theming) allows you to define multiple color themes for your animation. You can switch between these themes using the `theme` prop.
|
|
||||||
|
|
||||||
Available themes can be found in the debug info when `showDebugInfo` prop is enabled.
|
|
||||||
|
|
||||||
With the graphics kit, you'll likely get your text and prop values from an ArchieML doc...
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ArchieML doc
|
|
||||||
[blocks]
|
|
||||||
type: lottie
|
|
||||||
src: LottieFile.zip
|
|
||||||
theme: myTheme
|
|
||||||
:end
|
|
||||||
[]
|
|
||||||
```
|
|
||||||
|
|
||||||
... which you'll parse out of a ArchieML block object before passing to the `ScrollerLottie` component.
|
|
||||||
|
|
||||||
```svelte
|
|
||||||
<script lang="ts">
|
|
||||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
|
||||||
import { assets } from '$app/paths';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#each content.blocks as block}
|
|
||||||
<!-- Inside the content.blocks for loop... -->
|
|
||||||
{#if block.type == 'lottie'}
|
|
||||||
<ScrollerLottie
|
|
||||||
src={`${assets}/animations/${block.src}`}
|
|
||||||
theme={block.theme}
|
|
||||||
autoplay
|
|
||||||
loop
|
|
||||||
showDebugInfo
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
```
|
|
||||||
|
|
||||||
It is also possible to switch themes dynamically based on the `progress` prop by binding a variable to it.
|
|
||||||
|
|
||||||
[Demo](?path=/story/components-graphics-scrollerlottie--themes)
|
|
||||||
|
|
||||||
```svelte
|
|
||||||
<script lang="ts">
|
|
||||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
|
||||||
// make a folder named 'data' and place the .zip lottie file inside it
|
|
||||||
// append ?url to the import statement
|
|
||||||
import LottieSrc from './data/lottie-example.zip?url';
|
|
||||||
|
|
||||||
let progress = $state(0);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ScrollerLottie
|
|
||||||
src={LottieSrc}
|
|
||||||
bind:progress
|
|
||||||
themeId={progress < 0.33 ? 'water'
|
|
||||||
: progress < 0.66 ? 'air'
|
|
||||||
: 'earth'}
|
|
||||||
autoplay
|
|
||||||
showDebugInfo
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
## With ScrollerBase
|
|
||||||
|
|
||||||
The `ScrollerLottie` component can be used in conjunction with the `ScrollerBase` component to create a more complex scrolling experience. The `ScrollerBase` component provides a scrollable container that can hold the `ScrollerLottie` component as a background.
|
|
||||||
|
|
||||||
```svelte
|
|
||||||
<script lang="ts">
|
|
||||||
import { ScrollerLottie } from '@reuters-graphics/graphics-components';
|
|
||||||
// make a folder named 'data' and place the .zip lottie file inside it
|
|
||||||
// append ?url to the import statement
|
|
||||||
import LottieSrc from './data/lottie-example.zip?url';
|
|
||||||
|
|
||||||
// Pass `progress` as `videoPercentage` to ScrollerLottie
|
|
||||||
let progress = $state(0);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ScrollerBase bind:progress query="div.step-foreground-container">
|
|
||||||
{#snippet backgroundSnippet()}
|
|
||||||
<!-- Pass bindable prop `progress` as `progress` -->
|
|
||||||
<div class="lottie-container">
|
|
||||||
<ScrollerLottie src={LottieSrc} {progress} showDebugInfo />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet foregroundSnippet()}
|
|
||||||
<!-- Add custom foreground HTML or component -->
|
|
||||||
<div class="step-foreground-container">
|
|
||||||
<h3 class="text-center">Step 1</h3>
|
|
||||||
</div>
|
|
||||||
<div class="step-foreground-container">
|
|
||||||
<h3 class="text-center">Step 2</h3>
|
|
||||||
</div>
|
|
||||||
<div class="step-foreground-container">
|
|
||||||
<h3 class="text-center">Step 3</h3>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</ScrollerBase>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.lottie-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100lvh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-foreground-container {
|
|
||||||
height: 100lvh;
|
|
||||||
width: 50%;
|
|
||||||
padding: 1em;
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
## With foregrounds
|
|
||||||
|
|
||||||
The `ScrollerLottie` component can also be used to display captions or even components, such as `Headline` or ai2svelte files, as foregrounds at specific times in the animation. To do so, add ScrollerLottieForeground components as children of the ScrollerLottie component.
|
|
||||||
|
|
||||||
[Demo](?path=/story/components-graphics-scrollerlottie--with-foregrounds)
|
|
||||||
|
|
||||||
With the graphics kit, you'll likely get your text and prop values from an ArchieML doc...
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ArchieML doc
|
|
||||||
[blocks]
|
|
||||||
type: lottie
|
|
||||||
src: LottieFile.zip
|
|
||||||
|
|
||||||
# Array of foregrounds
|
|
||||||
[.foregrounds]
|
|
||||||
startFrame: 0 # When in the animation to start showing the foreground
|
|
||||||
endFrame: 50 # When to stop showing the foreground
|
|
||||||
type: text
|
|
||||||
{.foregroundProps}
|
|
||||||
text: Some text for the foreground
|
|
||||||
{}
|
|
||||||
|
|
||||||
startFrame: 50 # When in the animation to start showing the foreground
|
|
||||||
endFrame: 100 # When to stop showing the foreground
|
|
||||||
type: component
|
|
||||||
{.foregroundProps}
|
|
||||||
componentType: Headline
|
|
||||||
hed: Some headline text
|
|
||||||
dek: Some deck text
|
|
||||||
[.authors]
|
|
||||||
* Jane Doe
|
|
||||||
* John Smith
|
|
||||||
[]
|
|
||||||
{}
|
|
||||||
[]
|
|
||||||
:end
|
|
||||||
|
|
||||||
[]
|
|
||||||
```
|
|
||||||
|
|
||||||
... which you'll parse out of a ArchieML block object before passing to the `ScrollerLottie` component.
|
|
||||||
|
|
||||||
```svelte
|
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
ScrollerLottie,
|
|
||||||
ScrollerLottieForeground,
|
|
||||||
} from '@reuters-graphics/graphics-components';
|
|
||||||
import { assets } from '$app/paths';
|
|
||||||
|
|
||||||
const Components = $state({
|
|
||||||
Headline,
|
|
||||||
Video,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#each content.blocks as block}
|
|
||||||
<!-- Inside the content.blocks for loop... -->
|
|
||||||
{#if block.type == 'lottie'}
|
|
||||||
<ScrollerLottie
|
|
||||||
src={`${assets}/animations/${block.src}`}
|
|
||||||
theme={block.theme}
|
|
||||||
autoplay
|
|
||||||
loop
|
|
||||||
showDebugInfo
|
|
||||||
>
|
|
||||||
{#each block.foregrounds as foreground}
|
|
||||||
{#if foreground.type == 'text'}
|
|
||||||
<ScrollerLottieForeground
|
|
||||||
endFrame={parseInt(foreground.endFrame)}
|
|
||||||
startFrame={parseInt(foreground.startFrame)}
|
|
||||||
text={foreground.foregroundProps.text}
|
|
||||||
/>
|
|
||||||
{:else if foreground.type == 'component'}
|
|
||||||
{@const Component =
|
|
||||||
Components[foreground.foregroundProps.componentType]}
|
|
||||||
<ScrollerLottieForeground
|
|
||||||
endFrame={parseInt(foreground.endFrame)}
|
|
||||||
startFrame={parseInt(foreground.startFrame)}
|
|
||||||
>
|
|
||||||
<Component {...foreground.foregroundProps} />
|
|
||||||
</ScrollerLottieForeground>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</ScrollerLottie>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
```
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 210 KiB |
|
|
@ -1,69 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import ScrollerBase from '../../ScrollerBase/ScrollerBase.svelte';
|
|
||||||
import ScrollerLottie from '../ScrollerLottie.svelte';
|
|
||||||
import LottieSample from '../data/themesLottie.lottie?url';
|
|
||||||
|
|
||||||
// Optional: Bind your own variables to use them in your code.
|
|
||||||
let count = $state(1);
|
|
||||||
let index = $state(0);
|
|
||||||
let offset = $state(0);
|
|
||||||
let progress = $state(0);
|
|
||||||
let top = $state(0);
|
|
||||||
let threshold = $state(0.5);
|
|
||||||
let bottom = $state(1);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ScrollerBase
|
|
||||||
{top}
|
|
||||||
{threshold}
|
|
||||||
{bottom}
|
|
||||||
bind:count
|
|
||||||
bind:index
|
|
||||||
bind:offset
|
|
||||||
bind:progress
|
|
||||||
query="div.step-foreground-container"
|
|
||||||
>
|
|
||||||
{#snippet backgroundSnippet()}
|
|
||||||
<!-- Add custom background HTML or component -->
|
|
||||||
<div class="lottie-container">
|
|
||||||
<ScrollerLottie src={LottieSample} showDebugInfo {progress} />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet foregroundSnippet()}
|
|
||||||
<!-- Add custom foreground HTML or component -->
|
|
||||||
<div class="step-foreground-container"><p>Step 1</p></div>
|
|
||||||
<div class="step-foreground-container"><p>Step 2</p></div>
|
|
||||||
<div class="step-foreground-container"><p>Step 3</p></div>
|
|
||||||
<div class="step-foreground-container"><p>Step 4</p></div>
|
|
||||||
<div class="step-foreground-container"><p>Step 5</p></div>
|
|
||||||
{/snippet}
|
|
||||||
</ScrollerBase>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use '../../../scss/mixins' as mixins;
|
|
||||||
|
|
||||||
.lottie-container {
|
|
||||||
// border: 2px solid red;
|
|
||||||
width: 100%;
|
|
||||||
height: 100lvh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-foreground-container {
|
|
||||||
height: 100lvh;
|
|
||||||
width: 50%;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
padding: 1em;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
|
|
||||||
p {
|
|
||||||
position: relative;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
background-color: antiquewhite;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
function constrain(n: number, low: number, high: number) {
|
|
||||||
return Math.max(Math.min(n, high), low);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function map(
|
|
||||||
n: number,
|
|
||||||
start1: number,
|
|
||||||
stop1: number,
|
|
||||||
start2: number,
|
|
||||||
stop2: number,
|
|
||||||
withinBounds: boolean = true
|
|
||||||
) {
|
|
||||||
const newval = ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2;
|
|
||||||
if (!withinBounds) {
|
|
||||||
return newval;
|
|
||||||
}
|
|
||||||
if (start2 < stop2) {
|
|
||||||
return constrain(newval, start2, stop2);
|
|
||||||
} else {
|
|
||||||
return constrain(newval, stop2, start2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -120,8 +120,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap');
|
|
||||||
|
|
||||||
.debug-info {
|
.debug-info {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -160,7 +158,7 @@
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: 'Geist Mono', monospace;
|
font-family: var(--theme-font-family-monospace);
|
||||||
color: white;
|
color: white;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +186,7 @@
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: var(--theme-font-size-xxs);
|
font-size: var(--theme-font-size-xxs);
|
||||||
font-family: 'Geist Mono', monospace;
|
font-family: var(--theme-font-family-monospace);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ export { default as Spinner } from './components/Spinner/Spinner.svelte';
|
||||||
export { default as ScrollerBase } from './components/ScrollerBase/ScrollerBase.svelte';
|
export { default as ScrollerBase } from './components/ScrollerBase/ScrollerBase.svelte';
|
||||||
export { default as ScrollerVideo } from './components/ScrollerVideo/ScrollerVideo.svelte';
|
export { default as ScrollerVideo } from './components/ScrollerVideo/ScrollerVideo.svelte';
|
||||||
export { default as ScrollerVideoForeground } from './components/ScrollerVideo/ScrollerVideoForeground.svelte';
|
export { default as ScrollerVideoForeground } from './components/ScrollerVideo/ScrollerVideoForeground.svelte';
|
||||||
export { default as ScrollerLottie } from './components/ScrollerLottie/ScrollerLottie.svelte';
|
export { default as Lottie } from './components/Lottie/Lottie.svelte';
|
||||||
export { default as ScrollerLottieForeground } from './components/ScrollerLottie/ScrollerLottieForeground.svelte';
|
export { default as LottieForeground } from './components/Lottie/LottieForeground.svelte';
|
||||||
export { default as SponsorshipAd } from './components/AdSlot/SponsorshipAd.svelte';
|
export { default as SponsorshipAd } from './components/AdSlot/SponsorshipAd.svelte';
|
||||||
export { default as Table } from './components/Table/Table.svelte';
|
export { default as Table } from './components/Table/Table.svelte';
|
||||||
export { default as Theme, themes } from './components/Theme/Theme.svelte';
|
export { default as Theme, themes } from './components/Theme/Theme.svelte';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue