
The time wheel
Pickers, 3D, Physics
NewFree
A fabric banner being uploaded to an online printer: the banner hangs from rings on a rail, gathered at the left in pleats like a curtain pulled aside, and as the file goes up the rings slide along the rail and the fabric opens pleat by pleat until the print hangs flat. It is a real cloth in three dimensions, points and sticks with gravity, and every triangle is drawn with the print on it and lit by its normal, so the pleats have their shade and the back shows inside them. If the connection drops the last ring lets go and the corner falls until Retry hooks it again; at the end a breath of wind crosses the banner, and Upload another gathers it again.
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, viewport-fit=cover">
<title>The banner on the rail</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the banner on the rail: a print shop uploading a fabric banner, and the banner hangs from rings on a rail,
gathered at the left in pleats. As the file goes up the rings slide out along the rail and the fabric opens,
pleat by pleat, until the whole print hangs flat. The cloth is a real cloth in 3D (points, sticks, gravity),
printed and lit triangle by triangle on the canvas by script.js -->
<section class="bn" aria-labelledby="bn-title" data-banner>
<div class="bn__head">
<div class="bn__words">
<h2 class="bn__title" id="bn-title"><span class="bn__swap" data-title>Fabric banner</span></h2>
<p class="bn__meta"><span class="bn__swap" data-meta>night-market.pdf · 48 MB</span></p>
</div>
<p class="bn__figure" aria-hidden="true">
<span class="bn__pct" data-pct>0%</span>
<svg class="bn__tick" viewBox="0 0 28 28" data-tick><path d="M6.5 14.8l5 5 10-11.4" pathLength="1"/></svg>
</p>
</div>
<div class="bn__rail" role="progressbar" aria-labelledby="bn-title" aria-valuemin="0" aria-valuemax="100"
aria-valuenow="0" aria-valuetext="Not started" data-bar>
<canvas class="bn__canvas" aria-hidden="true" data-cloth></canvas>
</div>
<div class="bn__foot">
<span class="bn__status"><span class="bn__swap" data-status>200 × 110 cm · polyester</span></span>
<button class="pill" type="button" data-btn data-autoclic>
<span class="pill__fill" aria-hidden="true"></span>
<span class="pill__label" data-label>Upload</span>
</button>
</div>
<p class="sr" role="status" aria-live="polite" data-live></p>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.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.
35 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 banner on the rail” from Animorio and fit it into my siteThe banner is a grid of 17 by 11 points in three dimensions, joined by sticks along the rows and columns, on the diagonals (softer) and, downwards, two points apart, so it hangs like fabric and not like a chain. Every step (1/120 of a second, at most six a frame) moves the points with Verlet, with gravity and a little damping, then seven times puts every stick back to its length. There is no stick two points apart across: it would push the pleats open and the banner would fan out at its hem.
A ring on every other column holds the top point under it; the rings head for their places on a spring, and the place depends on the progress: GATHER apart when the banner is gathered, an eighth of its width when it is open. Squeezed between two rings, the column in between has to go forward or back: the starting pose is worked out by hand, every other column forward or back by just enough to keep the sticks their length, and a light pull towards that pose (the memory of the pleats, stronger while it gathers) makes it fold like an accordion and not into a heap.
The print is a separate canvas, drawn at twice its size. Every square of the grid is two triangles, projected in perspective and sorted from the farthest to the nearest; for every triangle the script works out the affine transform that takes its three points on the print to its three points on the screen, clips the triangle, grown by almost a pixel away from its middle so no line is left between neighbours, and draws the whole print through that transform.
The normal of every triangle, turned to face the camera, gives its light, which comes from the upper left and in front. The shade is measured against the flat banner: a flat triangle has no shade, so no seam shows on the open banner, and only the faces turned away darken, those turned to the light brighten a little. The winding of its points on the screen says whether the front or the back is seen, and the back, inside a pleat, is plainer and darker.
When the connection drops the last ring lets go of its point, which falls freely; Retry hooks it again by pulling it a little towards the ring at every step until it is held again. At the end the wind is a bell-shaped push backwards that runs along the banner from left to right in a little over a second, stronger towards the hem. When the rings are still and the points no longer move, the loop leaves GSAP’s ticker and draws nothing.
| Name | File | Default | What it changes |
|---|---|---|---|
SIZE_MB | script.js | 48 | How big the file is, in megabytes. |
SECONDS | script.js | 6 | How many seconds the upload takes in the demo. |
RINGS | script.js | 9 | How many rings hold the banner. |
GATHER | script.js | 6 | How close the rings sit with the banner gathered, in pixels. |
STIFF | script.js | 0.5 | How stiff the fabric hangs, from 0 (silk) to 1 (canvas). |
GRAVITY | script.js | 1400 | How hard gravity pulls, in pixels per second squared. |
WIND | script.js | 1 | How strong the breath of wind at the end is (0 none). |
WORDS | script.js | ['NIGHT', 'MARKET', 'Fridays · Pier 9 · 7 pm'] | The banner’s print: two big lines and a small one. |
FAIL_EVERY | script.js | 2 | One upload in how many loses the connection (0 never). |
FAIL | script.js | 0.62 | Where the connection is lost, from 0 to 1. |
PREVIEW | script.js | 1.4 | In the preview’s round: how many times faster. |
LABELS | script.js | ['Upload', 'Pause', 'Resume', 'Retry', 'Upload another'] | The words of the pill, state by state. |
--fabric | style.css | #efe8da | The colour of the fabric. |
--print-sun | style.css | #ff5a36 | The sun on the print. |
For uploading a file to print at an online printer (banners, posters, curtains, T-shirts), for sending a design or an artwork in a creative app, for any upload where what goes up is something to hang and look at.
The panel takes the whole width less the margins and the scene shrinks with it, because it is drawn in units of its box: the cloth and its movements are the same. The pill answers a touch as it does the mouse.
No physics: the banner simply takes the pose worked out for the progress, gathered, half open or flat, without swinging, with no springs in the rings and no wind; the corner that lets go folds down instead of falling. The words change without moving.
GSAP 3.15.0, Instrument Sans (font variabile, assi wght e wdth) 5.3.0.
Compressed with gzip, the three demo files weigh 11 KB; the libraries: GSAP 28 KB.
Tested on 19 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.