Three.js Scroll Möbius Ride — Camera on a One-Sided Surface

Three.js Scroll Möbius Strip Ride · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

True Möbius parametrization — the u/2 half-angle term builds the one-sided surface from its equation
Indexed 260×10 BufferGeometry grid with computed normals, rendered DoubleSide
Camera frame derived from finite-difference surface tangents — camera.up follows the local normal, no world up
The ride rolls fully inverted at one lap and recovers at two, experiencing the topology rather than asserting it
Lap-two normal negation keeps the surface underfoot across the parametric seam
Single boundary edge traced over 4π with alternating v-sides — one closed loop, as topology demands
36 runway dashes brightening near the rider with proper 2π angular-distance wrapping
Scrub-honest: pause inverted at the halfway point, reverse to unwind the roll

About this UI Snippet

How to Build a Scroll-Driven Möbius Strip Ride With Three.js and GSAP

Screenshot of the Three.js Scroll Möbius Strip Ride snippet rendered live

The Three.js Scroll Möbius Strip Ride snippet puts the camera on the centerline of a mathematically real Möbius strip and lets scroll drive it around — with the twist that makes the topology famous: after one full ring lap the camera is riding the *underside* of the surface, upside down relative to where it started, and only after a second lap (u = 4π) does it come home. GSAP's ScrollTrigger scrubs the ride parameter; the geometry, the roll, and the two-lap structure all fall out of the parametrization.

Building the strip from its equation

The surface is generated directly from the Möbius parametrization: for ring angle u and width coordinate v, the point is ((R + vW·cos(u/2))·cos u, vW·sin(u/2), (R + vW·cos(u/2))·sin u). The u/2 half-angle is the entire secret — the strip's cross-section rotates half a turn while u makes a full turn, joining the ribbon to itself with a flip. A 260×10 grid of these points becomes an indexed BufferGeometry with computeVertexNormals(), rendered DoubleSide because a one-sided surface, ironically, needs both GL faces drawn.

A camera frame with no world up

Riding a surface that turns you upside down means camera.lookAt with the default world-up would fight the roll and flip violently at the poles of the twist. Instead the snippet constructs the rider's frame from the surface itself: two finite-difference tangents (one step along u, one along v) cross-multiplied into a local surface normal, the camera positioned 1.5 units along that normal, camera.up set to it, and the look-at aimed at a point further along the centerline lifted by the same normal. The camera therefore rolls exactly as the surface rolls — by the end of lap one it is fully inverted, and most viewers only notice when the fog-dimmed far side of the ring appears "above" them.

The 4π edge and the lap-two flip

Two details encode the topology honestly. The glowing boundary line is built by walking u from 0 to 4π while alternating the v = ±1 side each lap — because a Möbius strip has exactly one edge, and that edge closes only after two circuits. And during lap two, the finite-difference normal (which always samples the same parametric side) is negated so the ride surface stays underfoot; without the flip the camera would clip through the strip the moment u crossed 2π. These are the kinds of sign subtleties that make one-sided surfaces a rite of passage.

Speed made visible

A camera gliding over a smooth purple ribbon has no motion cues, so 36 dash markers sit along the centerline, oriented by lookAt toward the next sample. Each dash brightens as the rider approaches — opacity derived from angular distance with proper 2π wrapping — producing a runway-light effect that communicates both speed and direction, similar in role to the recycled particles of the scroll tunnel. The single edge pulses slowly on clock time so the scene idles alive, per this series' convention.

Why scrub suits this topology

