173 lines
No EOL
3.9 KiB
Svelte
173 lines
No EOL
3.9 KiB
Svelte
<script lang="ts">
|
|
import ScrollerBase from '../components/ScrollerBase/ScrollerBase.svelte';
|
|
import { getResponsiveSizes } from '../utils/propValidators';
|
|
|
|
|
|
// Types
|
|
import type {
|
|
ContainerWidth,
|
|
ForegroundPosition,
|
|
ScrollerStep,
|
|
} from '../components/@types/global';
|
|
import Article from '@components/Article/Article.svelte';
|
|
|
|
type Graphic = {
|
|
src: string;
|
|
srcset: string;
|
|
alt: string;
|
|
title?: string;
|
|
description?: string;
|
|
notes?: string;
|
|
width?: ContainerWidth;
|
|
|
|
};
|
|
|
|
|
|
interface Props {
|
|
graphics: Graphic[];
|
|
/** Duration of the animation, milliseconds **/
|
|
animationDuration?: number;
|
|
/** The moment when the animation is at its peak.
|
|
** It is here that we transition between steps and swap the graphic. 0-1 **/
|
|
animationPeak?: number;
|
|
/** Width of the background */
|
|
backgroundWidth?: ContainerWidth;
|
|
/** Position of the foreground */
|
|
foregroundPosition?: ForegroundPosition;
|
|
}
|
|
|
|
let {
|
|
graphics,
|
|
backgroundWidth = 'fluid',
|
|
foregroundPosition = 'middle',
|
|
animationDuration = 350,
|
|
animationPeak = 0.8,
|
|
}: Props = $props();
|
|
|
|
let count = $state(1);
|
|
let index = $state(0);
|
|
let offset = $state(0);
|
|
let progress = $state(0);
|
|
|
|
// reset animation on index change, swap graphic at peak distortion (80%)
|
|
let displayedIndex = $state(0);
|
|
let imgEl = $state<HTMLImageElement | null>(null);
|
|
$effect(() => {
|
|
const newIndex = index; // track changes
|
|
if (!imgEl) return;
|
|
imgEl.style.animation = 'none';
|
|
imgEl.offsetHeight; // force reflow
|
|
imgEl.style.animation = '';
|
|
|
|
const timeout = setTimeout(() => {
|
|
displayedIndex = newIndex;
|
|
}, animationDuration * animationPeak);
|
|
|
|
return () => clearTimeout(timeout);
|
|
});
|
|
</script>
|
|
|
|
<Article>
|
|
|
|
<ScrollerBase
|
|
bind:count
|
|
bind:index
|
|
bind:offset
|
|
bind:progress
|
|
query="div.step-foreground-container"
|
|
>
|
|
{#snippet backgroundSnippet()}
|
|
<div
|
|
class="background min-h-screen relative p-0 flex justify-center"
|
|
class:right={foregroundPosition === 'left opposite'}
|
|
class:left={foregroundPosition === 'right opposite'}
|
|
aria-hidden="true"
|
|
>
|
|
<img
|
|
bind:this={imgEl}
|
|
class="graphic"
|
|
style="--animation-time: {animationDuration}ms"
|
|
src={graphics[displayedIndex].src}
|
|
srcset={graphics[displayedIndex].srcset}
|
|
sizes={getResponsiveSizes(backgroundWidth)}
|
|
alt={graphics[displayedIndex].alt}
|
|
/>
|
|
</div>
|
|
{/snippet}
|
|
|
|
{#snippet foregroundSnippet()}
|
|
{#each graphics as graphic, i}
|
|
<div class="step-foreground-container">
|
|
{#if graphic.title}<h2>{graphic.title}</h2>{/if}
|
|
{#if graphic.description}<p>{graphic.description}</p>{/if}
|
|
{#if graphic.notes}<aside>{graphic.notes}</aside>{/if}
|
|
</div>
|
|
{/each}
|
|
{/snippet}
|
|
</ScrollerBase>
|
|
</Article>
|
|
|
|
<style lang="scss">
|
|
@use 'mixins' as mixins;
|
|
|
|
.graphic {
|
|
display: block;
|
|
object-fit: contain;
|
|
transform-origin: bottom center;
|
|
animation: squash var(--animation-time) linear(0, 0.417 25.5%, 0.867 49.4%, 1 57.7%, 0.925 65.1%, 0.908 68.6%, 0.902 72.2%, 0.916 78.2%, 0.988 92.1%, 1) forwards;
|
|
}
|
|
|
|
@keyframes squash {
|
|
0% {
|
|
scale: 1;
|
|
translate: 0;
|
|
}
|
|
|
|
50% {
|
|
scale: 0.8 1.2;
|
|
translate: 0 -8px;
|
|
}
|
|
|
|
80% {
|
|
scale: 1.4 0.6;
|
|
translate: 0 -16px;
|
|
}
|
|
|
|
100% {
|
|
scale: 1;
|
|
translate: 0;
|
|
}
|
|
}
|
|
// we play this
|
|
@keyframes stretch {
|
|
0% {
|
|
scale: 1;
|
|
translate: 0;
|
|
}
|
|
|
|
50% {
|
|
scale: 0.8 1.2;
|
|
translate: 0 -8px;
|
|
}
|
|
|
|
80% {
|
|
scale: 1.4 0.6;
|
|
translate: 0 -16px;
|
|
}
|
|
|
|
100% {
|
|
scale: 1;
|
|
translate: 0;
|
|
}
|
|
}
|
|
|
|
.step-foreground-container {
|
|
height: 50vh;
|
|
/* background-color: rgba(0, 0, 0, 0.2); */
|
|
padding: 1em;
|
|
margin: 0 0 2em 0;
|
|
max-width: var(--normal-column-width, 660px);
|
|
position: relative;
|
|
}
|
|
|
|
</style> |