Skip to content
animorio
IT
All components

The sandpile pass

Loading, PhysicsGSAP, CanvasAdvancedCompressed weight: Demo 9.9 KBGSAP 28 KBFreeDownload the zip

A festival pass being generated, and the ticket’s seal is not drawn, it is grown: an abelian sandpile. Grains fall one after the other on the middle of a grid, and a cell holding four topples and gives one to each neighbour, which may topple in turn, in avalanches that run out to the edge. Nobody draws the pattern: it is what thousands of avalanches leave, a square lace of triangles and stripes, and the cells reached for the first time flash. The grains dropped are the progress. If the connection drops the seal stops and goes cold until Retry warms it from the middle; at the end a light crosses it and its code is set, then Add to Wallet, and Start over drains it back into its middle.

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, viewport-fit=cover">
  <title>The sandpile pass</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the sandpile pass: a festival pass whose seal is grown while it is generated. Grains are dropped one by one on
         the middle of a grid; a cell holding four topples and gives one to each neighbour, which may topple in turn.
         The avalanches build a pattern that no one drew, and the number of grains dropped is the progress. The seal is
         drawn by script.js -->
    <section class="sp" aria-labelledby="sp-title" data-pass>
      <div class="sp__head">
        <div class="sp__words">
          <h2 class="sp__title" id="sp-title"><span class="sp__swap" data-title>Your festival pass</span></h2>
          <p class="sp__meta"><span class="sp__swap" data-meta>Lumen Festival · 3-day pass</span></p>
        </div>
        <p class="sp__figure" aria-hidden="true">
          <span class="sp__pct" data-pct>0%</span>
          <svg class="sp__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="pass" data-ticket>
        <div class="pass__seal" role="progressbar" aria-label="Pass seal" aria-valuemin="0" aria-valuemax="100"
             aria-valuenow="0" aria-valuetext="Not started" data-bar>
          <canvas class="pass__canvas" aria-hidden="true" data-seal></canvas>
          <span class="pass__empty" data-empty>Seal</span>
        </div>
        <dl class="pass__info">
          <div><dt>Holder</dt><dd>Mara Quint</dd></div>
          <div><dt>Dates</dt><dd>12–14 June</dd></div>
          <div><dt>Entry</dt><dd>Gate B · General</dd></div>
          <div><dt>Seal</dt><dd class="pass__code" data-code>Not yet grown</dd></div>
        </dl>
      </div>

      <div class="sp__foot">
        <span class="sp__status"><span class="sp__swap" data-status>Every seal is grown once</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>Generate pass</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>

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.

42 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 sandpile pass” from Animorio and fit it into my site

What it does

On a black background, alone in the middle: at the top Your festival pass and under it Lumen Festival · 3-day pass, with 0% on the right; below, a ticket in the darkest grey with round corners, split by a perforation of tiny holes with a round notch at the top and the bottom. On the left the square of the seal, empty, with the word SEAL; on the right the pass details in a column: Holder Mara Quint, Dates 12–14 June, Entry Gate B · General, Seal Not yet grown. At the foot Every seal is grown once and the light pill Generate pass. Coming in, the ticket rises by a few pixels and the details arrive one after the other. Press Generate pass and a pixel sandpile starts to grow in the middle of the seal: every grain falling on the middle makes the cells holding four topple, and the avalanches widen the pile into an octagon full of triangles, stripes and lace, in three tones: coral for a cell holding one grain, cream for two, dark grey for three. The cells reached for the first time flash a warm white and fade into their colour, so the growing edge glimmers. Below, the count of grains goes up, 9,381 of 32,768 grains, and the percentage with it; the pill becomes a ring that says Pause, which stops the fall, and Resume. One time in two the connection drops: the seal stops and turns grey, the title says Connection lost in red and under it 10,276 grains kept; Retry warms it with a wave of colour from the middle to the edge and the grains start falling again. At the end a band of light crosses the seal on the diagonal, the percentage gives way to a green tick, the title says Your pass is ready and the seal’s code, LMN 7F3K 2QX9, is set in coral letter after letter. Add to Wallet gives the ticket a small nod on a spring and the title says In your Wallet; Start over drains the pile from the edge back to the middle until the seal is empty again.

How it works

The abelian sandpile

It is Bak, Tang and Wiesenfeld’s model: a grid of cells holding grains; a cell holding four or more topples, loses four and gives one to each of its four neighbours, which may topple in turn. The grains always fall on the middle cell. A settled pile holds a little over two grains a cell on average, so its radius is about the square root of N over 2.05π: the grid is made that size plus three cells of margin, so the avalanches never reach its edge.

