scratch/src/pages/Squash.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>