since3-website/modules/shared/ui/components/preloader.tsx
2026-09-25 18:53:25 +02:00

105 lines
3 KiB
TypeScript

"use client"
import { useEffect, useRef } from "react"
import {
INTRO_CLASS,
INTRO_END_EVENT,
INTRO_STORAGE_KEY,
isIntroRunning,
} from "@/modules/shared/lib/intro"
const COUNT_DURATION_MS = 1600
const CURTAIN_DURATION_MS = 1000
// The hero starts animating while the curtain is still moving up
const HERO_START_OFFSET_MS = 250
// Fast start, dramatic hold near 100
function easeCount(t: number) {
return t < 0.7 ? (t / 0.7) * 0.86 : 0.86 + (1 - Math.pow(1 - (t - 0.7) / 0.3, 3)) * 0.14
}
export function Preloader() {
const rootRef = useRef<HTMLDivElement>(null)
const countRef = useRef<HTMLSpanElement>(null)
const barRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const root = rootRef.current
if (!root || !isIntroRunning()) return
const timers: number[] = []
let frame = 0
const start = performance.now()
const finish = () => {
root.dataset.state = "leaving"
timers.push(
window.setTimeout(() => {
document.documentElement.classList.remove(INTRO_CLASS)
try {
sessionStorage.setItem(INTRO_STORAGE_KEY, "1")
} catch {
// Storage blocked: the intro simply plays again next time
}
window.dispatchEvent(new Event(INTRO_END_EVENT))
}, HERO_START_OFFSET_MS),
window.setTimeout(() => {
root.dataset.state = "done"
}, CURTAIN_DURATION_MS)
)
}
const tick = (now: number) => {
const t = Math.min((now - start) / COUNT_DURATION_MS, 1)
const value = easeCount(t)
if (countRef.current) {
countRef.current.textContent = String(Math.round(value * 100)).padStart(3, "0")
}
if (barRef.current) barRef.current.style.scale = `${value} 1`
if (t < 1) frame = requestAnimationFrame(tick)
else finish()
}
frame = requestAnimationFrame(tick)
return () => {
cancelAnimationFrame(frame)
timers.forEach(window.clearTimeout)
}
}, [])
return (
<div
ref={rootRef}
aria-hidden="true"
className="preloader fixed inset-0 z-[var(--z-preloader)] flex-col justify-between bg-black p-gutter text-white"
>
<div className="flex items-start justify-between font-mono text-label text-ink-400">
<span>Digitalagentur</span>
<span>Steinfurt, DE</span>
</div>
<p className="self-center font-mono text-2xl sm:text-4xl">
<span className="text-ink-500">&lt;</span>since3
<span className="text-ink-500">&gt;</span>
</p>
<div className="flex flex-col gap-4">
<div className="flex items-end justify-between">
<span className="font-mono text-label text-ink-400">Lädt Erlebnis</span>
<span
ref={countRef}
className="text-mega leading-none tabular-nums type-poster"
>
000
</span>
</div>
<div className="h-px w-full bg-white/15">
<div ref={barRef} className="h-full origin-left bg-white [scale:0_1]" />
</div>
</div>
</div>
)
}