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.
This titles 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 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.
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.