Three.js Scroll Tornado Vortex — GSAP Particle Funnel
Three.js Scroll Tornado Vortex · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
How to Build a Scroll-Driven Tornado Vortex With Three.js and GSAP

The Three.js Scroll Tornado Vortex snippet gathers 2,600 dust particles scattered across a dark plain into a spinning, wandering tornado funnel as the user scrolls — complete with tumbling debris that lifts off only once the storm passes half strength, and a dispersal phase that lets the storm die at the end of the scroll. GSAP's ScrollTrigger scrubs one intensity value; the funnel itself is pure parametric math evaluated per frame.
Particles store parameters, not positions
The core design decision: no particle stores its position, velocity, or any integrated state. Each stores five permanent parameters — a height fraction in the funnel, an orbital phase, a radius jitter, a speed multiplier, and a scattered rest position on the ground — all generated by the seeded sin-hash used throughout this series (see the Rubik's cube assembly). Every frame computes each particle's funnel position from those parameters plus clock time, then lerps between rest position and funnel position by eased intensity. The tornado is therefore a pure function of scroll and clock: scrub to any point and the storm is exactly what it should be, with no simulation to diverge or explode.
The funnel profile is one function
The tornado's silhouette lives in a single line: funnelRadius(h) = 1.2 + h^1.6 × 14 — narrow at the ground, flaring wide at the top, with the 1.6 exponent controlling how "stalky" the profile looks. Angular speed also varies by height, 2.2 + (1 − h) × 3.2, so the base spins visibly faster than the crown, matching how real vortices conserve angular momentum as radius shrinks. Changing two numbers redesigns the storm.
An intensity arc with an ending
Rather than mapping scroll linearly to strength, intensity ramps 0→1 across the first 70% of scroll, holds full force through 85%, then falls back to zero over the final 15% — so the storm has a narrative arc: gathering, rampage, dispersal. The raw intensity passes through a smoothstep (i² × (3 − 2i)) before use, removing the velocity discontinuities at the ramp boundaries. The funnel core also wanders the plain on slow sine paths scaled by intensity, so a strong storm stalks while a weak one stays put — and the camera's look-at target tracks the wandering core.
Debris with a lift-off threshold
Twelve box-geometry debris chunks behave differently from dust: below intensity 0.5 they only tremble in place (a high-frequency positional shiver scaled by 1 − lift), and only above it do they lerp from their rest spots into the funnel's rotation, tumbling with accumulating rotation. That threshold creates the storm's most legible beat — the moment the ground itself starts coming apart — and demonstrates how one scrubbed value can gate qualitatively different behaviors, the way the voxel build gates per-block slices.
Atmosphere from light, fog, and camera
A cold point light above the funnel flickers with a sine tremor scaled by intensity, standing in for internal lightning; FogExp2 matched to the background swallows the plain's edge. The camera closes from 40 to 30 units and rises as the storm builds, on top of a slow permanent orbit — motion layering consistent with the rest of the series, like the ocean dive's sway. For a calmer take on scroll-gathered particles, compare the particle assembly snippet, which pulls points into a shape instead of a storm.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You do not need to invent stateless vortex math. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain why positions are computed rather than integrated, how the funnel profile function shapes the silhouette, or how the debris threshold gates lift-off. The same assistant can push the storm further — a second counter-rotating outer particle shell, ground dust rings that ripple outward from the wandering core, color grading the dust darker as intensity rises, or replacing scroll with microphone amplitude so the storm reacts to sound. If you need more particles, ask it to move the per-particle math into a custom ShaderMaterial vertex shader with intensity as a uniform. 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-driven tornado vortex" 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 resize), cool ambient light, a PointLight above the funnel, a dark CircleGeometry ground, and FogExp2 matched to the background.
- ~2,600 particles in one THREE.Points with a position BufferAttribute. Each particle stores ONLY permanent parameters from a seeded sin-hash (no Math.random): height fraction, orbital phase, radius jitter, speed multiplier, and a scattered ground rest position. No stored velocities.
- One GSAP tween (ease "none") scrubbing p 0→1 on a ScrollTrigger with pin: true and end ~+=450%.
- Derive intensity with an arc: ramp 0→1 over p in [0, 0.7], hold 1 in [0.7, 0.85], fall to 0 in [0.85, 1]; pass it through smoothstep before use.
- Each frame compute every particle's funnel position: y = heightFrac × 30 × intensity; radius = (1.2 + heightFrac^1.6 × 14) × jitter; angle = phase + time × (2.2 + (1 − heightFrac) × 3.2) × speed × (0.15 + intensity); then LERP between rest position and funnel position by intensity. Funnel core (cx, cz) wanders on slow sine paths scaled by intensity.
- 12 BoxGeometry debris chunks with lift = max(0, (intensity − 0.5) × 2): below threshold they tremble in place with a high-frequency shiver, above it they lerp into a funnel orbit at 85% radius and accumulate tumble rotation.
- Storm light intensity flickers with a sine scaled by intensity; camera orbits slowly, closing from 40 to ~30 units and rising as the storm builds, lookAt tracking the wandering core.
- An INTENSITY % HUD from the derived intensity and an intro overlay fading at p > 0.02.
- Confirm scrolling back re-disperses every particle to its exact rest spot.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 JSA pinned dark plain shows dust scattered flat on the ground, debris chunks at rest, and an INTENSITY 0% HUD.
- 3Scroll to gather the stormDust spirals up into a funnel — narrow at the base, flaring at the crown — while the core begins to wander the plain.
- 4Cross half intensityDebris chunks stop trembling and lift off into the rotation, tumbling as the internal lightning flickers harder.
- 5Ride out the arcThe storm holds full force through 85% of the scroll, then disperses — particles settle back to their exact rest spots.
- 6Redesign the stormEdit funnelRadius() and the height-speed formula to reshape the silhouette and spin profile; raise COUNT if your audience runs desktop GPUs.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Integrated state (position += velocity) breaks under scrubbing: fast scrolls skip frames, reverse scrolls would need inverse forces, and error accumulates. Storing permanent parameters (height fraction, phase, jitter, speed, rest position) and computing position fresh each frame from parameters + intensity + clock makes the storm a pure function — any scroll position yields an exact, repeatable configuration, forwards or backwards.
One function: funnelRadius(h) = 1.2 + h^1.6 × 14, where h is the particle's height fraction. The 1.2 floor keeps a visible core at the ground, the exponent 1.6 makes radius grow slowly near the base then flare toward the crown. Multiply by each particle's jitter factor so the wall has thickness instead of being a perfect surface of revolution.
Debris computes lift = max(0, (intensity − 0.5) × 2): zero below half strength, ramping to one at full. Below the threshold, chunks only get a high-frequency positional shiver (scaled by 1 − lift) so they tremble; above it they lerp from rest into a funnel orbit and accumulate tumble rotation. One scrubbed value gates two qualitatively different behaviors.
A linear map means the storm is strongest exactly at the last pixel of scroll, which feels unfinished — the user leaves mid-rampage. The 70/15/15 arc gives the sequence an ending: dust settles back to its exact rest spots (the lerp target at intensity 0), so the pinned section closes on calm and hands off cleanly to the content below.
Yes. Export via the JSX, Vue, Angular, or Tailwind buttons. Build the particle arrays and ScrollTrigger inside a mount effect against a canvas ref. The Float32Array and parts array should live in the effect scope, not state. On cleanup kill the ScrollTrigger, dispose the points geometry/material and each debris mesh, and call renderer.dispose().