Three.js Scroll Rubik’s Cube Assembly — GSAP Scrub

Three.js Scroll Rubik’s Cube Assembly · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

27 BoxGeometry cubelets with per-face material arrays — colors outside, shared dark plastic inside
Seeded sin-hash scatter: identical cloud and flight paths on every load, keeping the scrub reversible
Hand-rolled center-out stagger from distance-based delays, scrubbing cleanly in both directions
Tumble rotation multiplied by (1 − progress) so every cubelet lands at exactly zero rotation
Phase split derived from one value: 80% assembly window, 20% turntable presentation spin
Camera dolly-in during presentation tied to the same derived phase value
HUD percentage counts actual arrivals (local progress > 0.985), not raw scroll progress
Clock-driven idle rotation keeps the cloud and cube alive while scrolling is paused

About this UI Snippet

How to Build a Scroll-Scrubbed Rubik’s Cube Assembly With Three.js and GSAP

Screenshot of the Three.js Scroll Rubik’s Cube Assembly snippet rendered live

The Three.js Scroll Rubik’s Cube Assembly snippet scatters 27 colored cubelets across a wide spherical cloud and lets scroll pull them into a solved 3×3×3 Rubik’s cube, finishing with a turntable presentation spin — all scrubbed through one GSAP ScrollTrigger value with a hand-rolled, fully reversible stagger. It shares DNA with the particle assembly snippet but works at mesh scale: 27 solid boxes with per-face materials rather than thousands of points, which makes material choices and stagger design the interesting problems.

Per-face materials so the interior reads as plastic

A real Rubik’s cube only shows color on outward faces; the interior is black plastic. Each BoxGeometry cubelet receives an array of six materials, and a face gets its classic color only when the cubelet sits on that face of the 3×3×3 grid (x === 1 gets red, y === 1 gets white, and so on) — every other side shares one dark material instance. This matters mid-flight: as scattered cubelets tumble toward home you see their dark interior sides, and when the cube closes up, the thin dark gaps between stickers appear automatically, with no texture work.

Deterministic scatter from a seeded hash

Scatter positions come from a tiny sin-hash function (Math.sin(seed × 127.1 + 311.7) × 43758.5453, take the fraction) instead of Math.random(). Each cubelet derives its scatter direction, radius, and tumble spin from its index, so the cloud is identical on every page load. That determinism is what keeps the scrub honest: scroll down, up, and down again and every cubelet retraces exactly the same flight path, something unseeded randomness regenerated on any rebuild would silently break.

A hand-rolled stagger that scrubs in both directions

GSAP timelines offer stagger, but this snippet computes stagger manually so everything stays a pure function of one scrubbed value: each cubelet's local progress is (build − delay) / (1 − 0.35) clamped to 0–1 and shaped by an ease-out cubic, where delay is proportional to the cubelet's distance from the cube center. The result is a cube that grows from its core outward — the center cubelet snaps in first, corners arrive last — and reverses into an outward explosion when scrolling up. Position uses lerpVectors(scatter, home, lp) while rotation multiplies the tumble spin by (1 − lp), so every cubelet lands at exactly zero rotation.

A phase split: 80% build, 20% presentation

The scrubbed progress is split into two windows, the same pattern as the staging in product stages: the first 80% drives assembly, the final 20% drives a full 360° turntable rotation plus a 5-unit camera dolly-in. Because present is derived (not a separate tween), the handoff point cannot drift, and reversing through it un-spins the cube before disassembling it.

A live percentage from arrival counting

The HUD counts cubelets whose local progress exceeds 0.985 and shows the fraction as a percentage. Counting arrivals rather than echoing raw scroll progress makes the number honest — it sits at 0% while everything is still tumbling, climbs as pieces click in center-out, and reaches 100% just as the presentation spin begins, giving the user a satisfying sense of completion mechanics similar to the gate counter in the portal gate sequence.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not need to derive reversible stagger math from scratch. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain why the scatter uses a seeded sin-hash, how the per-cubelet delay creates a center-out build, or why rotation is multiplied by (1 − progress) instead of tweened separately. The same assistant can extend the scene — making the assembled cube perform actual face turns after assembly, swapping the six colors for your brand palette while keeping the dark interior, scaling up to a 4×4×4 with delays recomputed automatically, or adding a click handler that re-explodes the cube with gsap.to on the same progress object. Treat the code as a starting point to interrogate and reshape, not a finished artifact.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "scroll-scrubbed Rubik's cube assembly" in plain HTML, CSS, and JavaScript using Three.js and GSAP's ScrollTrigger plugin, all loaded from a CDN (no bundler, no build step).

