Warped Text Wall

A wall of text in tall draggable columns, crisp at the top and smeared into hot-pink weather at the bottom as a flow field resamples every character.

How it works

This is the full-bleed hero on nell.ai — a wall of text you can drag sideways, readable along the top and dissolving into a smear as it falls. The React component is called NellShaderCanvas, which is a misnomer worth calling out: there is no WebGL anywhere in it. It is a plain 2D canvas painting monospace characters with batched fillText. Everything below was read out of their minified chunk (_next/static/chunks/8147-*.js), so this is a port of the real pipeline rather than a guess at the look.

The column wall. A ~700-word company text is split on whitespace into words. The code then builds an index list f of *mid-sentence entry points* — scanning from word 9 to length - 9, it keeps every position whose previous word does not end a sentence (/[.!?]["")\]]*$/). Those words are laid out into narrow vertical columns: pitch 294.21875px, column width 283.828125px, left pad 10.390625px, 28 columns per block, giving a block width of 8238.125px (294.21875 × 28, exactly). Column *n* takes the text rotated to begin at word f[(37n + 19) % f.length] — so every column opens mid-thought rather than on a tidy capital — concatenates that rotation to itself four times so it can never run dry, then greedy-word-wraps it to floor(283.828125 / charWidth) characters. One quirk is preserved faithfully: because the inner loop variable shadows the outer one in their minified code, column content repeats every 28 columns while column positions keep advancing — so extra blocks lengthen the strip without ever introducing new text.

The warp is a resample, not a transform. The wall is rendered as a character grid — 10px monospace, 13px line height, with charWidth measured once as measureText("M" × 20).width / 20. For every grid cell the renderer computes a displacement and then samples the character from the displaced position instead of the cell's own. Nothing is rotated, skewed, or scaled; each cell simply asks a different part of the text wall what letter it ought to be. That resampling through a flow field is the whole effect.

The field is classic 3D value noise: hash fract(sin(127.1x + 311.7y + 74.7z) * 43758.5453123) * 2 - 1, smoothstep-interpolated (t*t*(3-2t)) trilinearly across the lattice. Per cell, with o = max(rectWidth, rectHeight) and t in seconds, a swirl amount s = 1.2 + 2.9 * |noise3(x/o*3, y/o*3 - 0.1t, 0.03t)| warps the lookup coordinates into h = (x/o - 0.5) * s * 2 and u = (y/o - 0.5) * s - 0.08t at depth d = 0.1t, and the displacement is [620 * noise3(h + 31.7, u, d) * l, 780 * noise3(h, u + 47.3, d) * l]. Time enters only as the noise's third dimension (0.1t) plus that slow -0.08t vertical drift, so the field flows rather than jitters.

l is the falloff, and it is why the thing stays legible. It is anchored at the top of the canvas — their anchor term is always 0 — and computed as the squared normalized Y, so the first rows are essentially undisplaced and the smear grows quadratically as your eye travels down. Crisp text at the top, illegible pink weather at the bottom.

The call to action is stamped into the grid, not drawn over it. On the real site JOIN THE WAITLIST is written into the row nearest the visual-viewport centre; those cells always render the CTA character, and the only thing that animates is their colour. The reveal order is a hash shuffle — character indices sorted by fract(sin((i+1) * 92821.731) * 1e4) — each index flipping from ink to white once elapsed progress passes (order + 1) / count, across 3.2 seconds. The words don't type in; they precipitate out of the noise in a fixed, deterministic scatter.

Interaction is one number. Mouse wheel (whichever of deltaX/deltaY is larger in magnitude), pointer drag, and the Arrow keys (one column pitch per press) all feed a single target offset, clamped to [-(8238.125 - innerWidth + 294.21875), 0], which the renderer eases toward with current += (target - current) * 0.16. Pointer-down adds a grabbing-cursor class, and presses landing on links, buttons, inputs, or dialogs are excluded so the page stays usable. prefers-reduced-motion freezes time — it renders the finished 3.2s state and redraws only on interaction — and viewports <= 767px throttle to 24fps with devicePixelRatio capped at 1.5. For performance, runs of consecutive same-coloured characters are accumulated and flushed as a single fillText rather than one call per glyph. The palette comes from CSS custom properties: paper hsl(0 100% 18.2%) (deep oxblood), ink hsl(351.4 100% 65.7%) (hot pink-red), CTA hsl(0 0% 100%).

Two honest deviations. (1) The text here is placeholder lorem ipsum, not Nell's own company text. The effect is the layout-and-warp pipeline, not their marketing copy, and the text lives in a single clearly-marked constant you can swap for any long body of writing. (2) The CTA reads DRAG TO EXPLORE instead of JOIN THE WAITLIST, which doubles as a usage hint. Every geometry, noise, warp, reveal, easing, and clamp constant above is kept exactly as captured. Fully self-contained: no external scripts, fonts, or images — just the system monospace stack and one 2D canvas.

One further concession to living in a gallery: the original calls preventDefault on every wheel event, which costs it nothing on a page that never scrolls but would trap the host page's scroll inside an iframe. Here the wheel handler stands down when the file detects it is framed — drag and the arrow keys still move the strip.

How to use this file

This is a complete, self-contained HTML document — copy it verbatim rather than reconstructing it. Markdown version: https://fx.statico.io/effects/nell-text-warp.md. Full writeup and live preview: https://fx.statico.io/demos/nell-text-warp.

Source and credit

Effect originally seen on Nell (https://nell.ai/) — credit to Nell. This is an independent recreation built from scratch for educational use; no proprietary source code was copied. Released under the Unlicense.

More

Terms used above are defined in the glossary. See also the site map, AGENTS.md and llms.txt.

This effect needs a 2D canvas context.