
The swatch fan menu
Menus, Physics, Transitions, Hover
NewFree
The timer of a meditation app, where a sitting lasts as long as a stick of incense: the stick burns down in its celadon bowl as the time runs, the smoke rises in threads that wave and curl, the ash grows on the tip and drops into the bowl, and a hand passed through the smoke stirs it. At zero the ember goes out and a new stick rises out of the ash.
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 incense timer</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/newsreader@5.3.0/opsz.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the incense timer: a sitting lasts as long as a stick of incense. The stick burns down in its bowl while the
smoke rises in threads that curl, the ash grows on the tip and drops into the bowl, and a hand passing
through the smoke stirs it -->
<section class="sit" aria-label="Meditation timer" data-timer>
<div class="sit__scene" data-scene>
<canvas class="sit__canvas" aria-hidden="true" data-canvas></canvas>
</div>
<div class="sit__readout">
<span class="sit__time" role="timer" aria-atomic="true" data-time-box>
<span class="sit__digits" aria-hidden="true" data-time></span>
<span class="sr" data-time-words></span>
</span>
<span class="sit__state" aria-hidden="true" data-state></span>
</div>
<div class="sit__controls" data-controls>
<button class="sit__reset" type="button" aria-label="Start over" aria-disabled="true" data-reset>
<span class="sit__turn">
<svg viewBox="0 0 24 24" aria-hidden="true" data-reset-icon><path d="M5.6 9.2A7.2 7.2 0 1 1 4.9 14.2"/><path d="M5.2 4.6v4.8H10"/></svg>
</span>
</button>
<button class="sit__go" type="button" data-go data-autoclic>
<span class="sit__words" data-go-words></span>
</button>
<span class="sit__balance" aria-hidden="true"></span>
</div>
<p class="sr" 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.
23 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 incense timer” from Animorio and fit it into my siteThree threads of puffs come off the ember, RATE a second each, from almost the same point. Each puff rises at RISE pixels a second and is carried by the curl of a two-octave 3D Perlin noise: a field that turns but never gathers or spreads, like air. The noise moves up with the smoke, so the curls rise with it. Just off the tip the field counts for little and the smoke rises straight; higher up it counts for more (SWIRL) and the thread waves and curls. The three threads read the noise a little apart in time, so they leave together and part as they rise. Each puff is born where it would have been at its exact moment within the frame, so they are all evenly spaced.
The puffs of a thread are joined in the order they were born. A stretched stretch is a thinner thread, so it fades; the distance is smoothed over its neighbours, so the fade does not flicker from one joint to the next. Each piece goes into a group by age and opacity (one Path2D per group) and is drawn additively, with flat ends. The soft body of the smoke is the same drawing, wide, on a canvas a quarter of the size, stretched over the picture: the blur comes for free. As it ages the sharp thread goes before the soft body, and the colour turns from blue to grey, as in real smoke.
The pointer (mouse or finger) leaves gusts: a point, the speed of the hand, a radius that widens and a strength that dies away in a third of a second. Nearby puffs are pushed along with the hand and turned into a whirl. Which way the whirl turns depends on which side of the path the puff is, so two of them are born, as in a real wake. A gust near the tip brightens the ember and the smoke for a moment (HAND).
The ash grows on the tip as long as the part burnt, with a ragged edge and a slight lean. At a length picked at random within ASH it tips over on the ember like a rod on its foot (acceleration 3g/2L times the sine of the angle) and, past 35 degrees, breaks off with the speed it had and falls turning. Where it meets the ash in the bowl it crumbles into bits that bounce once; at rest, they are drawn on a canvas of their own laid over the bowl, so they stay at no cost.
The time left comes from the clock (performance.now), not from the frames, so a tab left in the background still ends on time. The share burnt is the time gone. Begin is a GSAP timeline: the small flame, the ember, the flame going out and the puff. Pause takes the ember to a dull red and the smoke to nothing, the end makes the ember flare and go out, and a timeline sinks the stub (the part under the ash is cut away) and brings the new stick up with a back.out. The words and the figures change by rising and blurring. The preview’s click (detail 0) only begins and resumes, never pauses.
| Name | File | Default | What it changes |
|---|---|---|---|
SECONDS | script.js | 15 | How long a sitting lasts: the stick burns down in this many seconds (short for trying it out; minutes in a real app). |
RISE | script.js | 46 | How fast the smoke rises, in pixels a second. |
SWIRL | script.js | 1 | How much the smoke waves and curls away from the tip: 0 = a straight thread. |
SMOKE | script.js | 1 | How thick the smoke is. |
HAND | script.js | 1 | How much a hand (or a finger) passed through the smoke stirs it; 0 = the smoke ignores the hand. |
ASH | script.js | [12, 22] | How long the ash grows on the tip before it drops, in pixels: the shortest and the longest. |
AFTER | script.js | 2.6 | The seconds the spent stick stays before a new one rises. |
For the timer of a meditation, breathing or yoga app, a focus break, studying in blocks: wherever the time should pass calmly and without stress, and watching it is part of unwinding. It does not suit a countdown that is meant to hurry you.
The component is 320 pixels wide and fits a 390 screen without spilling over. A finger drawn through the smoke does what the mouse does; on the drawing the page does not scroll (touch-action none), so the finger stirs the smoke instead of moving the page. The smoke is cheap (about six hundred puffs and a hundred or so strokes a frame) and runs at 60 frames a second even without a graphics card.
The smoke does not move: it is worked out once, still, stands on the tip wherever the tip is and fades in and out. No flame, no flicker in the ember, no hand in the smoke; the ash does not fall, it fades and its bits are already in the bowl. The new stick appears in place of the old one without rising, and the figures and words change by fading. The stick still gets shorter, because it is the time.
GSAP 3.15.0, Newsreader (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 16 KB; the libraries: GSAP 28 KB.
Tested on 6 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.