Requirements:
- A pinned full-viewport section with a canvas, WebGLRenderer, and PerspectiveCamera resized (with aspect) on window resize; ambient plus one directional light.
- 27 BoxGeometry cubelets in a 3×3×3 grid (size ~1.9, gap ~0.12). Each cubelet takes an ARRAY of six materials: the classic face color only when the cubelet lies on that outer layer (x=+1 red, x=−1 orange, y=+1 white, y=−1 yellow, z=+1 green, z=−1 blue), all other faces sharing one dark plastic material instance.
- Deterministic scatter: derive each cubelet's scatter direction, radius (~16–34), and tumble spin vector from its index via a seeded sin-hash (fract(sin(seed*127.1+311.7)*43758.5453)) — no Math.random — so every load and every scrub reversal retraces identical paths.
- One GSAP tween (ease "none") scrubbing a progress value 0→1 on a ScrollTrigger with pin: true and end ~+=400%.
- Hand-rolled stagger in the frame loop: each cubelet's delay is proportional to its home distance from the center; local progress = clamp((build − delay)/0.65) shaped by ease-out cubic; position = lerpVectors(scatter, home, lp); rotation = spin × (1 − lp) so pieces land at exactly zero rotation. The cube must visibly grow center-out.
- Phase split derived from the same value: first 80% assembles, last 20% spins the whole group a full 2π turntable while the camera dollies in ~5 units.
- A HUD showing percent assembled by COUNTING cubelets whose local progress exceeds 0.985, plus an intro overlay that fades once scrolling starts and a slow clock-driven idle rotation.
- Confirm scrolling back up un-spins the cube then explodes it outward along the identical flight paths.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Load the three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js in that order before the snippet JS.
  2. 2
    Paste HTML, CSS, and JSA pinned view shows 27 colored cubelets tumbling in a slow-rotating scattered cloud with a 0% ASSEMBLED HUD.
  3. 3
    Scroll to assembleCubelets fly home center-out — the core snaps in first, corners last — while each one untumbles to land at zero rotation.
  4. 4
    Watch the finishPast 80% progress the completed cube performs a full turntable spin as the camera dollies in five units.
  5. 5
    Scroll back upThe cube un-spins, then explodes outward along the exact same flight paths — the seeded scatter makes reversal pixel-identical.
  6. 6
    Restyle itSwap the six FACE colors for a brand palette, or change STEP and the delay multiplier to alter cube tightness and stagger spread.

Real-world uses

Common Use Cases

SaaS "everything comes together" heroes
Map the 27 pieces to product modules assembling into one platform — a solid alternative to a scroll tile assemble built in 2D.
SHOP
Product configurator and toy-brand pages
The turntable finish doubles as a product presentation, comparable to the staged reveal in product stages.
Agency and portfolio intros
A recognizable object assembling from chaos is an instant metaphor for "we organize complexity" pitches.
Teaching reversible stagger design
Shows why scrubbed animation needs delays computed from one value rather than fired timeline staggers, and why scatter must be seeded.
Puzzle game landing pages
Lead a puzzle or brain-training app with the most famous puzzle in the world solving itself as users scroll.
Section dividers in long scrollytelling
Run the assembly between chapters as a palate cleanser, then hand off to a scroll camera path scene.

Got questions?

Frequently Asked Questions

The scrub must be a pure function of scroll position: scrolling down, up, and down again has to retrace identical flight paths. Math.random() would generate a new cloud on every reload and make per-cubelet phase relationships unstable. The sin-hash (fract of Math.sin(seed × 127.1 + 311.7) × 43758.5453) gives each index a fixed pseudo-random direction, radius, and spin — random-looking, but permanent.

GSAP staggers offset start times inside a playing timeline, but this scene is scrubbed, not played. Instead each cubelet stores a delay proportional to its distance from the cube center, and every frame computes local progress as (build − delay) / 0.65, clamped and eased. That makes stagger a pure function of the single scrubbed value, so it reverses perfectly — corners leave first on the way back out.

A real Rubik's cube is black plastic with colored stickers on outward faces only. Passing a material array to a Mesh assigns one material per BoxGeometry face group: a cubelet gets red only if it sits on the x = +1 layer, white only on y = +1, and so on, with all interior faces sharing a single dark material instance. Mid-flight tumbles show dark undersides, and the assembled cube shows sticker-gap lines for free.

Position uses lerpVectors(scatter, home, lp) which hits home exactly at lp = 1, and rotation is spin × (1 − lp), which reaches exactly zero at the same moment. Since the ease-out cubic maps input 1 to output 1 precisely, there is no residual drift to snap or correct — arrival is mathematically exact, not approximated with a tolerance.

Yes. Use the JSX, Vue, Angular, or Tailwind export buttons. Build the cubelet loop and ScrollTrigger inside a mount effect against a canvas ref; on cleanup kill the ScrollTrigger, dispose each BoxGeometry and every non-shared face material (dispose the shared dark material once), and call renderer.dispose() so the pin and WebGL context release on unmount.