Skip to content
animorio
IT
All components

The incense timer

Timers, Physics, Hover, TransitionsCanvas, GSAPAdvancedCompressed weight: Demo 16 KBGSAP 28 KBFreeDownload the zip

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.

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

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.

23 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 incense timer” from Animorio and fit it into my site

What it does

On a black background a pale green celadon bowl, its glaze crackled, a window reflection on the left and a bare clay foot, full of sand coloured ash; in the middle a brown stick of incense stands on its red reed. Under the bowl the time, 0:15, in large thin figures, the word Ready in italics and two buttons: a round one to start over, dimmed, and a pill in the same celadon as the bowl with Begin. When the page opens the bowl appears and the stick rises out of the ash with a small bounce. With Begin a small flame catches on the tip and flickers, the ember takes and the flame goes out in a puff of smoke; from then on the smoke rises all the time, first straight and thin, then it waves, splits into three threads that curl, spreads into a veil and fades away at the top. It is born a little blue and turns grey as it ages. The stick gets shorter second by second, the orange ember goes down with the tip and above it a small column of grey ash grows, which now and then bends over, breaks off, falls into the bowl and crumbles: the bits stay on the ash, and the bowl gathers the time gone by. The figures change by rising like the smoke, blurring, and the new one comes up from below. Moving the mouse through the smoke, or drawing a finger through it, pushes the smoke away and curls it into two whirls either side of the path, and the fanned ember brightens. Pause lowers the ember to a dull red and the smoke stops, the last thread rises away; Resume lights it again with a puff. At zero the ember flares one last time and goes out, the last ash falls, the last thread rises, and under the figures it says Complete; shortly after the stub sinks into the ash, the bits in the bowl fade and a new stick rises in its place, with the time full again and Ready. The round button starts over at any moment.

How it works

The smoke, puff by puff

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

Threads that thin out

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 hand in the 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 that falls

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 and the states

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.

Values to change

NameFileDefaultWhat it changes
SECONDSscript.js15How long a sitting lasts: the stick burns down in this many seconds (short for trying it out; minutes in a real app).
RISEscript.js46How fast the smoke rises, in pixels a second.
SWIRLscript.js1How much the smoke waves and curls away from the tip: 0 = a straight thread.
SMOKEscript.js1How thick the smoke is.
HANDscript.js1How much a hand (or a finger) passed through the smoke stirs it; 0 = the smoke ignores the hand.
ASHscript.js[12, 22]How long the ash grows on the tip before it drops, in pixels: the shortest and the longest.
AFTERscript.js2.6The seconds the spent stick stays before a new one rises.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • The smoke looked like a string of beads. The puffs were born on whole frames, so at uneven distances; the opacity changed group at every joint and the round ends of different strokes added up. Now each puff is born at its exact moment, the distance is smoothed over its neighbours and the strokes have flat ends.
  • The ash under the smoke, added up additively, became a white block brighter than the ember: it is drawn after the smoke, in grey.
  • Old smoke, drawn with the sharp thread, looked like tangled wire: the sharp thread goes before the soft body, which widens.
  • Three threads leaving a pixel apart rose side by side like optical fibres: they leave from almost the same point and only part as they rise, because they read the noise a little apart.
  • The hand pushed the whole column sideways for seconds: the push is now weak, and the two whirls do most of the work.
  • With reduced motion the still smoke, worked out from the first tip, was half as tall as needed and went down with the tip, leaving a gap at the top. Now it is worked out from the lowest tip and faded at the top as it is drawn. It must be worked out at a noise time close to the real one: later on the noise bunched it up low.
  • Counting the time by frames, a tab in the background lost time (no frames, and GSAP shortens the jumps): the time left comes from the clock.
  • The library’s preview clicks by itself every 2.8 seconds and would have paused a sitting just begun: a click from neither a hand nor the keyboard does not pause. Enter on the button clicks with detail 0 like the script: it is told apart by its keydown.

Libraries

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.

Testing

Tested on 6 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