You Might Also Like
Three.js Scroll Pendulum Wave — GSAP Physics Scrub
Three.js Scroll Pendulum Wave · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
How to Build a Scroll-Scrubbed Pendulum Wave With Three.js and GSAP

The Three.js Scroll Pendulum Wave snippet recreates the famous physics demonstration — fifteen pendulums of graduated frequency swinging from one beam, drifting from a perfect traveling wave into apparent chaos and snapping back into unison — with one twist: scroll position *is* time. GSAP's ScrollTrigger scrubs a simulated-time value from 0 to 30 seconds, so users can play the demonstration forward, pause at any instant, and run physics backwards, which no video of the real apparatus can do.
The pendulum wave equation
The classic apparatus works because pendulum i completes exactly 26 + i oscillations in one shared cycle. This snippet encodes that directly: each pendulum's angular frequency is (26 + i) / 30 × 2π rad/s, and its swing angle at any instant is A × sin(ω × T). At T = 0 all fifteen are in phase; small frequency differences accumulate into traveling snakes, then two interleaved groups swinging opposite ways, then apparent randomness — and at exactly T = 30 every phase difference is a whole multiple of 2π and the line snaps back together. None of this is choreographed; it falls out of fifteen sine evaluations per frame.
Scrubbing time instead of animating it
Because the swing angle is a closed-form function of T (no integration, no accumulated state), simulated time can jump anywhere without error: scroll fast and the wave fast-forwards, stop mid-scroll and it freezes at a physically exact configuration, scroll up and entropy visibly runs backwards into order. This is the deepest version of the derive-from-one-value pattern in this series — where the gear train scrubs an angle, this scrubs the independent variable of a physics equation. Any simulation expressible in closed form gets scrub-reversibility for free.
Rotation about the pivot, not bob positioning
Each pendulum is a Group positioned at the beam with the wire and bob as children hanging below the group origin. Setting group.rotation.x swings the entire assembly about the pivot — the wire tilts and the bob traces its arc automatically through the transform hierarchy, the same scene-graph leverage used by the chips in the exploded view. Computing bob arc positions by hand would need trigonometry per frame and would leave the wire pointing the wrong way.
Emissive glow at the turning points
Real pendulum bobs seem to hang at their extremes, where velocity crosses zero. To emphasize this, each bob's emissiveIntensity scales with |angle| / A — bobs glow brightest exactly where they pause. Since neighboring pendulums reach extremes at different instants, the glow sweeps along the line as its own secondary wave, making phase relationships readable even from the front view. Bob hues sweep a blue-to-magenta HSL range so each pendulum is identifiable across camera moves.
A camera move that changes what you understand
The pendulum wave looks completely different from different angles: side-on you see the swing amplitude; front-on you see the phase pattern — the snakes and interleaving groups. The camera slides 1.2 radians from profile toward front view across the cycle, so the scroll journey doubles as a lesson in why this demonstration is filmed from the end of the beam. A small clock-driven bob keeps the shot alive during scroll pauses, consistent with the idle-motion convention across this series, like the drifting clouds of the planet approach.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You do not need to rediscover why 26, 27, 28... oscillations per cycle produce snakes and realignment. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain the phase mathematics, why closed-form angles make time scrubbing exact, or what happens if you change BASE_OSC. The same assistant can extend the demonstration — adding motion-blur trails by rendering bob positions at several recent T values with fading opacity, a slider that decouples from scroll for touch-free replay, damping that makes amplitude decay with T for realism, or labels that appear at notable instants (half-cycle interleave, third-cycle triplets). It can also retheme bob hues to a brand ramp. 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:
Build a "scroll-scrubbed pendulum wave" 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, PerspectiveCamera (resized with aspect on window resize), ambient plus directional light, and a metallic support beam (BoxGeometry) at the top.
- 15 pendulums hanging from the beam. Each is a THREE.Group with its origin AT the pivot: a thin CylinderGeometry wire child and a SphereGeometry bob child at the wire's end. Bob materials use an HSL hue ramp (blue → magenta) with an emissive component.
- Pendulum wave frequencies: pendulum i has angular frequency (26 + i) / 30 × 2π rad/s, so the line realigns exactly at T = 30. Slightly longer wires for slower pendulums.
- One GSAP tween (ease "none") scrubbing a SIMULATED TIME value from 0 to 30 on a ScrollTrigger with pin: true, scrub ~0.4, end ~+=500%.
- Each frame set group.rotation.x = 0.55 × sin(freq × T) per pendulum — closed form, no integration — and set emissiveIntensity = 0.4 + 1.1 × |angle| / 0.55 so bobs glow at their turning points.
- A camera that slides ~1.2 radians from a side profile toward a front view across the cycle (lookAt beam center), with a small clock-driven bob so the shot idles alive.
- A HUD showing T to one decimal place, and an intro overlay fading once T passes ~0.15.
- Confirm: pausing scroll freezes an exact configuration, and scrolling backwards runs the wave in reverse until the pendulums realign at T = 0.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
- 1Load the three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js in that order before the snippet JS.
- 2Paste HTML, CSS, and JSFifteen pendulums hang motionless and aligned from a beam, viewed from the side, with a T = 0.0 S HUD.
- 3Scroll to start timeThe line swings as one, then a traveling snake forms as small frequency differences accumulate.
- 4Pause anywhereStop scrolling and the system freezes at an exact instant — inspect the two interleaved counter-swinging groups around mid-cycle.
- 5Scroll backwardsPhysics runs in reverse: chaos reassembles into the traveling wave and back to unison, frame-perfect.
- 6Retune the demonstrationChange N, CYCLE, or BASE_OSC — e.g. 20 pendulums over 40 s starting at 35 oscillations — and the wave patterns recompute automatically.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Each pendulum completes a whole number of oscillations in the shared 30-second cycle — 26 for the first, 27 for the next, and so on. At T = 30 every pendulum has finished its integer count, so every phase difference is a multiple of 2π and the line is exactly as it was at T = 0. The intermediate snakes and interleaved groups are just the fractional phase differences passing through simple ratios like 1/2 and 1/3.
Because the swing angle is closed-form — angle = A·sin(ω·T) — there is no integrator carrying state from frame to frame. Any T gives an exact configuration, so jumping time around (which is what fast or reversed scrolling does) cannot accumulate error. A force-integrated simulation would need fixed timesteps and could not run backwards for free.
Each pendulum is a Group whose origin sits at the pivot on the beam; the wire and bob are children positioned below it. Setting group.rotation.x rotates the whole assembly about the pivot, so the wire tilts and the bob follows its arc purely through the transform hierarchy — no per-frame trigonometry to place the bob, and the wire always points at it by construction.
Each bob's emissiveIntensity is 0.4 + 1.1 × |angle| / A, peaking at the swing extremes where the pendulum momentarily pauses. Neighboring pendulums hit their extremes at slightly different times, so the brightness peak travels along the line as its own wave — a secondary visualization of the same phase relationships that create the position snakes.
Yes. Export with the JSX, Vue, Angular, or Tailwind buttons. Create the pendulum groups and ScrollTrigger in a mount effect against a canvas ref; keep sim.t in a plain object (not state) since it updates every scrubbed frame. On cleanup kill the ScrollTrigger, dispose wire and bob geometries and materials, and call renderer.dispose() so the pin and WebGL context release cleanly.