adds ScrollerLottie
This commit is contained in:
parent
041bbeabeb
commit
2d211ab5d3
17 changed files with 1599 additions and 0 deletions
|
|
@ -98,6 +98,7 @@
|
|||
"dependencies": {
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@lottiefiles/dotlottie-web": "^0.52.2",
|
||||
"@reuters-graphics/svelte-markdown": "^0.0.3",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ importers:
|
|||
'@fortawesome/free-solid-svg-icons':
|
||||
specifier: ^6.7.2
|
||||
version: 6.7.2
|
||||
'@lottiefiles/dotlottie-web':
|
||||
specifier: ^0.52.2
|
||||
version: 0.52.2
|
||||
'@reuters-graphics/svelte-markdown':
|
||||
specifier: ^0.0.3
|
||||
version: 0.0.3(svelte@5.28.1)
|
||||
|
|
@ -569,6 +572,9 @@ packages:
|
|||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@lottiefiles/dotlottie-web@0.52.2':
|
||||
resolution: {integrity: sha512-aeXCMUB5RdusHrvZ3Py2KaMgQ0w7SdA8NFbPK+SpwqC1UW1CDFZl5vPLueZHju7vLhB1rpPvpQ5fQ0L/KEZt7w==}
|
||||
|
||||
'@manypkg/find-root@1.1.0':
|
||||
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
|
||||
|
||||
|
|
@ -4641,6 +4647,8 @@ snapshots:
|
|||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@lottiefiles/dotlottie-web@0.52.2': {}
|
||||
|
||||
'@manypkg/find-root@1.1.0':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
|
|
|
|||
|
|
@ -69,6 +69,17 @@ export type ScrollerVideoForegroundPosition =
|
|||
| 'center left'
|
||||
| 'center right';
|
||||
|
||||
export type ScrollerLottieForegroundPosition =
|
||||
| 'top center'
|
||||
| 'top left'
|
||||
| 'top right'
|
||||
| 'bottom center'
|
||||
| 'bottom left'
|
||||
| 'bottom right'
|
||||
| 'center center'
|
||||
| 'center left'
|
||||
| 'center right';
|
||||
|
||||
// Complete ScrollerVideo instance interface
|
||||
export interface ScrollerVideoInstance {
|
||||
// Properties
|
||||
|
|
|
|||
271
src/components/ScrollerLottie/Debug.svelte
Normal file
271
src/components/ScrollerLottie/Debug.svelte
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
<script lang="ts">
|
||||
const { componentState } = $props();
|
||||
|
||||
let isMoving = $state(false);
|
||||
let preventDetails = $state(false);
|
||||
let position = $state({ x: 8, y: 8 });
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
isMoving = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (isMoving) {
|
||||
position = {
|
||||
x: position.x + e.movementX,
|
||||
y: position.y + e.movementY,
|
||||
};
|
||||
preventDetails = true;
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onMouseUp(e: MouseEvent) {
|
||||
if (isMoving) {
|
||||
isMoving = false;
|
||||
setTimeout(() => {
|
||||
preventDetails = false;
|
||||
}, 5);
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onClick(e: MouseEvent) {
|
||||
if (preventDetails) {
|
||||
e.preventDefault();
|
||||
}
|
||||
isMoving = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onmousemove={onMouseMove} />
|
||||
|
||||
<div
|
||||
style="position: absolute; top: {position.y}px; left: {position.x}px; z-index: 5; user-select: none;"
|
||||
role="region"
|
||||
>
|
||||
<details class="debug-info" open>
|
||||
<summary
|
||||
class="text-xxs font-sans font-bold title"
|
||||
style="grid-column: span 2;"
|
||||
onmousedown={onMouseDown}
|
||||
onmouseup={onMouseUp}
|
||||
onclick={onClick}
|
||||
>
|
||||
CONSOLE
|
||||
</summary>
|
||||
<div class="state-debug">
|
||||
<!-- -->
|
||||
<p>Progress:</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<p class="state-value">{componentState.progress}</p>
|
||||
<div id="video-progress-bar">
|
||||
<div
|
||||
style="width: {componentState.progress * 100}%; height: 100%;"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- -->
|
||||
<p>Duration:</p>
|
||||
<p class="state-value">
|
||||
{componentState.duration}s
|
||||
</p>
|
||||
<!-- -->
|
||||
{#if componentState.segment}
|
||||
<p>Segment:</p>
|
||||
<p class="state-value">
|
||||
{componentState.segment[0]} -- {componentState.segment[1]}
|
||||
</p>
|
||||
{/if}
|
||||
<!-- -->
|
||||
<p>Current frame:</p>
|
||||
<p class="state-value">
|
||||
{componentState.currentFrame}/{componentState.totalFrames}
|
||||
</p>
|
||||
<!-- -->
|
||||
<p>Speed:</p>
|
||||
<p class="state-value">
|
||||
{componentState.speed}
|
||||
</p>
|
||||
<!-- -->
|
||||
<p>Autoplay:</p>
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.autoplay}</span>
|
||||
</p>
|
||||
<!-- -->
|
||||
<p>Loop:</p>
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.loop}</span>
|
||||
{componentState.loop ? `(Loop count: ${componentState.loopCount})` : ''}
|
||||
</p>
|
||||
<!-- -->
|
||||
<p>Mode:</p>
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.mode}</span>
|
||||
</p>
|
||||
<!-- -->
|
||||
<p>Layout:</p>
|
||||
<p class="state-value">
|
||||
{JSON.stringify(componentState.layout)}
|
||||
</p>
|
||||
<!-- -->
|
||||
{#if Object.keys(componentState.allMarkers).length}
|
||||
<p>All markers:</p>
|
||||
<p class="state-value">
|
||||
{componentState.allMarkers}
|
||||
</p>
|
||||
{/if}
|
||||
<!-- -->
|
||||
{#if componentState.marker}
|
||||
<p>Active marker:</p>
|
||||
<p class="state-value">
|
||||
{componentState.marker}
|
||||
</p>
|
||||
{/if}
|
||||
<!-- -->
|
||||
{#if componentState.allThemes.length}
|
||||
<p>All themes:</p>
|
||||
<p class="state-value">
|
||||
{componentState.allThemes.join(', ')}
|
||||
</p>
|
||||
{/if}
|
||||
{#if componentState.activeThemeId}
|
||||
<p>Active theme ID:</p>
|
||||
<p class="state-value">
|
||||
{componentState.activeThemeId}
|
||||
</p>
|
||||
{/if}
|
||||
<!-- -->
|
||||
<p>isPaused:</p>
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.isPaused}</span>
|
||||
</p>
|
||||
<!-- -->
|
||||
<p>isPlaying:</p>
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.isPlaying}</span>
|
||||
</p>
|
||||
<!-- -->
|
||||
<p>isStopped:</p>
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.isStopped}</span>
|
||||
</p>
|
||||
<!-- -->
|
||||
<p>isLoaded:</p>
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.isLoaded}</span>
|
||||
</p>
|
||||
<!-- -->
|
||||
<p>isFrozen:</p>
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.isFrozen}</span>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap');
|
||||
|
||||
.debug-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
z-index: 3;
|
||||
margin: 0;
|
||||
width: 50vmin;
|
||||
min-width: 50vmin;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
resize: horizontal;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.3s ease;
|
||||
filter: drop-shadow(0 0 16px rgba(0, 0, 0, 0.5));
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
interpolate-size: allow-keywords;
|
||||
}
|
||||
|
||||
&::details-content {
|
||||
opacity: 0;
|
||||
block-size: 0;
|
||||
overflow-y: clip;
|
||||
transition:
|
||||
content-visibility 0.4s allow-discrete,
|
||||
opacity 0.4s,
|
||||
block-size 0.4s cubic-bezier(0.87, 0, 0.13, 1);
|
||||
}
|
||||
|
||||
&[open]::details-content {
|
||||
opacity: 1;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
font-family: 'Geist Mono', monospace;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.debug-info[open] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.state-debug {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
padding: 8px 8px 16px 8px;
|
||||
grid-template-columns: 20vmin 1fr;
|
||||
align-items: center;
|
||||
gap: 0.75rem 0.25rem;
|
||||
background-color: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--theme-font-size-xxs);
|
||||
font-family: 'Geist Mono', monospace;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 100%;
|
||||
font-variant: tabular-nums;
|
||||
}
|
||||
|
||||
.state-value {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#video-progress-bar {
|
||||
width: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
height: 2px;
|
||||
border-radius: 50px;
|
||||
// margin: auto;
|
||||
|
||||
div {
|
||||
background-color: white;
|
||||
border-radius: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 0.1rem 0.2rem;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
345
src/components/ScrollerLottie/ScrollerLottie.mdx
Normal file
345
src/components/ScrollerLottie/ScrollerLottie.mdx
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
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.
|
||||
|
||||
## 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
|
||||
: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';
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- Inside the content.blocks for loop... -->
|
||||
{#if block.type == 'lottie'}
|
||||
<ScrollerLottie
|
||||
src={`./data/${block.src}.lottie?url`}
|
||||
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
|
||||
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';
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- Inside the content.blocks for loop... -->
|
||||
{#if block.type == 'lottie'}
|
||||
<ScrollerLottie
|
||||
src={`./data/${block.src}.lottie?url`}
|
||||
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
|
||||
[.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';
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- Inside the content.blocks for loop... -->
|
||||
{#if block.type == 'lottie'}
|
||||
<ScrollerLottie
|
||||
src={`./data/${block.src}.lottie?url`}
|
||||
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
|
||||
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';
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- Inside the content.blocks for loop... -->
|
||||
{#if block.type == 'lottie'}
|
||||
<ScrollerLottie
|
||||
src={`./data/${block.src}.lottie?url`}
|
||||
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';
|
||||
import LottieSrc from './data/lottie-example.lottie?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';
|
||||
import LottieSrc from './data/lottie-example.lottie?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
|
||||
|
||||
# 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 } from '@reuters-graphics/graphics-components';
|
||||
|
||||
const Components = $state({
|
||||
Headline,
|
||||
Video,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- Inside the content.blocks for loop... -->
|
||||
{#if block.type == 'lottie'}
|
||||
<ScrollerLottie
|
||||
src={`./data/${block.src}.lottie?url`}
|
||||
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}
|
||||
```
|
||||
125
src/components/ScrollerLottie/ScrollerLottie.stories.svelte
Normal file
125
src/components/ScrollerLottie/ScrollerLottie.stories.svelte
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import ScrollerLottie from './ScrollerLottie.svelte';
|
||||
import ScrollerLottieForeground from './ScrollerLottieForeground.svelte';
|
||||
import Headline from '../Headline/Headline.svelte';
|
||||
import Theme from '../Theme/Theme.svelte';
|
||||
|
||||
import MarkerSample from './data/markerSample.lottie?url';
|
||||
import ForegroundSample from './data/foregroundSample.lottie?url';
|
||||
import WithScrollerBase from './demo/withScrollerBase.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Graphics/ScrollerLottie',
|
||||
component: ScrollerLottie,
|
||||
argTypes: {
|
||||
data: {
|
||||
table: {
|
||||
disable: true, // Hides from Docs table
|
||||
},
|
||||
control: false, // Removes from Controls panel
|
||||
},
|
||||
lottiePlayer: {
|
||||
table: {
|
||||
category: 'Bindable states',
|
||||
},
|
||||
},
|
||||
progress: {
|
||||
table: {
|
||||
category: 'Bindable states',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let progress = $state(0);
|
||||
</script>
|
||||
|
||||
<Story name="Basic">
|
||||
<ScrollerLottie autoplay loop showDebugInfo />
|
||||
</Story>
|
||||
|
||||
<Story name="Marker">
|
||||
<ScrollerLottie
|
||||
src={MarkerSample}
|
||||
showDebugInfo
|
||||
autoplay
|
||||
marker="ballerina"
|
||||
loop
|
||||
mode="bounce"
|
||||
/>
|
||||
</Story>
|
||||
|
||||
<Story name="Segment">
|
||||
<ScrollerLottie autoplay loop showDebugInfo segment={[0, 20]} speed={0.5} />
|
||||
</Story>
|
||||
|
||||
<Story name="Themes">
|
||||
<ScrollerLottie
|
||||
src={'https://lottie.host/9a5a6605-fc90-4935-8d10-9df4c83902ff/PFUKH53LJk.lottie'}
|
||||
showDebugInfo
|
||||
autoplay
|
||||
bind:progress
|
||||
themeId={progress < 0.33 ? 'Water'
|
||||
: progress < 0.66 ? 'air'
|
||||
: 'earth'}
|
||||
/>
|
||||
</Story>
|
||||
|
||||
<Story name="Using with ScrollerBase" exportName="ScrollerBase">
|
||||
<WithScrollerBase />
|
||||
</Story>
|
||||
|
||||
<Story name="With foregrounds">
|
||||
<ScrollerLottie
|
||||
src={ForegroundSample}
|
||||
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}
|
||||
endFrame={50}
|
||||
position="center center"
|
||||
backgroundColour="rgba(0, 0, 0)"
|
||||
width="normal"
|
||||
>
|
||||
<Theme base="dark">
|
||||
<Headline
|
||||
hed="ScrollerLottie with foreground component"
|
||||
dek="This is an example of using a Svelte component as the foreground."
|
||||
width="normal"
|
||||
authors={['Jane Doe', 'John Doe']}
|
||||
/>
|
||||
</Theme>
|
||||
</ScrollerLottieForeground>
|
||||
</ScrollerLottie>
|
||||
</Story>
|
||||
|
||||
<style lang="scss">
|
||||
:global {
|
||||
.scroller-lottie-foreground {
|
||||
header {
|
||||
padding: 2rem;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.foreground-text {
|
||||
p {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
521
src/components/ScrollerLottie/ScrollerLottie.svelte
Normal file
521
src/components/ScrollerLottie/ScrollerLottie.svelte
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
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 { isEqual } from 'es-toolkit';
|
||||
import Debug from './Debug.svelte';
|
||||
import { map } from './ts/utils';
|
||||
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 = {
|
||||
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'] | null | 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 & {
|
||||
// Additional properties can be added here if needed
|
||||
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 canvasWidth: number = $state(1);
|
||||
let canvasHeight: number = $state(1);
|
||||
let prevSrc = void 0;
|
||||
let prevData = void 0;
|
||||
let progressTween = new Tween(0, { duration: 100 });
|
||||
let start: number = $state(0);
|
||||
let end: number = $state(0);
|
||||
|
||||
let {
|
||||
autoplay = false,
|
||||
loop = false,
|
||||
mode = 'forward',
|
||||
src = DefaultLottie,
|
||||
speed = 1,
|
||||
data = undefined,
|
||||
backgroundColor = '#ffffff',
|
||||
segment = undefined,
|
||||
renderConfig = undefined,
|
||||
dotLottieRefCallback = () => {},
|
||||
useFrameInterpolation = true,
|
||||
themeId = '',
|
||||
themeData = '',
|
||||
playOnHover = false,
|
||||
marker = '',
|
||||
layout = { fit: 'contain', align: [0.5, 0.5] },
|
||||
animationId = '',
|
||||
lottiePlayer = $bindable(undefined),
|
||||
width = 'widest',
|
||||
height = '100lvh',
|
||||
showDebugInfo = false,
|
||||
lottieState = createLottieState(),
|
||||
progress = $bindable(0),
|
||||
tweenDuration = 100,
|
||||
easing = (t: number) => t,
|
||||
onLoad = () => {},
|
||||
onRender = () => {},
|
||||
onComplete = () => {},
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
// pass on component state to child components
|
||||
// this controls fade in and out of foregrounds
|
||||
setContext('lottieState', lottieState);
|
||||
|
||||
function onLoadEvent() {
|
||||
if (showDebugInfo) {
|
||||
// set layout
|
||||
lottiePlayer.setLayout(layout);
|
||||
|
||||
lottieState.allMarkers = lottiePlayer.markers().map((x) => x.name);
|
||||
|
||||
if (lottiePlayer.manifest) {
|
||||
lottieState.allThemes =
|
||||
lottiePlayer?.manifest.themes ?
|
||||
lottiePlayer.manifest.themes.map((t) => t.id)
|
||||
: [];
|
||||
}
|
||||
|
||||
if (marker == '' || marker == null || marker == undefined) {
|
||||
start = segment ? segment[0] : 0;
|
||||
end = segment ? segment[1] : lottiePlayer.totalFrames - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// set to frame 1 to trigger initial render
|
||||
// helpful especially when themeId is set
|
||||
lottiePlayer.setFrame(1);
|
||||
|
||||
onLoad(); // call user-defined onLoad function
|
||||
}
|
||||
|
||||
function onCompleteEvent() {
|
||||
onComplete();
|
||||
}
|
||||
|
||||
function onRenderEvent() {
|
||||
const keys = [
|
||||
'currentFrame',
|
||||
'totalFrames',
|
||||
'duration',
|
||||
'loop',
|
||||
'speed',
|
||||
'loopCount',
|
||||
'mode',
|
||||
'isPaused',
|
||||
'isPlaying',
|
||||
'isStopped',
|
||||
'isLoaded',
|
||||
'isFrozen',
|
||||
'segment',
|
||||
'autoplay',
|
||||
'layout',
|
||||
'activeThemeId',
|
||||
'marker',
|
||||
];
|
||||
|
||||
if (lottiePlayer && lottieState) {
|
||||
keys.forEach((key) => {
|
||||
lottieState[key] = lottiePlayer[key];
|
||||
});
|
||||
}
|
||||
|
||||
progress = (lottiePlayer.currentFrame + 1) / lottiePlayer.totalFrames;
|
||||
|
||||
lottieState.progress = progress;
|
||||
|
||||
onRender(); // call user-defined onRender function
|
||||
}
|
||||
|
||||
const hoverHandler = (event) => {
|
||||
if (!playOnHover || !lottiePlayer.isLoaded) return;
|
||||
if (event.type === 'mouseenter') {
|
||||
lottiePlayer.play();
|
||||
} else if (event.type === 'mouseleave') {
|
||||
lottiePlayer.pause();
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const shouldAutoplay = autoplay && !playOnHover;
|
||||
|
||||
progressTween = new Tween(0, { duration: tweenDuration, easing: easing });
|
||||
|
||||
const _renderConfig = {
|
||||
autoResize: true,
|
||||
devicePixelRatio:
|
||||
window.devicePixelRatio > 1 ? window.devicePixelRatio * 0.75 : 1,
|
||||
freezeOnOffscreen: true,
|
||||
};
|
||||
|
||||
lottiePlayer = new DotLottie({
|
||||
canvas,
|
||||
src,
|
||||
autoplay: shouldAutoplay,
|
||||
loop,
|
||||
speed,
|
||||
data,
|
||||
renderConfig: _renderConfig,
|
||||
segment,
|
||||
useFrameInterpolation,
|
||||
backgroundColor,
|
||||
mode,
|
||||
animationId,
|
||||
themeId,
|
||||
});
|
||||
|
||||
DotLottie.setWasmUrl(WASM);
|
||||
|
||||
lottiePlayer.addEventListener('load', onLoadEvent);
|
||||
lottiePlayer.addEventListener('frame', onRenderEvent);
|
||||
lottiePlayer.addEventListener('complete', onCompleteEvent);
|
||||
|
||||
if (dotLottieRefCallback) {
|
||||
dotLottieRefCallback(lottiePlayer);
|
||||
}
|
||||
|
||||
canvas.addEventListener('mouseenter', hoverHandler);
|
||||
canvas.addEventListener('mouseleave', hoverHandler);
|
||||
|
||||
return () => {
|
||||
canvas.removeEventListener('mouseenter', hoverHandler);
|
||||
canvas.removeEventListener('mouseleave', hoverHandler);
|
||||
lottiePlayer.destroy();
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (lottiePlayer) {
|
||||
lottiePlayer.removeEventListener('render', onRender);
|
||||
lottiePlayer.removeEventListener('load', onLoad);
|
||||
lottiePlayer.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Handles progress change
|
||||
$effect(() => {
|
||||
if (lottieState.isLoaded && lottieState.progress !== progress) {
|
||||
autoplay = false;
|
||||
lottiePlayer.pause();
|
||||
loop = false;
|
||||
|
||||
if (progress >= 0 && progress <= 1) {
|
||||
if (lottieState.isFrozen) {
|
||||
lottiePlayer.unfreeze();
|
||||
lottieState.isFrozen = false;
|
||||
}
|
||||
const targetFrame = map(
|
||||
mode == 'reverse' || mode == 'reverse-bounce' ?
|
||||
1 - progress
|
||||
: progress,
|
||||
0,
|
||||
1,
|
||||
start,
|
||||
end
|
||||
);
|
||||
progressTween.target = targetFrame;
|
||||
// lottiePlayer.setFrame(targetFrame);
|
||||
} else if ((progress < 0 || progress > 1) && !lottieState.isFrozen) {
|
||||
// lottiePlayer.setFrame(progress < 0 ? start : end);
|
||||
if (mode == 'reverse' || mode == 'reverse-bounce') {
|
||||
progressTween.target = progress < 0 ? end : start;
|
||||
} else {
|
||||
progressTween.target = progress < 0 ? start : end;
|
||||
}
|
||||
lottiePlayer.freeze();
|
||||
lottieState.isFrozen = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tweens to progress value
|
||||
$effect(() => {
|
||||
if (progressTween.current >= 0) {
|
||||
lottiePlayer.setFrame(progressTween.current);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles layout change
|
||||
$effect(() => {
|
||||
if (
|
||||
typeof layout === 'object' &&
|
||||
lottiePlayer.isLoaded &&
|
||||
!isEqual(layout, lottiePlayer.layout)
|
||||
) {
|
||||
lottiePlayer.setLayout(layout);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles marker change
|
||||
$effect(() => {
|
||||
if (lottieState.isLoaded && lottiePlayer.marker !== marker) {
|
||||
if (typeof marker === 'string') {
|
||||
lottiePlayer.setMarker(marker);
|
||||
|
||||
start =
|
||||
lottiePlayer.markers().find((m) => m.name === marker)?.time ?? 0;
|
||||
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('');
|
||||
} else {
|
||||
console.warn('Invalid marker type:', marker);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handles speed change
|
||||
$effect(() => {
|
||||
if (
|
||||
lottieState.isLoaded &&
|
||||
typeof speed == 'number' &&
|
||||
lottiePlayer.speed !== speed
|
||||
) {
|
||||
lottiePlayer.setSpeed(speed);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles frame interpolation change
|
||||
$effect(() => {
|
||||
if (
|
||||
lottieState.isLoaded &&
|
||||
typeof useFrameInterpolation == 'boolean' &&
|
||||
lottiePlayer.useFrameInterpolation !== useFrameInterpolation
|
||||
) {
|
||||
lottiePlayer.setUseFrameInterpolation(useFrameInterpolation);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles segment change
|
||||
$effect(() => {
|
||||
if (lottieState.isLoaded && !isEqual(lottiePlayer.segment, segment)) {
|
||||
if (
|
||||
Array.isArray(segment) &&
|
||||
segment.length === 2 &&
|
||||
typeof segment[0] === 'number' &&
|
||||
typeof segment[1] === 'number'
|
||||
) {
|
||||
let [start, end] = segment;
|
||||
lottiePlayer.setSegment(start, end);
|
||||
} else if (segment === null || segment === undefined) {
|
||||
lottiePlayer.setSegment(0, lottiePlayer.totalFrames);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handles loop change
|
||||
$effect(() => {
|
||||
if (
|
||||
lottieState.isLoaded &&
|
||||
typeof loop == 'boolean' &&
|
||||
lottiePlayer.loop !== loop
|
||||
) {
|
||||
lottiePlayer.setLoop(loop);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles autoplay change
|
||||
$effect(() => {
|
||||
if (
|
||||
lottieState.isLoaded &&
|
||||
typeof autoplay == 'boolean' &&
|
||||
lottieState.autoplay !== autoplay
|
||||
) {
|
||||
lottieState.autoplay = !autoplay;
|
||||
}
|
||||
});
|
||||
|
||||
// Handles background color change
|
||||
$effect(() => {
|
||||
if (
|
||||
lottieState.isLoaded &&
|
||||
lottiePlayer.backgroundColor !== backgroundColor
|
||||
) {
|
||||
lottiePlayer.setBackgroundColor(backgroundColor || '');
|
||||
}
|
||||
});
|
||||
|
||||
// Handles mode change
|
||||
$effect(() => {
|
||||
if (
|
||||
lottieState.isLoaded &&
|
||||
typeof mode == 'string' &&
|
||||
lottiePlayer.mode !== mode
|
||||
) {
|
||||
lottiePlayer.setMode(mode);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles src change
|
||||
$effect(() => {
|
||||
if (lottieState && src !== prevSrc) {
|
||||
lottiePlayer.load({
|
||||
src,
|
||||
autoplay,
|
||||
loop,
|
||||
speed,
|
||||
data,
|
||||
renderConfig,
|
||||
segment,
|
||||
useFrameInterpolation,
|
||||
backgroundColor,
|
||||
mode,
|
||||
marker,
|
||||
layout,
|
||||
animationId,
|
||||
themeId,
|
||||
});
|
||||
|
||||
prevSrc = src;
|
||||
}
|
||||
});
|
||||
|
||||
// Generate new instance if data changes
|
||||
$effect(() => {
|
||||
if (lottiePlayer && data !== prevData) {
|
||||
lottiePlayer.load({
|
||||
src,
|
||||
autoplay,
|
||||
loop,
|
||||
speed,
|
||||
data,
|
||||
renderConfig,
|
||||
segment,
|
||||
useFrameInterpolation,
|
||||
backgroundColor,
|
||||
mode,
|
||||
marker,
|
||||
layout,
|
||||
animationId,
|
||||
themeId,
|
||||
});
|
||||
prevData = data;
|
||||
}
|
||||
});
|
||||
|
||||
// Handles animationId change
|
||||
$effect(() => {
|
||||
if (
|
||||
lottieState.isLoaded &&
|
||||
lottiePlayer.activeAnimationId !== animationId
|
||||
) {
|
||||
lottiePlayer.loadAnimation(animationId);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles themeId change
|
||||
$effect(() => {
|
||||
if (lottieState.isLoaded && lottiePlayer.activeThemeId !== themeId) {
|
||||
lottiePlayer.setTheme(themeId);
|
||||
}
|
||||
});
|
||||
|
||||
// Handles themeData change
|
||||
$effect(() => {
|
||||
if (lottieState.isLoaded && lottiePlayer.isLoaded) {
|
||||
lottiePlayer.setThemeData(themeData);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Block {width} class="lottie-block">
|
||||
{#if showDebugInfo && lottiePlayer}
|
||||
<Debug componentState={lottieState} />
|
||||
{/if}
|
||||
|
||||
<div class="lottie-container" style:height>
|
||||
<canvas
|
||||
bind:this={canvas}
|
||||
bind:clientWidth={canvasWidth}
|
||||
bind:clientHeight={canvasHeight}
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
</Block>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.lottie-block) {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.lottie-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 3;
|
||||
margin: 0;
|
||||
min-width: 25vmin;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
padding: 4px 0 0 8px;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
p {
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 4px 8px 8px 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
170
src/components/ScrollerLottie/ScrollerLottieForeground.svelte
Normal file
170
src/components/ScrollerLottie/ScrollerLottieForeground.svelte
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
<script lang="ts">
|
||||
import Block from '../Block/Block.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { getContext, onDestroy } from 'svelte';
|
||||
import { Markdown } from '@reuters-graphics/svelte-markdown';
|
||||
|
||||
// Types
|
||||
import type { Component, Snippet } from 'svelte';
|
||||
import type { LottieState } from './lottieState.svelte';
|
||||
import type {
|
||||
ContainerWidth,
|
||||
ScrollerLottieForegroundPosition,
|
||||
} from '../@types/global';
|
||||
|
||||
interface ForegroundProps {
|
||||
id?: string;
|
||||
class?: string;
|
||||
startFrame?: number;
|
||||
endFrame?: number;
|
||||
children?: Snippet;
|
||||
backgroundColour?: string;
|
||||
width?: ContainerWidth;
|
||||
position?: ScrollerLottieForegroundPosition | string;
|
||||
text?: string;
|
||||
Foreground?: Component;
|
||||
}
|
||||
|
||||
let {
|
||||
id = '',
|
||||
class: cls = '',
|
||||
startFrame = 0,
|
||||
endFrame = 10,
|
||||
children,
|
||||
backgroundColour = '#000',
|
||||
width = 'normal',
|
||||
position = 'center center',
|
||||
text,
|
||||
Foreground,
|
||||
}: ForegroundProps = $props();
|
||||
|
||||
let componentState: LottieState | null = getContext('lottieState');
|
||||
|
||||
onDestroy(() => {
|
||||
componentState = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Block class={`scroller-lottie-foreground ${cls}`} {id}>
|
||||
{#if componentState?.currentFrame && componentState.currentFrame >= startFrame && componentState.currentFrame <= endFrame}
|
||||
<div
|
||||
class="scroller-foreground"
|
||||
in:fade={{ delay: 100, duration: 200 }}
|
||||
out:fade={{ delay: 0, duration: 100 }}
|
||||
>
|
||||
<!-- Text blurb foreground -->
|
||||
{#if text}
|
||||
<Block
|
||||
class="scroller-lottie-foreground-text {position.split(' ')[1]}"
|
||||
{width}
|
||||
>
|
||||
<div
|
||||
style="background-color: {backgroundColour};"
|
||||
class="foreground-text {position.split(' ')[0]}"
|
||||
>
|
||||
<Markdown source={text} />
|
||||
</div>
|
||||
</Block>
|
||||
<!-- Render children snippet -->
|
||||
{:else if children}
|
||||
<div class="scroller-lottie-foreground-item">
|
||||
{@render children()}
|
||||
</div>
|
||||
<!-- Render Foreground component -->
|
||||
{:else if Foreground}
|
||||
<div class="scroller-lottie-foreground-item">
|
||||
<Block width="fluid">
|
||||
<Foreground />
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Block>
|
||||
|
||||
<style lang="scss">
|
||||
@use './../../scss/mixins' as mixins;
|
||||
|
||||
.scroller-foreground {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.scroller-lottie-foreground) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.scroller-lottie-foreground-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global {
|
||||
.scroller-lottie-foreground-text {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: calc(mixins.$column-width-normal * 0.9);
|
||||
height: 100%;
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&.left {
|
||||
left: 0;
|
||||
}
|
||||
&.right {
|
||||
right: 0;
|
||||
}
|
||||
&.center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.foreground-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 0.25rem;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
@include mixins.fpy-5;
|
||||
@include mixins.fpx-4;
|
||||
@include mixins.fm-0;
|
||||
|
||||
:global(*) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.center {
|
||||
top: 50%;
|
||||
}
|
||||
&.top {
|
||||
top: 0;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
&.bottom {
|
||||
top: 100%;
|
||||
transform: translate(-50%, -150%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
src/components/ScrollerLottie/assets/marker.png
Normal file
BIN
src/components/ScrollerLottie/assets/marker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
BIN
src/components/ScrollerLottie/data/defaultLottie.lottie
Normal file
BIN
src/components/ScrollerLottie/data/defaultLottie.lottie
Normal file
Binary file not shown.
BIN
src/components/ScrollerLottie/data/dotlottie-player.wasm
Normal file
BIN
src/components/ScrollerLottie/data/dotlottie-player.wasm
Normal file
Binary file not shown.
BIN
src/components/ScrollerLottie/data/foregroundSample.lottie
Normal file
BIN
src/components/ScrollerLottie/data/foregroundSample.lottie
Normal file
Binary file not shown.
BIN
src/components/ScrollerLottie/data/markerSample.lottie
Normal file
BIN
src/components/ScrollerLottie/data/markerSample.lottie
Normal file
Binary file not shown.
74
src/components/ScrollerLottie/demo/withScrollerBase.svelte
Normal file
74
src/components/ScrollerLottie/demo/withScrollerBase.svelte
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<script lang="ts">
|
||||
import Block from '../../Block/Block.svelte';
|
||||
import ScrollerBase from '../../ScrollerBase/ScrollerBase.svelte';
|
||||
import ScrollerLottie from '../ScrollerLottie.svelte';
|
||||
import LottieSample from '../data/markerSample.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={'https://lottie.host/9a5a6605-fc90-4935-8d10-9df4c83902ff/PFUKH53LJk.lottie'}
|
||||
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>
|
||||
49
src/components/ScrollerLottie/ts/lottieState.svelte.ts
Normal file
49
src/components/ScrollerLottie/ts/lottieState.svelte.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
export interface LottieState {
|
||||
progress: number;
|
||||
currentFrame: number;
|
||||
totalFrames: number;
|
||||
duration: number;
|
||||
loop: boolean;
|
||||
speed: number;
|
||||
loopCount: number;
|
||||
mode: string;
|
||||
isPaused: boolean;
|
||||
isPlaying: boolean;
|
||||
isStopped: boolean;
|
||||
isLoaded: boolean;
|
||||
isFrozen: boolean;
|
||||
segment: null | [number, number];
|
||||
autoplay: boolean;
|
||||
layout: null | string;
|
||||
allMarkers: Array<string>;
|
||||
marker: null | string;
|
||||
allThemes: Array<string>;
|
||||
activeThemeId: null | string;
|
||||
}
|
||||
|
||||
export function createLottieState(): LottieState {
|
||||
const lottieState = $state<LottieState>({
|
||||
progress: 0,
|
||||
currentFrame: 0,
|
||||
totalFrames: 0,
|
||||
duration: 0,
|
||||
loop: false,
|
||||
speed: 1,
|
||||
loopCount: 0,
|
||||
mode: '',
|
||||
isPaused: false,
|
||||
isPlaying: false,
|
||||
isStopped: false,
|
||||
isLoaded: false,
|
||||
isFrozen: false,
|
||||
segment: null,
|
||||
autoplay: false,
|
||||
layout: null,
|
||||
allMarkers: [],
|
||||
marker: null,
|
||||
allThemes: [],
|
||||
activeThemeId: null,
|
||||
});
|
||||
|
||||
return lottieState;
|
||||
}
|
||||
22
src/components/ScrollerLottie/ts/utils.ts
Normal file
22
src/components/ScrollerLottie/ts/utils.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,8 @@ export { default as Spinner } from './components/Spinner/Spinner.svelte';
|
|||
export { default as ScrollerBase } from './components/ScrollerBase/ScrollerBase.svelte';
|
||||
export { default as ScrollerVideo } from './components/ScrollerVideo/ScrollerVideo.svelte';
|
||||
export { default as ScrollerVideoForeground } from './components/ScrollerVideo/ScrollerVideoForeground.svelte';
|
||||
export { default as ScrollerLottie } from './components/ScrollerLottie/ScrollerLottie.svelte';
|
||||
export { default as ScrollerLottieForeground } from './components/ScrollerLottie/ScrollerLottieForeground.svelte';
|
||||
export { default as SponsorshipAd } from './components/AdSlot/SponsorshipAd.svelte';
|
||||
export { default as Table } from './components/Table/Table.svelte';
|
||||
export { default as Theme, themes } from './components/Theme/Theme.svelte';
|
||||
|
|
|
|||
Loading…
Reference in a new issue