Motion
How the animation is built, timed and switched off, using three easing curves, three durations and one switch.
The site animates a great deal, so the rules for it are strict. All of it is written by hand on
a small toolkit in src/lib/motion. The only library is three.js, for the 3D scenes.
The switch
One switch in the header turns animation on or off for the whole site and remembers the
choice. Until it is touched it follows the operating system's reduced-motion setting. It sets
data-motion on the page before anything is drawn, and both the CSS and the
scripts read that one attribute.
Easing
Three curves cover the site. Each is plotted here from the value in the stylesheet: time runs left to right, progress bottom to top.
-
ease-outThings arriving. Fast at first, then settling.
-
ease-in-outThings moving from one place to another.
-
ease-springAnything a visitor has just let go of. Overshoots, then settles.
Code
<div class="motion:transition-transform duration-(--duration-base) ease-spring">…</div>
html[data-motion='full'] .badge {
animation: sew var(--duration-slow) var(--ease-out) both;
}
Duration
Short for feedback, longer for distance. Scenes that run continuously keep their own time.
-
duration-quick160ms Feedback: a switch, a hover, a press. -
duration-base320ms Most movement. -
duration-slow640ms Large distances and anything that should be watched.
A scene
Every canvas and WebGL scene follows one pattern: loaded when it nears the viewport, coloured from the palette around it, paused when off screen, and drawn once as a still when animation is off. This one is the smallest possible example. Change its palette with the menu above it, or move the pointer across it.
Code
import { whenNear } from '~/lib/motion';
whenNear(host, async () => {
// three.js arrives with the scene, not with the page.
const { mount } = await import('~/scenes/specimen');
mount(host);
});
const loop = createLoop(host, {
frame: (time, delta) => draw(time), // on screen, tab visible, animation on
still: () => draw(0), // animation off: one finished frame
});
Rules
-
Each animation shows something
Every moving piece on the site is a diagram of the claim next to it: a query being run, a request being routed, a badge being sewn on. Nothing moves only to fill space.
-
Off means off
With animation switched off, every piece shows its finished state as a still. Nothing is hidden, nothing is pinned to the screen, and controls still work.
-
The page is HTML first
All text, lists and links are in the markup. Canvas and WebGL are drawn over real content and are hidden from screen readers, so the site reads the same without JavaScript.
-
Nothing runs unseen
Each scene stops drawing when it scrolls out of view or the tab is hidden. three.js is fetched only when a 3D scene is about to come into view, and only on pages that have one.
-
Colour comes from the palette
Scenes read the colour roles of the element they sit in, so a 3D scene changes palette along with the page. The specimen scene above shows it: change its palette with the menu beside its title.
Between pages
Moving between pages uses the browser's own view transitions between two ordinary HTML documents. The incoming page is drawn over the outgoing one from one side across, which shows the change of palette, and a shoal of fish in the new page's two inks swims across on top of the edge. The heading is picked at random each time: to the left or the right, level or on a diagonal. There is no client-side router. Browsers without view transitions load the next page as normal.