
The breakfast machine
Scroll, SVG, Physics
NewFree
Scroll inside the preview
As you scroll, a square wheel rolls along a road of humps: they are catenary arches, the real maths of the square wheel, and its hub never leaves the dashed line. On the way it gains sides one at a time, from four to twelve, then swells into a circle and the road flattens out beneath it.
A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The wheel that rounds off</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/standard.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full-italic.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/red-hat-mono@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- a made-up bike workshop, Round Trip Cycle Works. The stage sticks while you scroll: a square wheel rolls along a
road of catenary arches (the real maths of the square wheel, drawn on a canvas), takes on sides from four to
twelve, swells into a circle and the road flattens out under it. The four steps of a service light up as the
wheel passes over them. Everything is drawn by the script -->
<main>
<section class="corsa" data-corsa>
<div class="palco" data-palco>
<canvas class="strada" data-strada aria-hidden="true"></canvas>
<header class="testa">
<a class="marchio" href="#" data-marchio>
<svg class="marchio__segno" viewBox="0 0 40 40" aria-hidden="true">
<rect class="marchio__ruota" data-marchio-ruota x="7" y="7" width="26" height="26" rx="7"/>
<path class="marchio__raggi" d="M20 9.5v21M9.5 20h21"/>
<circle class="marchio__mozzo" cx="20" cy="20" r="3.6"/>
</svg>
<span class="marchio__nome" data-entra-testa>Round Trip<span>Cycle Works</span></span>
</a>
<p class="testa__orari" data-entra-testa>Open Tuesday to Saturday</p>
<a class="pillola" href="#" data-pillola data-entra-testa><span class="pillola__fondo" data-pillola-fondo></span><span class="pillola__testo">Book a service</span></a>
</header>
<div class="apertura">
<p class="apertura__etichetta" data-entra>How a service goes</p>
<h1 class="apertura__titolo">
<span class="riga"><span class="riga__dentro" data-riga>From square one</span></span>
<span class="riga"><span class="riga__dentro morbido" data-riga data-morbido>to a smooth ride.</span></span>
</h1>
<p class="apertura__testo" data-entra>Bring us the bike that rattles. We take the corners off, one at a time, until it rolls like the day you bought it.</p>
</div>
<div class="bandiera" data-bandiera aria-hidden="true">
<div class="bandiera__telo" data-telo>
<span class="bandiera__nome" data-bandiera-nome>Square</span>
<span class="bandiera__lati" data-bandiera-lati>4 sides</span>
</div>
</div>
<p class="cenno" data-cenno>
<span>Scroll to roll</span>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 3v13M4.5 10.5 10 16l5.5-5.5"/></svg>
</p>
<ul class="passi" data-passi>
<li class="passo" data-passo data-forma="primo">
<svg class="passo__forma" viewBox="-12 -12 24 24" aria-hidden="true"><path data-passo-forma/></svg>
<h2 class="passo__nome">Drop off</h2>
<p class="passo__riga">Wheel in, coffee on. We write down every rattle and squeak.</p>
</li>
<li class="passo" data-passo data-forma="medio">
<svg class="passo__forma" viewBox="-12 -12 24 24" aria-hidden="true"><path data-passo-forma/></svg>
<h2 class="passo__nome">Check-up</h2>
<p class="passo__riga">Spokes, bearings, brakes and chain. We find every corner.</p>
</li>
<li class="passo" data-passo data-forma="ultimo">
<svg class="passo__forma" viewBox="-12 -12 24 24" aria-hidden="true"><path data-passo-forma/></svg>
<h2 class="passo__nome">Tune</h2>
<p class="passo__riga">Trued, tensioned and greased, one side at a time.</p>
</li>
<li class="passo" data-passo data-forma="cerchio">
<svg class="passo__forma" viewBox="-12 -12 24 24" aria-hidden="true"><path data-passo-forma/></svg>
<h2 class="passo__nome">Ride away</h2>
<p class="passo__riga">Round, quiet and quick. Same day, most days.</p>
</li>
</ul>
<p class="passi__ora" aria-hidden="true"><span data-passo-ora>Wheel in, coffee on. We write down every rattle and squeak.</span></p>
</div>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.min.js"></script>
<script src="script.js"></script>
</body>
</html>Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.
37 values to change
With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:
Take “The wheel that rounds off” from Animorio and fit it into my siteSeen from the hub, the wheel’s outline is a distance r for every angle. If the hub stays at the same height and the wheel does not slip, the point touching the road is always right under the hub: there the road is r below it, and rolling on by dx turns the wheel by dx over r. So the road is x equals the sum of r times the angle, y equals r. For a straight side at distance d from the centre, r is d over the cosine of the angle from its perpendicular, and the sum comes out as d times asinh(tan(angle)): an upside-down catenary arch, d·cosh(x/d), as long as the side. That is the square wheel’s road.
Between n and n+1 sides the wheel never jumps: the polygon gets a new corner in the middle of its last side, then all the corners slide to where the polygon with one more side has them, turned by just enough to move them as little as possible. The sides go from SIDES[0] to SIDES[1] in the first half of the ride (with a smoothstep), then the sides swell: every point of the outline moves out towards the full radius, so r becomes lerp(r, 1, round), and the road is worked out from the same r, so it flattens by itself.
Since the shape changes while it rolls, the wheel’s angle cannot be read from the shape: it is added up once, at the start, in 2,400 small steps (dx over r, with a half step in between to stay accurate) and kept in a table. Every frame reads the table at the point of the ride, so scrolling back the wheel goes through the same positions.
ScrollTrigger gives the share of the ride; the drawing chases it, covering CHASE of the distance every frame (worked out on real time, so the same at 60 and 120 Hz), and once the gap is under 0.0005 it stops there. The canvas is only redrawn when something has changed: the ride, the swaying flag or dust still in the air.
The flag’s pole is a spring: it leans back with the wheel’s speed and gets a knock every time a corner drops into a dip, harder the more angular the wheel is; at the same moment a puff of dust flies up (DUST). The shape’s name changes on the rounded number of sides, with a bounce. Each step lights up when the hub passes the start of its column, and the SOFT axis of the italic Fraunces follows how round the wheel is.
| Name | File | Default | What it changes |
|---|---|---|---|
SIDES | script.js | [4, 12] | The wheel’s sides at the start (at least 4: a triangle would hit the next hump) and how many it gets before turning into a circle; 4 and 12 on the site. |
RADIUS | script.js | 0.13 | The wheel’s radius, as a share of the screen’s short side (times 1.4 on a phone); 0.13 on the site. |
CHASE | script.js | 0.2 | How much of the way to the scroll the drawing covers every frame: 1 follows at once, 0.05 arrives very softly; 0.2 on the site. |
TYRE | script.js | 0.13 | How thick the tyre is, as a share of the radius. |
DUST | script.js | 1 | The puff of dust when a corner drops into a dip (0 = none). |
FLAG | script.js | 1 | How much the flag sways and bounces on its pole (0 = stiff). |
RIDE | script.js | 1 | How much scroll the whole ride takes: 2 is twice as much. |
To tell a method in a few steps (a service, a process, a path from A to B) where the idea is “from rough to smooth”: workshops, studios, consultancies, apps that promise to make things simpler. The wheel shows the improvement without words.
The same: the wheel is a little bigger in proportion, the four steps sit in a row with just their names, and the line of the step reached shows underneath, changing with a bounce.
The wheel does not roll: it waits at each step, with the shape it has there, and the drawing crosses from one stop to the next with a fade. No dust, a straight flag, the entrance only fades in.
GSAP 3.15.0, Lenis 1.3.26, Bricolage Grotesque (font variabile) 5.3.0, Fraunces (font variabile) 5.3.0, Red Hat Mono (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 14 KB; the libraries: GSAP + ScrollTrigger 45 KB, Lenis 5.3 KB.
Tested on 13 October 2026
Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.