Skip to content
← writing

Turning WebGL into ASCII, every frame

three
webgl
shaders
frontend

This titled the HypeDuel arena over in ai-arena, and it's a neat trick: real 3D text, rendered with shaders, then converted to ASCII in the browser every single frame.

loading…

Move your cursor over it — the plane tilts and the colors hue-shift to follow.

Two canvases doing different jobs

It starts ordinary. The word gets drawn to an offscreen 2D canvas, uploaded as a texture, and mapped onto a subdivided PlaneGeometry. A vertex shader rolls the vertices on sine waves; a fragment shader nudges the R, G, and B channels by slightly different amounts so the edges chromatically shimmer.

transformed.x += sin(time + position.y) * 0.5 * waveFactor;
transformed.y += cos(time + position.z) * 0.15 * waveFactor;
transformed.z += sin(time + position.x) * waveFactor;

The ASCII pass

Here's the part that makes it. Each frame, the WebGL output is drawn into a second, tiny canvas — sized so one pixel equals one output character. Then every pixel is read back, converted to brightness, and mapped to a glyph from a ramp ordered dark-to-light:

const gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;
const idx = Math.floor((1 - gray) * (charset.length - 1));
str += charset[idx]; // " .:-=+*#%@" style ramp, just longer

The whole string drops into a <pre> with mix-blend-mode: difference and a gradient clipped to the text. Cursor position feeds both the plane's rotation and a hue-rotate on the container, so the thing tracks the mouse with zero per-character work.

Keeping it off the server

three runs in the browser only. On Cloudflare Workers, importing it into the SSR graph crashes the render, so the component is a thin loader that pulls the heavy scene in with a dynamic import() inside useEffect. Same pattern I used for the Infinite Terrain scene.

From B3's ai-arena; the underlying effect comes from the React Bits community. Grab it on the ASCII Text page.

Ask your agent to implement this

Read the full writeup at https://seangeng.com/writing/ascii-text-from-webgl.md and implement it in my project.

It covers: Turning WebGL into ASCII, every frame — A three.js plane rendered to a hidden canvas, then read pixel-by-pixel and rewritten as ASCII characters in a <pre>. The trick is sampling the render at one pixel per glyph. From B3's ai-arena.

Requirements:
- Follow the technique/approach exactly as described in the writeup.
- Adapt names, colors, and styling to my project's existing conventions.
- If it's a component, make it reusable with sensible props and TypeScript types.
- Keep it accessible: semantic HTML, keyboard support, and respect prefers-reduced-motion.
- When done, tell me which files you created or changed and how to use it.

Paste into Claude Code, Codex, Cursor, or any agent. view raw .md download source .zip