Add a 404 page concept and a spring-physics motion primitive

404: "Page Fault" — a SIGSEGV/memory-fault framing distinct from the
retired terminal easter egg, still fitting the CS-flavored identity.
Plain/unstyled for now like the rest of the placeholder layout;
restyle once the design system lands.

src/lib/spring.ts: a small CSS+Web Animations API spring utility
(damped harmonic oscillator -> keyframe progress values), per the
locked "no animation library" decision. Not wired into any component
yet — this is the foundational primitive the design system's motion
work will build on, kept free of any visual/component opinions.
This commit is contained in:
2026-08-30 19:12:44 +03:00
parent 1b7c4039e3
commit 529a0efbfe
2 changed files with 80 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
export interface SpringOptions {
stiffness?: number;
damping?: number;
mass?: number;
/** Stop simulating once position and velocity are both within this of rest. */
precision?: number;
}
/**
* Simulates a damped harmonic oscillator from 0 to 1 and returns per-frame
* progress values (60fps), for use as Web Animations API keyframes. Pure
* physics simulation, no DOM/CSS opinions — see `animateSpring` below for
* the usual way to consume it.
*/
export function springProgress({
stiffness = 170,
damping = 26,
mass = 1,
precision = 0.005,
}: SpringOptions = {}): number[] {
const frames: number[] = [];
let position = 0;
let velocity = 0;
const dt = 1 / 60;
const maxDuration = 4; // seconds, safety cap against under-damped configs
for (let t = 0; t < maxDuration; t += dt) {
const acceleration =
(-stiffness * (position - 1) - damping * velocity) / mass;
velocity += acceleration * dt;
position += velocity * dt;
frames.push(position);
if (Math.abs(1 - position) < precision && Math.abs(velocity) < precision)
break;
}
frames.push(1);
return frames;
}
/**
* Animates `element` from `from` to `to` along a spring curve, via the
* native Web Animations API — no animation library dependency. `toKeyframe`
* shapes each interpolated value into whatever CSS properties you need,
* e.g. `(v) => ({ transform: \`translateY(${(1 - v) * 20}px)\`, opacity: v })`.
* Honors `prefers-reduced-motion` by jumping straight to the end state.
*/
export function animateSpring(
element: HTMLElement,
options: SpringOptions & {
from: number;
to: number;
toKeyframe: (value: number) => Keyframe;
},
): Animation {
const { from, to, toKeyframe, ...spring } = options;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
return element.animate([toKeyframe(to)], { duration: 1, fill: "forwards" });
}
const progress = springProgress(spring);
const keyframes = progress.map((p) => toKeyframe(from + (to - from) * p));
const durationMs = (progress.length / 60) * 1000;
return element.animate(keyframes, {
duration: durationMs,
fill: "forwards",
easing: "linear",
});
}
+14
View File
@@ -0,0 +1,14 @@
---
import Base from "../layouts/Base.astro";
---
<Base title="Page Fault" description="404 — this page is not mapped in this process's address space.">
<h1>Page Fault</h1>
<p class="meta">SIGSEGV — the address you requested was never mapped.</p>
<p>
Whatever used to live here has been moved, renamed, or never existed in
this address space to begin with. Unlike a real page fault, there's no
handler that's going to page this back in for you.
</p>
<p><a href="/">Return to a valid address →</a></p>
</Base>