GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

css 3d transform Scenes: 9 Pointer Miniatures

These css 3d transform scenes turn pointer position into a camera angle: a Ferris wheel, a rippling terrain, or an arch bridge miniature that spins with no

Auto-plays · click a tile to jump to its section · all nine in one zip

Why this order

The nine boards are stacked in the order the perspective math itself stacks up, not by flashiness. Boards 01 and 02 are the bedrock: rotate one point around a circle, or one grid point over a heightfield, then divide a focal length by depth to place it on screen. Boards 03 and 04 put many points under shared rules — a two-line rotation matrix for the molecule, three flocking forces recomputed every frame for the birds. Boards 05 and 06 move the points onto curved surfaces, where a torus is two angles and a staircase is a golden-angle walk. Boards 07 through 09 hand the whole scene over to physics and waves: a bead integrating gravity along a track, islands bobbing on separate phases, an arch drawn by a single sine.

Every board here is an arithmetic imitation of css 3d perspective — the divide that makes near things large and far things small happens in one line of script, not in a style sheet. The widest movers measured 29.176% and 28.832% cumulative changed area, and the quietest still covers 23 of 24 frame gaps. The pixel-level relatives of these boards live in the canvas scene effects set, and the DOM-side cousins that rotate real page elements are in pointer-following 3d rotation.

01Ferris wheel scene — sixteen spokes through perspective

What you see

Move the pointer left and right to change the spin speed, up and down to tilt the camera. Sixteen spokes and eight gondolas keep their painter's order while every gondola hangs plumb below its attach point — amusement and event landing heroes.

Ferris wheel scene — 16 spokes cycling through perspective
Open in a new tab

01 · Ferris wheel scene — 16 spokes cycling through perspective — Measured area 23.298% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

rotate3dsortrequestAnimationFrame

Where to look in the code

The record shows 23.298% cumulative changed area, 11.328% in the widest single frame, across 23 of 24 frame gaps with zero CSS animations.

function wheelPoint(theta: number, radius: number): Pt3 {
  const x = Math.cos(theta) * radius;
  const y = Math.sin(theta) * radius;
  const ct = Math.cos(TILT), st = Math.sin(TILT);          // x-axis tilt — lays the wheel back
  const y1 = y * ct, z1 = y * st;
  const ca = Math.cos(YAW), sa = Math.sin(YAW);            // y-axis twist — the disc stops facing the screen
  return { x: x * ca + z1 * sa, y: y1, z: -x * sa + z1 * ca };
}

function project(p: Pt3): Proj {                          // perspective divide — this one line is the whole trick
  const size = sizeRef.current;
  const s = focalRef.current / (focalRef.current + p.z);
  return { sx: size.w * 0.5 + p.x * s, sy: size.h * 0.56 - p.y * s, s };
}

02Heightfield terrain scene — hills turned by the pointer

What you see

Two stacked sine waves push ridges across a 17×13 grid that orbits with the pointer, while a cream base disc and a cream pin on the tallest cell frame it as a physical model — game and map service intros.

Heightfield terrain scene — mouse-turned rippling hills
Open in a new tab

02 · Heightfield terrain scene — mouse-turned rippling hills — Measured area 28.832% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

focalsinbeginPath

Where to look in the code

The render measured 28.832% cumulative area, 17.519% at the widest frame, moving in 23 of 24 gaps.

function height(i: number, j: number, t: number): number {
  const x = (i / (COLS - 1)) * TAU;      // 0..TAU
  const y = (j / (ROWS - 1)) * TAU;
  return Math.sin(x * F1 + t) * 0.62 + Math.sin(y * F2 + t * 0.8) * 0.38
       + Math.sin((x + y) * 0.9 + t * 1.3) * 0.18;
}

function corner(i: number, j: number, t: number): Corner {
  const size = sizeRef.current;
  const heading = headingRef.current, pitch = pitchRef.current, amp = ampRef.current;
  const wx = (i / (COLS - 1) - 0.5) * 430;
  const wz = (j / (ROWS - 1) - 0.5) * 340;
  const wy = height(i, j, t) * amp;
  const ch = Math.cos(heading), sh = Math.sin(heading);
  const x1 = wx * ch - wz * sh;
  const z1 = wx * sh + wz * ch;
  const a = ALT + pitch;
  const ca = Math.cos(a), sa = Math.sin(a);
  const y2 = wy * ca - z1 * sa;
  const z2 = wy * sa + z1 * ca;
  const s = FOCAL / (FOCAL + z2 + 300);
  return { sx: size.w * 0.5 + x1 * s, sy: size.h * 0.62 - y2 * s, s, h: wy };
}

