
Standing letters with a long shadow
Text, Scroll, 3D
NewFree
Scroll inside the preview
A comic in four engraved panels. When a caption comes on screen its box unrolls from the left edge and its lines come in one after another; then a narrator reads it, one red word at a time, at the pace of a real voice, and waits if you scroll in a hurry.
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>Captions that read aloud</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/im-fell-english@5.3.0/index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/im-fell-english@5.3.0/400-italic.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="bar">
<a class="brand" href="#">Tallow & Ink</a>
<nav class="bar__nav" aria-label="Sections">
<a href="#">Books</a>
<a href="#">Pre-order</a>
</nav>
</header>
<main>
<section class="intro">
<p class="kicker">A graphic novel in six chapters</p>
<h1 class="title" data-title>The Night Ferry</h1>
<p class="lede">Scroll down slowly. The narrator reads each caption as it arrives, and waits whenever you hurry.</p>
</section>
<!-- the story: four panels drawn by the script as engravings, each with its caption; a caption unrolls from its
left edge when it comes on screen, then the narrator reads it, one word at a time -->
<section class="strip" aria-label="Chapter one">
<figure class="panel panel--wide" data-panel="harbour">
<canvas class="panel__art" data-art aria-hidden="true"></canvas>
<figcaption class="caption caption--top" data-caption>Every night at eleven the last ferry leaves Corran harbour with its lamps lit and nobody aboard.</figcaption>
</figure>
<figure class="panel panel--right" data-panel="shore">
<canvas class="panel__art" data-art aria-hidden="true"></canvas>
<figcaption class="caption caption--bottom" data-caption>The ferryman counts the lights along the shore, one by one, until the fog has taken all of them.</figcaption>
</figure>
<figure class="panel panel--left" data-panel="bell">
<canvas class="panel__art" data-art aria-hidden="true"></canvas>
<figcaption class="caption caption--top caption--end" data-caption>Halfway across, a bell rings out where no buoy has ever been.</figcaption>
</figure>
<figure class="panel panel--wide" data-panel="answer">
<canvas class="panel__art" data-art aria-hidden="true"></canvas>
<figcaption class="caption caption--bottom" data-caption>He slows the engine and lifts his lantern. Far below, the water answers with a light of its own.</figcaption>
</figure>
</section>
<section class="end">
<p class="kicker">Chapter one is free to read</p>
<h2 class="end__title">Out on 14 November</h2>
<div class="end__actions">
<a class="button" href="#">Read chapter one</a>
<a class="button button--ghost" href="#">Pre-order the book</a>
</div>
</section>
</main>
<!-- the narrator: what it is doing, and the switch for a real voice where the browser has one -->
<aside class="narrator" data-narrator aria-live="off">
<span class="narrator__bars" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<span class="narrator__state" data-state>Narrator: quiet</span>
<button class="narrator__voice" type="button" data-voice aria-pressed="false" hidden>
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><path d="M3 7.5h3l4-3.5v12l-4-3.5H3z" fill="currentColor"/><path d="M13 7a4.2 4.2 0 0 1 0 6M15.4 4.8a7.4 7.4 0 0 1 0 10.4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<span data-voice-label>Read aloud</span>
</button>
</aside>
<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="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.
41 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 “Captions that read aloud” from Animorio and fit it into my siteEach scene is painted in greys on three hidden canvases: the tone, from black to white, the angle of the lines and how much accent colour. Then every pixel becomes ink, paper or red by comparing the tone with a field of wavy lines SPACING pixels apart, as in the site’s shader: below a first threshold it is ink, above it the colour, above a second threshold paper, and the lines push both thresholds about, so the edges of the shadows are hatched. In the dark half of each line is ink, in the light none; in between the lines break into dots.
Each panel is engraved three times, with a slightly different wobble in the lines and a frame crooked another way, and the three drawings take turns BOIL times a second. They are prepared when the panel is one screen away, one drawing at a time, so the page never stalls.
When the caption’s top edge passes ENTER per cent of the screen, the box starts at FROM of its width and unrolls in UNROLL seconds with the original’s cubic-bezier(0.45, 0.30, 0.16, 1), while the whole caption comes in from SHIFT pixels to the left. The box widens with scaleX, the text does not: a clip-path following the box’s edge cuts it. Each line comes in from the left edge in SLIDE seconds (power3.out), STAGGER seconds after the one before.
Each word lasts VOICE.perLetter seconds for each letter plus VOICE.base, never less than VOICE.shortest; between two words comes VOICE.gap, after a comma VOICE.comma, after a full stop VOICE.stop. These are the averages measured on the words of the site’s narrating voice. A word is red from LEAD seconds before it starts to LEAD after it ends, with a smoothstep; PACE speeds everything up or slows it down.
Captions that arrive join a queue. The narrator takes the first one only when it is not reading already and the page has stayed under CALM pixels a frame for WAIT seconds. A caption that leaves the screen leaves the queue, or fades out in 0.4 seconds if it was the one being read; with REPLAY it rolls up and is read again when it comes back.
With «Read aloud» the narrator uses the browser’s speech synthesis. At every word the voice reaches (the boundary event) the words’ clock is put back on that word, so the red follows the voice even when it goes at another pace; where the event does not come, the red follows the computed timings.
| Name | File | Default | What it changes |
|---|---|---|---|
UNROLL | script.js | 0.8 | The seconds the box takes to unroll from its left edge. |
FROM | script.js | 0.25 | How much of the box is already there when it starts to unroll (0 to 1). |
SHIFT | script.js | 40 | How many pixels to the left the whole caption comes in from. |
SLIDE | script.js | 2 | The seconds each line takes to come in from the left edge. |
STAGGER | script.js | 0.2 | The seconds between one line and the next (three quarters of it past four lines). |
LEAD | script.js | 0.4 | The seconds a word takes to turn red before it is read, and to go back after. |
PACE | script.js | 1 | The narrator’s speed: 1 is the pace of the recorded voice. |
CALM | script.js | 15 | The scroll speed, in pixels a frame, under which the narrator may start. |
WAIT | script.js | 0.1 | The seconds of calm scrolling before the narrator reads the next caption. |
ENTER | script.js | 62 | Where a caption comes in: its height on the screen, from the top, as a percentage. |
REPLAY | script.js | true | A caption that has left the screen rolls up and is read again when it comes back. |
BOIL | script.js | 8 | How many times a second the engravings change drawing. |
SPACING | script.js | 4.2 | The distance between the lines of the engravings, in pixels. |
VOICE | script.js | { base: -0.036, perLetter: 0.076, shortest: 0.12, gap: 0.06, comma: 0.68, stop: 0.92, first: 0.23 } | The voice’s timings, in seconds: how long a word lasts for each letter (perLetter, plus base, never under shortest), the pause between words, after commas and after full stops, and the wait before the first word. |
For a story told in panels: a comic, a brand’s story, a guided tour, the captions of a silent video. The waiting narrator works best with short captions of two or three lines; the engravings can be replaced with real pictures without touching the captions.
The panels turn upright (4:5) and the captions narrower, with the same narrator and voice. Scrolling with a finger counts like the wheel: a fast flick makes the narrator wait until the page slows down.
No letters flying in, no boxes unrolling and no boiling drawings: the captions are already there and the panels stay still. The narrator still reads, because the red passing over the words changes a colour and moves nothing.
GSAP 3.15.0, IM Fell English 5.3.0, Archivo (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 16 KB; the libraries: GSAP + ScrollTrigger 45 KB.
Tested on 5 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.