A frame’s grains all at once

The pile is abelian: the order of the topplings does not change the result, so a frame’s grains are all put on the middle at once and toppled together. A cell holding v grains gives v over four to each neighbour in one go and keeps v modulo four; the cells to topple sit in a stack of integers. That is how 32,768 grains and tens of millions of topplings fit inside the frames, on a Uint32Array.

The timing of the grains

The grains dropped are N times the progress to the power EASE_IN: at 2, since the area of the pile grows with its grains, the radius of the seal grows evenly and reads as a progress bar. The progress is a clock in GSAP’s ticker with a step of at most 1/30 of a second, which pausing and a lost connection hold.

One pixel per cell

Every frame the grid becomes an ImageData of one pixel per cell, drawn on a canvas a whole number of device pixels per cell, with no smoothing, so every cell is crisp. The same pass mixes in the warm white of the cells just reached (fading in SPARK seconds), the grey of the cold seal outside the radius being warmed, the final band of light on the diagonal and the edge beyond which the pile has already drained away.

The preview’s round

A click from a script on the pill (detail 0) plays the whole round, faster by PREVIEW: the lost connection, Retry, the pass ready, Add to Wallet and Start over, with a pretend press on the pill at every step; the waits between the steps are GSAP delayedCalls. A real click stops the round and takes over. When nothing grows and nothing glimmers the loop leaves the ticker.

Values to change

NameFileDefaultWhat it changes
GRAINSscript.js32768How many grains grow the seal: more grains, a bigger and finer pattern.
SECONDSscript.js6.5How many seconds the pass takes to generate in the demo.
EASE_INscript.js2How the grains arrive: at 2 the seal widens evenly, below it fills fast at first.
SPARKscript.js0.45How many seconds a cell reached for the first time flashes.
CODEscript.js'LMN 7F3K 2QX9'The seal’s code, set when it has grown.
FAIL_EVERYscript.js2One pass in how many loses the connection on the way (0 never).
FAILscript.js0.56Where the connection is lost, from 0 to 1.
PREVIEWscript.js1.5In the preview’s round: how many times faster.
LABELSscript.js['Generate pass', 'Pause', 'Resume', 'Retry', 'Add to Wallet', 'Start over']The words of the pill, state by state.
--sand-1style.css#ff5b3aThe cells holding one grain: the accent, used for the code too.
--sand-2style.css#f4f1eaThe cells holding two grains.
--sand-3style.css#2e2d31The cells holding three grains.

Try them in the Playground

When to use it

For generating a ticket, a pass or a season ticket in an events, concerts or festival app, for a one-of-a-kind collectible, for an avatar or a pattern generated on the spot, for any wait that ends with something that is only yours.

On phones

The ticket takes the whole width less the margins, the seal stays 172 pixels wide and the details narrow beside it; the pile’s grid does not change, so the pattern is the same. The pill answers a touch as it does the mouse.

With reduced motion

The pile grows all the same, because it is the progress, but the cells reached do not flash, the final light does not pass, the colour comes back without a wave, the words change without moving and the ticket does not nod.

Pitfalls

  • With coral on the cells holding three grains the seal was almost all coral and noisy, because the areas full of threes are the largest: now the threes are the darkest tone and coral and cream draw the lace.
  • With 16,384 grains and a cell of a pixel and a half the pattern hardly showed and the pixels were not all alike: now there are 32,768 grains and every cell is a whole number of device pixels, with no smoothing.
  • With the grains arriving as the progress to the power 1.6 the seal stayed small for a long time and then widened all at once: the area grows with the grains, so the exponent is now 2 and the radius grows evenly.
  • At the end of the round one frame had to do over four hundred thousand topplings, nearly twenty milliseconds on a computer and too many on a phone: now there are at most 150,000 topplings a frame and a big avalanche runs on over the next frames, the cells still unstable shown in the spark’s colour; the stack only takes a cell when it becomes unstable, so it never holds one twice and never overflows, and the percentage waits at 99 until the last avalanche stops.
  • Nearly every cell topples in every frame, so flashing the cells that topple lit up the whole pile: only the cells reached for the first time flash, which is the growing edge.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile, assi wght e wdth) 5.3.0.

Compressed with gzip, the three demo files weigh 9.9 KB; the libraries: GSAP 28 KB.

Testing

Tested on 19 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 components