03Molecule scene — atoms pulsing inside bonds

What you see

Fifteen atoms and eighteen bonds rotate through a two-line matrix while each atom pulses ±5% on its own phase. The pointer nudges the viewing angle and the spin dial sets the base pace — science and education intros.

Molecule scene — atoms drifting into bonds
Open in a new tab

03 · Molecule scene — atoms drifting into bonds — Measured area 10.773% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

projectsortlineTo

Where to look in the code

The record shows 10.773% cumulative area with 89.3 average intensity across 23 of 24 gaps.

function rot(p: Pt3): Pt3 {
  const cy = Math.cos(angYRef.current), sy = Math.sin(angYRef.current);
  const x1 = p.x * cy + p.z * sy;
  const z1 = -p.x * sy + p.z * cy;
  const cx = Math.cos(angXRef.current), sx = Math.sin(angXRef.current);
  const y2 = p.y * cx - z1 * sx;
  const z2 = p.y * sx + z1 * cx;
  return { x: x1, y: y2, z: z2 };
}

function project(p: Pt3): Proj {
  const size = sizeRef.current;
  const s = FOCAL / (FOCAL + p.z + 150);      // perspective divide — near atoms draw larger
  return { sx: size.w * 0.5 + p.x * s, sy: size.h * 0.5 - p.y * s, s };
}

04Boids flock scene — forty birds under three rules

What you see

Forty birds hold a flock through separation, alignment and cohesion recomputed every frame, and the pointer becomes the food point they gather around. The cohesion dial tightens or loosens the swarm — nature and campaign pages.

Boids flock scene — a swarm held by three rules
Open in a new tab

04 · Boids flock scene — a swarm held by three rules — Measured area 28.538% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

requestAnimationFramesqrtsort

Where to look in the code

The render measured 28.538% cumulative area at intensity 115.0, moving in 23 of 24 gaps.

if (cn > 0) {
  ax += (cxs / cn - b.x) * coh * 1.1; ay += (cys / cn - b.y) * coh * 1.1; az += (czs / cn - b.z) * coh * 1.1;  // cohesion
  ax += (vxs / cn - b.vx) * 1.1; ay += (vys / cn - b.vy) * 1.1; az += (vzs / cn - b.vz) * 1.1;                  // alignment
}
ax += sxs * 260; ay += sys * 260; az += szs * 260;                                                              // separation
ax += (st.food.x - b.x) * FOOD_GAIN * coh; ay += (st.food.y - b.y) * FOOD_GAIN * coh; az += (st.food.z - b.z) * FOOD_GAIN * coh;
b.vx += ax * dt; b.vy += ay * dt; b.vz += az * dt;
const sp = Math.sqrt(b.vx * b.vx + b.vy * b.vy + b.vz * b.vz) || 1;
const cl = sp > SPD_MAX ? SPD_MAX / sp : (sp < SPD_MIN ? SPD_MIN / sp : 1);
b.vx *= cl; b.vy *= cl; b.vz *= cl;

05Torus lattice scene — light circling a donut grid

What you see

A donut lattice of 26×13 cells swells as a light phase travels the surface, mint, lilac and pink trading places under smoothstep while a cream highlight ring marks where the light is — product and tech brand heroes.

Torus lattice scene — light flowing over a donut grid
Open in a new tab

05 · Torus lattice scene — light flowing over a donut grid — Measured area 29.176% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

cossinfillStyle

Where to look in the code

The record shows 29.176% cumulative area — the widest of the nine boards — with 25.398% changed inside a single frame.

function tubeR(u: number, v: number, tube: number, now: number): number {
  const ph = (u / TAU) * 2 + (v / TAU) * 3 + now / 1300;
  return tube + Math.sin(ph * TAU) * WAVE_GAIN;
}

