Ghosty

Engine · v1
How it’s made

A quiet character

Ghosty lives on rnspencer.com, where the work is complex systems made quiet. It’s there to keep you company, not to take the stage: clearly someone, rarely the focus. Everything below is how that restraint is built, with the engine’s real numbers.

01

One body, many faces

The silhouette never changes; the face is the variable. Fifteen faces, each cut through the body with an SVG mask, so the page shows through them in any light. Two soft oval eyes are simple enough to read at sixteen pixels, the favicon’s size, and expressive enough to carry every mood.

15 faces · 1 silhouette · masked, not drawn on · legible at 16px

02

Alive, not animated

There are no keyframes. One small loop runs every frame, and every motion in it is a spring: a target, a stiffness, and a damping. Springs are stepped on real elapsed time, so the ghost moves the same at 60 or 120 frames a second, and a dropped frame can’t make it jump.

1 loop · 6 springs · time step capped at 50ms

03

Damped so it settles

A spring with too little damping bounces; that read as rubbery, not calm. Each one here is critically damped, damping close to twice the square root of its stiffness, so it arrives without overshoot. The squash is set just under critical: a single soft settle when you poke it, then still.

float k18 c8.5 · lean k16 c8 · eyes k40 c12.6 · nod k30 c11 · squash k70 c16

04

A float that never loops

The hover is two slow sine waves at unrelated speeds, so their sum never visibly repeats. Delight lifts it a full twenty pixels, a little leap of joy; sleepiness sinks it ten, toward its shadow, and quiets the waves. A fast scroll leaves it trailing a few pixels behind, then it catches up.

3.2px at 0.7 rad/s + 1.4px at 0.31 rad/s · up 20px happy · down 10px sleepy · scroll trail ±6px

05

A hem like cloth

The scalloped hem is redrawn from fifteen points, each riding two waves out of phase with its neighbours, so it ripples rather than wobbles. It trails the lean, calms to a third when it sleeps, and streams out behind it in flight. Thirty redraws a second is enough for cloth, at half the cost.

15 points · 2 waves · ×0.35 asleep · up to ×2.4 in flight · 30fps

06

A blink, timed like one

Lids drop faster than they lift, so the blink is uneven on purpose: eased shut, held a moment, eased open. Faces only change while the eyes are closed, and only once a mood has held for a beat, so sweeping across the page doesn’t flicker it. Ordinary blinks come at random intervals, never two in a row.

close 140ms · hold 60ms · open 210ms · mood holds 140ms · 260ms between changes · every 4.5–9s

07

Eyes that pay attention

The eyes glide toward what you’re looking at: the pointer, or the project you’re hovering. They travel further sideways than up and down, because the eyes are wider than they are tall. They widen slightly as you come near. Left alone, it glances around: at the headline, at the work, or at nothing at all. The faces that shouldn’t follow (happy, asleep) hold still, so nothing slides back to centre.

reach 4.5 × 3.2px · full reach at 260px · pupils +5% within 320px · glances after 8s, every 3–6s

08

A shadow, in orange

The bar beneath it is its shadow, and it obeys the float: narrower and fainter as the ghost rises, fuller as it settles. When it flies, the shadow stays on the ground and fades with the distance. Over its empty home it brightens: that’s where it lands. It’s the one orange thing on the page, the site’s colour for what’s different.

width 0.85 − 0.15 × lift · fades over 140px of flight · 0.95 as the landing cue

09

Moods with reasons

Every face answers something real. On the site it beams at Radar, is smitten with Radar’s rules, smug about Terminal, and happy at “Say hello”. Rush it and it startles; poke it and it’s surprised; three quick pokes make it dizzy; five, and it sinks behind its shadow, a portal opens at its hem, and it climbs back out, smug (so does clicking the bar, pushing it down into the bar, or the Portal button). Leave it alone and it yawns, then sleeps, with a “z” drifting up now and then.

startle above 4.2px/ms within 220px, then a 2.8s pause · pokes counted over 1.6s · yawn at 17.8s · asleep at 20s · a “z” every 1.7s

10

Flight

Double-click it on the homepage, or use Fly here. It follows the pointer lazily, three-quarters of a body-width off to one side so it never sits under your cursor, tilting into its heading as the hem streams. Leave the pointer still and it floats home; Escape sends it straight back. Landing only arms once you’ve moved away, because a double-click leaves you hovering over its home.

critically damped k5 · capped at 520px/s · tilt up to 10° · home after 4s still

11

Peekaboo

On the homepage, the About panel slides over the ghost as you scroll. When it comes back into view, it reacts as if you caught it: it reacts as if caught out: a surprised hop, then a small smile. On the site it plays once, ever (remembered in the browser), so it never gets repetitive; here it plays every time. Try Hide.

hidden for 0.4s or more · once, ever, on the site

12

Care you don’t see

With reduced motion on, the faces still change and nothing moves. Its label speaks its mood to screen readers. It stops when the tab is hidden, finds the panels that can cover it once rather than every frame, and costs a fraction of a millisecond a frame. It’s one SVG and one file of plain JavaScript, with no libraries.

462 lines · 0 dependencies · paused when hidden · reduced motion respected