Text effects playground
Seven interactive text-animation experiments built on top of @chenglou/pretext — magnetic repulsion, constellation lines, shatter, idle rain, cipher decode, spotlight, and a sine wave. Click and hover the demos.
Most text on the web is laid out by the browser and never animates. The browser is fast at this, but it’s also opaque. Once a paragraph is rendered, the only way to know where each character is on screen is to wrap each one in a span and call getBoundingClientRect. That forces a layout cascade and tanks performance the moment you try to do it on every mousemove.
@chenglou/pretext takes the other route. It measures and lays out text without ever touching the DOM, using canvas to measure characters and arithmetic for everything downstream. Once you have per-character {x, y} positions for cheap, a lot of typographic animation becomes possible at 60fps.
This post is seven of those experiments. Each demo is a hydrated React island. Pretext computes the per-character cells once on mount, then the per-frame work is a tiny arithmetic loop. Hover, click, and watch.
1. Magnetic repulsion
Hover anywhere over the text below. Each character pushes away from the cursor, and the strength fades with distance.
What pretext does for it: the layout call runs once on mount and returns the absolute pixel position of every character. The mousemove handler is a pure arithmetic loop (distance, force, transform) and never touches the DOM for measurement. Try doing the same with getBoundingClientRect per character per frame and the page falls over.
2. Constellation hover
A typography starmap. Faint accent-colored lines connect each visible character to its 2 nearest neighbors. Move your cursor over the text and the lines closest to the cursor brighten.
What pretext does for it: every line endpoint is a character center. The pairwise distance graph is computed once from pretext’s coordinates. No SVG remeasure, no DOM walk. Cursor proximity is the only thing that runs per frame, and it’s a single Math.hypot call per edge.
3. Shatter on click
Click the text. Every character detaches, falls under gravity for a moment, then flies back to its exact original position.
What pretext does for it: the “come home” phase needs exact target coordinates for every character, and that’s the part you can’t fake. Without pretext you’d have to wrap every character in a span, snapshot positions via getBoundingClientRect at mount (paying a layout cascade), and store them yourself. Pretext gives you the same data with one canvas pass.
4. Idle rain
The text below stays intact, but every so often a single character drips. A copy of one of its letters detaches and falls a short distance, fading as it goes. The original text never breaks. Wait a second or two for the first drop.
What pretext does for it: each drip’s starting position has to match the source character’s exact pixel position. Otherwise the drip pops weirdly into place instead of detaching cleanly. Pretext lets us pick a random character from the layout and know exactly where to spawn the falling copy from.
5. Cipher decode
The text below is scrambled. Every letter is a random glyph. Click it, and the characters cycle through random letters while a decode wave moves left-to-right, locking each one in on its real value. Click again to re-encode back to nonsense.
What pretext does for it: during the cycle, every character at every frame is a different random glyph. The X position has to stay anchored to the original layout. Otherwise the text would jitter sideways as random letters with different widths get rendered in. Pretext gives us those anchor positions once, and a monospace font keeps each glyph visually centered in its slot during the cycle.
6. Spotlight
The text below is dimmed to a faint silhouette. Move your cursor across it and only the characters within ~80px of your cursor become readable. Drag the spotlight to read.
What pretext does for it: every character needs a center coordinate so the per-frame distance check works. Distance, smoothstep falloff, opacity write. Three operations per character per mousemove, all arithmetic, no DOM measurement. With getBoundingClientRect per character per frame this would be unusable.
7. Sine wave
A continuous, ambient effect with no interaction required. Each character bobs up and down on a sine wave whose phase is offset by its X position. Looks like a flag rippling in slow motion.
What pretext does for it: the phase offset is cell.x * frequency, which means the wave is correlated to pixel position, not character index. That matters because monospace and proportional fonts produce wildly different-looking waves under the two formulas. Pretext lets us drive the math from real layout coordinates, so the result looks the same regardless of font. Honors prefers-reduced-motion (the wave stops if the user opts out).
How the layout helper works
All seven components share a single helper, src/lib/textLayout.ts, that wraps pretext into a position-returning interface:
import { layoutCharacters } from "@/lib/textLayout";
const { cells, totalWidth, totalHeight } = layoutCharacters(
"Hover near these letters and they will lean away.",
"16px ui-sans-serif, system-ui, sans-serif",
520, // maxWidth
28, // lineHeight
);
// cells = [
// { char: "H", x: 0, y: 0, width: 11, lineIndex: 0, index: 0 },
// { char: "o", x: 11, y: 0, width: 8, lineIndex: 0, index: 1 },
// ...
// ]
Internally it calls pretext’s prepareWithSegments(text, font) to analyze the text, then layoutWithLines(prepared, maxWidth, lineHeight) to wrap it at the given width. For each line, it walks the graphemes via Intl.Segmenter, measures each one with a hidden canvas, and accumulates X coordinates. Y is lineIndex * lineHeight.
Pretext does the part that’s actually hard: wrapping text correctly across scripts, emoji, and bidi, without touching the DOM. The per-character position bookkeeping is the easy part on top.
Why a post and not the home page
These effects are fun, but they’re also loud. Putting one on the home page would be an instant gimmick. The kind of thing visitors notice once, click around for ten seconds, and then find slightly annoying on every reload after. Keeping them in a dedicated playground post means people who want to play with them can, and people who don’t aren’t ambushed by them.
Pretext earns a place in the toolkit either way. If you’re ever thinking “I wish I could animate the position of every word in a paragraph and the browser keeps reflowing on me”, this is the library that solves it.