Skip to content
animorio
IT
Back to the library

The wheel that rounds off

Scroll, PhysicsCanvas, GSAPAdvancedCompressed weight: Demo 14 KBGSAP + ScrollTrigger 45 KBLenis 5.3 KBFreeDownload the zip

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.

The code

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>

Playground

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

Or ask Claude Code for it

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 site

What it does

The “how a service goes” page of a made-up bike workshop, Round Trip Cycle Works. The stage stays still while you scroll: at the top the title “From square one to a smooth ride”, at the bottom, on the ground, the four steps of a service (Drop off, Check-up, Tune, Ride away). On opening the road draws itself from the left, the wheel drops from above and bounces, the little flag on its pole sways. As you scroll the square wheel (black tyre, spokes, orange hub and valve) rolls to the right over the humps of the road: the hub runs straight along the dashed line, while the corners drop into the dips and kick up a little dust. Soon it gains a side, then another: pentagon, hexagon, up to twelve sides, and meanwhile the humps get lower and closer, always right for the shape of the moment. Then the sides swell, the wheel becomes a circle and the road flattens into a straight line. The flag says what shape the wheel is (Square, Pentagon… Circle), changes its label with a bounce, leans back when the wheel speeds up and shivers at every dip. Each step lights up when the wheel reaches it, its shape icon spinning in on an elastic; in the title the italic half grows softer as the wheel loses its corners, and at the last step the “Book a service” button gives a little hop.

How it works

The road for any wheel

Seen 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.

The sides that grow

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.

How far it has turned

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.

Chasing the scroll

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.

Flag, dust and steps

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.

Values to change

NameFileDefaultWhat it changes
SIDESscript.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.
RADIUSscript.js0.13The wheel’s radius, as a share of the screen’s short side (times 1.4 on a phone); 0.13 on the site.
CHASEscript.js0.2How 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.
TYREscript.js0.13How thick the tyre is, as a share of the radius.
DUSTscript.js1The puff of dust when a corner drops into a dip (0 = none).
FLAGscript.js1How much the flag sways and bounces on its pole (0 = stiff).
RIDEscript.js1How much scroll the whole ride takes: 2 is twice as much.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • The turning is not proportional to the distance: over a hump the wheel turns faster near the corners (dx over r, and r is bigger there). With a turn in proportion to the road covered, and humps as wide as the side instead of 2a·asinh(tan(π/n)), the square floats over the dips or sinks into them.
  • With a fractional number of sides the shape is not regular, so equal catenaries no longer fit it: here the road is worked out from the shape of the moment, starting at the contact point, and the angle is added up once in a table. So the wheel always touches without sinking into the road, even halfway between two shapes.
  • The sharp dips get lost if the road is drawn point by point at a fixed step: it has to be drawn side by side, with the exact corners, and stroked with a miter lineJoin.
  • A requestAnimationFrame loop that is always on drains battery even on a still page: here the ticker is always there, but the canvas is only redrawn when something has changed.

Libraries

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.

Testing

Tested on 13 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

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.

All animations