function torusPoint(u: number, v: number, tube: number, now: number): P3 {
  const rr = tubeR(u, v, tube, now);
  const ring = 112 + Math.cos(v) * rr;
  return { x: ring * Math.cos(u), y: Math.sin(v) * rr, z: ring * Math.sin(u) };
}

06Spiral stairs scene — treads climbing the golden angle

What you see

Twenty-six treads climb around the golden angle under a camera that looks up from below, each tread a filled trapezoid with a dark front face and a cream lantern crowning the axis — architecture and space intros.

Spiral stairs scene — landings turning upward
Open in a new tab

06 · Spiral stairs scene — landings turning upward — Measured area 24.083% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

rotate3dsortarc

Where to look in the code

The render measured 24.083% cumulative area at intensity 72.1, moving in 23 of 24 gaps.

function treadCorners(i: number, rise: number): { inA: P3; inB: P3; outA: P3; outB: P3 } {
  const st = stateRef.current;
  const ang = i * GOLD + st.spin;
  const y = i * rise * RISE_K;
  const wide = 0.9 - i / STEPS * 0.42;    // lower treads claim a wider angular span
  const a0 = ang - wide, a1 = ang + wide;
  return {
    inA: { x: Math.cos(a0) * RIN, y: y, z: Math.sin(a0) * RIN },
    inB: { x: Math.cos(a1) * RIN, y: y, z: Math.sin(a1) * RIN },
    outA: { x: Math.cos(a0) * ROUT, y: y, z: Math.sin(a0) * ROUT },
    outB: { x: Math.cos(a1) * ROUT, y: y, z: Math.sin(a1) * ROUT }
  };
}

07Coaster track scene — a bead racing its own curve

What you see

A glowing bead races a closed 200-sample track, gaining speed on descents and losing it on climbs while a 26-point trail fades behind it. The gravity dial retunes the whole run — game and experiential campaigns.

Coaster track scene — a light bead racing a curve
Open in a new tab

07 · Coaster track scene — a light bead racing a curve — Measured area 18.396% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

quadraticCurveToarcsort

Where to look in the code

The record shows 18.396% cumulative area, 6.998% in the widest frame, moving in 23 of 24 gaps.

function step(dt: number): void {
  const st = stateRef.current;
  const track = trackRef.current, cum = cumRef.current;
  const p0 = track[st.segIdx], p1 = track[(st.segIdx + 1) % SAMPLES];
  const segLen = cum[st.segIdx + 1] - cum[st.segIdx];
  const slope = (p1.y - p0.y) / segLen;              // gains speed downhill (−)
  const g = gravValRef.current * 2;                  // dial % → px/s²
  st.beadV += -g * slope * dt;
  st.beadV *= 1 - 0.05 * dt;
  if (st.beadV < VMIN) st.beadV = VMIN;
  st.beadS += st.beadV * dt;
}

08Floating islands scene — seven isles on their own waves

What you see

Seven islands bob on individually phased waves over a fixed starfield and a moon glow, with twelve fireflies riding the same coordinate space in z order — fantasy and game landings.

Floating islands scene — isles bobbing over waves
Open in a new tab

08 · Floating islands scene — isles bobbing over waves — Measured area 8.134% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

translate3dscalesin

Where to look in the code

The render measured 8.134% cumulative area with 97.9 average intensity, moving in 23 of 24 gaps.

function project(p: P3): Proj {
  const st = stateRef.current;
  const s = 320 / (320 + p.z + 40);   // FOCAL=320, BACK=40
  return { sx: st.W * 0.5 + p.x * s, sy: st.H * 0.52 - p.y * s, s: s };
}

function islandWorld(isl: { baseY: number; phase: number; speedK: number; ang: number; r: number }, now: number): P3 {
  const amp = ampValRef.current;
  const by = isl.baseY + Math.sin(now / 1000 * FLOAT_SPEED * isl.speedK + isl.phase) * amp;
  return { x: Math.cos(isl.ang) * isl.r, y: by, z: Math.sin(isl.ang) * isl.r };
}

09Arch bridge scene — one sine holding twenty-two blocks

What you see

Twenty-two stone blocks follow one sine curve into an arch, spandrel posts lifting the plank deck, twin railings and a cart shuttling across while the water below ripples in four rows — infrastructure and city intros.