The Möbius property is an experiential claim — "go around once and you are flipped" — that a fired animation asserts but a scrub *proves*: users can stop at u = 2π, look around while inverted, and back up to re-watch the roll accumulate. The gear train scrubs an angle and the pendulum wave scrubs time; this scrubs a coordinate on a surface, completing the series' tour of what a single scrubbed number can be. For the straight-line cousin of this ride, see the staircase climb.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not need to fight quaternion flips and sign errors alone — this snippet already contains the two subtle fixes (surface-normal camera.up and the lap-two negation) that make riding a one-sided surface work. Paste its HTML, CSS, and JS into an AI assistant like Claude and ask it to explain the u/2 half-twist term, why the boundary edge needs 4π to close, or what breaks with a world-up lookAt. The same assistant can extend the ride — a trefoil-knot path using the same finite-difference frame technique, strip vertex colors that reveal "sides" as the rider passes, a HUD arrow proving inversion by pointing at world-up, or three half-twists (3u/2) to compare topologies. 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-driven Möbius strip ride" 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 (~70° FOV, resized with aspect on window resize), FogExp2 matched to a dark background, ambient + directional light.
- A mobiusPoint(u, v, target) function implementing the standard parametrization with ring radius R ≈ 14 and half-width W ≈ 3.4: r = R + vW·cos(u/2); position = (r·cos u, vW·sin(u/2), r·sin u).
- The strip as an indexed BufferGeometry: a 260×10 grid over u ∈ [0, 2π], v ∈ [−1, 1], two triangles per cell, computeVertexNormals(), MeshStandardMaterial with side: THREE.DoubleSide.
- The single boundary edge as one THREE.Line built by walking u from 0 to 4π while alternating the v = ±1 side each lap — one closed loop, not two circles — with slowly pulsing opacity.
- 36 small dash markers along the centerline, each oriented by lookAt toward the next centerline sample, brightening near the rider using angular distance with correct 2π wrapping.
- One GSAP tween (ease "none") scrubbing the ride parameter u from 0 to 4π (TWO ring laps) on a ScrollTrigger with pin: true, scrub ~0.5, end ~+=600%.
- Rider frame each frame WITHOUT any world up vector: position = centerline point + 1.5 × local normal, where the normal = normalize(cross(finite-difference tangent along u, finite-difference tangent along v)); set camera.up to that normal; lookAt a point ~0.14 rad ahead lifted by the same normal. NEGATE the normal during the second lap (u > 2π) so the surface stays underfoot across the parametric seam.
- A LAP % HUD showing u / 4π, and an intro overlay fading once u passes ~0.1.
- Confirm: at 50% scroll the camera is fully inverted relative to its start; at 100% it has returned to the starting orientation; reverse scrolling unwinds the roll smoothly.

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 sits on the purple strip's centerline, runway dashes ahead, the single glowing edge pulsing at both sides.
  3. 3
    Scroll to rideThe camera glides along the ribbon while rolling with the surface — the horizon tilts continuously as the half-twist accumulates.
  4. 4
    Pause at 50%At one full ring lap you are riding the underside, fully inverted — look up at where you started.
  5. 5
    Complete lap twoOnly at 100% (u = 4π) does the camera return to its starting orientation — the Möbius property, proven by scroll.
  6. 6
    Reshape the ribbonChange R and W for ring radius and strip width, or replace u/2 with 3u/2 in mobiusPoint for a three-half-twist strip — the frame math adapts unchanged.

Real-world uses

Common Use Cases

Math education and topology pages
The strongest possible demo of one-sidedness: students ride the flip, pause inverted, and reverse it — pair with a pendulum wave for a physics-and-math duo.
Infinity-themed brand loops
Logistics, recycling, and subscription brands get a literal endless-loop hero where the journey visibly never hits an edge.
Award-site signature interactions
A camera that rolls upside down mid-scroll is a memorable centerpiece, more disorienting than a scroll tunnel flythrough.
Album, film, and title sequences
Use the inversion moment as a narrative beat — drop a title card exactly at u = 2π when the world is upside down.
Puzzle and perspective game promos
Monument-Valley-adjacent games can let visitors experience impossible geometry before seeing gameplay.
Generative and kinetic art portfolios
Present the strip as sculpture with the ride as the exhibition path, alongside pieces like the wave ribbon.

Got questions?

Frequently Asked Questions

That is the defining property of a Möbius strip. The u/2 term rotates the strip's cross-section half a turn per ring lap, so after u = 2π the rider is on the same surface but locally inverted — the "other side" that does not exist as a separate side. Only after 4π of travel has the accumulated roll returned to identity. The scrub lets users verify this by pausing at 50% and looking around while upside down.

lookAt orthonormalizes against camera.up; with a fixed world up, the computed right vector degenerates as the surface rolls past 90° and the camera snaps or spins wildly. Setting camera.up to the local surface normal each frame — computed from cross(tangent-along-u, tangent-along-v) — lets the camera roll smoothly and continuously through full inversion, which is the entire point of the ride.

The finite-difference normal always samples the same parametric v-direction, but after u = 2π the rider is physically on the opposite face of the ribbon, where that sampled normal points into the surface. Negating it during lap two keeps the camera hovering 1.5 units above the face underfoot instead of clipping through the strip at the seam. It is a one-line fix for a sign error that is invisible until it teleports the camera.

A Möbius strip has exactly one edge: follow the v = +1 border around the ring and you arrive at v = −1 of your starting point, needing a second lap to close the loop. The edge line walks u through 4π while alternating which v-extreme it samples per lap, producing the single closed loop. Drawing two separate v = ±1 circles would render the topology of a cylinder — visibly wrong once you know what to look for.

Yes. Export via the JSX, Vue, Angular, or Tailwind buttons. Build the parametric geometry, dashes, and ScrollTrigger in a mount effect against a canvas ref, reusing the module-scope scratch vectors as effect-scope consts. On cleanup kill the ScrollTrigger, dispose the strip and edge geometries, dash geometries/materials, and call renderer.dispose(). The ride value lives in a plain object — never React state.