webdevpuneet.com

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

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

Copy & paste

Source Code

<section class="rbx-stage" id="rbxStage">
  <div class="rbx-intro"><p>Scroll ↓ to assemble the cube</p></div>
  <canvas id="rbxCanvas"></canvas>
  <div class="rbx-hud"><span id="rbxPct">0</span>% ASSEMBLED</div>
</section>
<section class="rbx-bottom"><p>Solved.</p></section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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.