Arch bridge scene — a bridge swept by pointer angle
Open in a new tab

09 · Arch bridge scene — a bridge swept by pointer angle — Measured area 4.622% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

rotate3dsinlineTo

Where to look in the code

The record shows 4.622% cumulative area, the quietest of the nine boards, yet still 23 of 24 gaps moving with zero CSS animations.

function archY(t: number, height: number): number { return Math.sin(t * Math.PI) * height; }
function archX(t: number): number { return (t - 0.5) * SPAN; }

function project(p: P3): Proj {
  const st = stateRef.current;
  const s = 300 / (300 + p.z + 90);   // FOCAL=300, BACK=90
  return { sx: st.W * 0.5 + p.x * s, sy: st.H * 0.62 - p.y * s, s: s };
}

Where it breaks — the trap

One rotation leaves a flat ellipse

The most common failure is rotating once where two rotations are needed. Lay the wheel of 01 back on its x axis alone and the screen still shows a flattened ellipse; only after the y-axis yaw turns the disc away from facing the viewer does depth arrive and the scene earn its place among css 3d effects. The terrain of 02 hides the same lesson one layer deeper: a heading rotation alone would slide the hills sideways like a sticker, so an altitude rotation has to follow before ridges roll over the way physical ground does.

No z-sort and the blocks tunnel through the bridge

The first version of 09 drew the stone blocks straight out of the array with no depth ordering, and turning the pointer made rear blocks punch through the front lip of the arch every few degrees. Collecting every drawable — blocks, spandrel posts, deck planks, railings, cart — into one basket sorted by z each frame is what restored the painter's order; 01, 04, 05 and 06 keep the same discipline for spokes, birds, cells and treads. The full source of all nine boards and their React ports sits in the password archive below, and the password is afcdc6hj.

A wave gain too large folds the surface inward

On 05 the tube radius is not a constant: it is the dial value plus a sine of the flowing phase. Push the wave gain far past its shipped 4.2 and the radius crosses zero, folding the donut's surface through itself into a shape that reads as broken glass rather than a crystal. The same cliff edge exists on 08, where a float amplitude past the dial's ceiling snaps the bobbing into a pumping motion — which is why both dials stop where they stop.

Variations — changing one number

One number each, and a board changes character.

Name Value changed Feel
Focal length 01 focal 240 → 140 Near gondolas swell and the wheel crowds the frame
Wave amplitude 02 amp 13 → 26 Ridges grow into mountains under the same camera
Cohesion 04 cohesion 0.8 → 1.4 The flock pulls tight into one dense ball

Accessibility (reduced-motion)

Every board runs on a canvas carrying role="application" with tabindex="0", an outline marking focus, and arrow-key control alongside the pointer. Under prefers-reduced-motion: reduce the requestAnimationFrame loop never starts and each board draws one static frame instead: 01 parks the wheel with its gondolas seated, 02 fixes the waves at the instant its ridges read best, 03 freezes the molecule at an angle where no atom hides another, 04 warms the simulation 240 steps and shows the flock settled around the food, and 05, 06, 07, 08 and 09 each leave their finished scene standing. A preference switched after load is honored the next time the pointer or a dial touches the board. The rule for how much motion to remove follows WCAG's animation from interactions.

FAQ

Are the css 3d transform examples in this post ready to paste?

Yes. Each board runs on plain Canvas 2D drawing calls and requestAnimationFrame with no library and no CDN line (see the Canvas API reference), so the item code blocks and the archive's index.html files run as-is once pasted.

Do the scenes answer a finger the same way they answer a mouse?

They read pointer events, one event set covering mouse, pen and touch, so a finger drives the same camera angles; the interaction fields set touch-action: none so a drag rotates the scene instead of scrolling the page.

Why does the pointer change speed on some boards and angle on others?

Each scene maps the pointer to whatever its math needs: 01 and 05 use left-right as spin acceleration, 02 and 09 orbit the camera, 03 adds a nudge on top of a base spin, and 04 feeds the pointer to the flock as a food point. The mapping is chosen per scene, not shared.

Further reading

Enter the archive password

The password is inside this article. You will find it as you read.