Dirck Mulder
Text Animations||3 min read

Build an ASCII Text Renderer in React

Render text as ASCII art using a character ramp mapped to pixel brightness, entirely in the browser with a canvas element.

Before high-resolution screens, programmers rendered images and text using nothing but printable characters. ASCII art was practical necessity. Today it is pure aesthetic, and it hits differently in a world of polished UI. This component brings that texture to the browser, with real-time waves and mouse interaction.

The final result

ASCIIText renders a string as an ASCII art display using a Three.js scene with a custom post-processing filter. The text undulates with shader-driven waves, reacts to mouse movement, and renders with a radial color gradient applied through CSS mix-blend-mode.

Setting up

This component is the most complex in the library. It requires Three.js and loads the IBM Plex Mono font from Google Fonts.

tsx
import * as THREE from 'three';

Building the component

Three classes do the work: CanvasTxt renders the text to an offscreen canvas, AsciiFilter converts the rendered scene to ASCII characters, and CanvAscii orchestrates everything.

CanvasTxt draws the text string to a canvas sized exactly to fit the text.

tsx
render() {
  if (this.context) {
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.context.fillStyle = this.color;
    this.context.font = this.font;
    const metrics = this.context.measureText(this.txt);
    const yPos = 10 + metrics.actualBoundingBoxAscent;
    this.context.fillText(this.txt, 10, yPos);
  }
}

AsciiFilter reads pixel data from the WebGL renderer, downsamples it to a low-resolution canvas, then maps brightness values to characters from a density ramp.

tsx
asciify(ctx: CanvasRenderingContext2D, w: number, h: number) {
  const imgData = ctx.getImageData(0, 0, w, h).data;
  let str = '';
  for (let y = 0; y < h; y++) {
    for (let x = 0; x < w; x++) {
      const i = x * 4 + y * 4 * w;
      const [r, g, b, a] = [imgData[i], imgData[i+1], imgData[i+2], imgData[i+3]];
      if (a === 0) { str += ' '; continue; }
      const gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;
      let idx = Math.floor((1 - gray) * (this.charset.length - 1));
      if (this.invert) idx = this.charset.length - idx - 1;
      str += this.charset[idx];
    }
    str += '\n';
  }
  this.pre.innerHTML = str;
}

The shader material adds wave distortion to the text mesh. The vertex shader displaces vertices using sin and cos functions of time and position, creating a subtle wobble.

glsl
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 fragment shader splits the RGB channels slightly based on position and time, creating a chromatic aberration effect.

glsl
float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;
float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;
float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;

How to use it

tsx
<ASCIIText
  text="HELLO"
  asciiFontSize={8}
  textFontSize={200}
  textColor="#fdf9f3"
  enableWaves={true}
/>

The component needs a container with an explicit height. Set planeBaseHeight to adjust how large the text mesh appears in the scene.

Key takeaways

  • The ASCII effect works by reading pixel brightness from a WebGL render and mapping it to a character density ramp. Denser characters represent darker areas.
  • Using mix-blend-mode: difference on the <pre> element creates the gradient color effect without needing to color each character individually.
  • Waiting for document.fonts.ready before initializing prevents the font from loading mid-render, which would cause the character width measurements to be wrong.