All posts

Turning an Image into ASCII Art: How Luminance Mapping Works

July 27, 2026 · DevTools

ascii-art
image-processing
canvas
javascript
developer-tools

You have an image and you want it in text. You could save it as a PNG. But for terminals, logs, code comments, and the occasional aesthetic choice, ASCII art is the answer. The process sounds complex but is straightforward: read pixel brightness, map brightness to character density, and output the characters in a grid.

The Image to ASCII Converter does this in your browser using the Canvas API. Here is exactly how it works.

The luminance formula

Human perception of brightness is not uniform across colors. We are most sensitive to green and least sensitive to blue. The ITU-R BT.601 standard defines the luminance coefficients that approximate this:

Luminance = 0.299 × R + 0.587 × G + 0.114 × B

These weights matter. A naive average (R + G + B) / 3 treats all channels equally and produces incorrect brightness — blue-heavy images look brighter than they should be. The weighted formula produces visually accurate luminance.

For a pixel with RGB values (255, 128, 64):

Luminance = 0.299(255) + 0.587(128) + 0.114(64)
          = 76.245 + 75.136 + 7.296
          = 158.677
Normalize: 158.677 / 255 = 0.622 → 62.2% brightness

From pixels to characters

Once you have a luminance value (0–1), map it to a character from a density ramp. The ramp lists characters from darkest to lightest — each character fills a different fraction of its cell visually.

A typical density ramp (dark → light):

Density ramp:  @#%*+=-:.  (8 characters)
               ↓
Mapped by brightness:
0.00–0.12  →  @
0.12–0.25  →  #
0.25–0.37  →  %
...
0.88–1.00  →  .

A dark pixel becomes @. A light pixel becomes . (or space). This produces the recognizable ASCII art effect.

The Image to ASCII Converter uses a 70-character ramp for finer brightness gradation and supports multiple ramp styles (standard, simple, detailed).

The glyph-aspect correction

This is the subtle step that separates a good converter from a crude one.

Monospace fonts are taller than they are wide — typically a 2:1 height-to-width ratio. In a character grid, each cell is a rectangle twice as tall as it is wide. A 1:1 pixel-to-character mapping would therefore produce an image that looks vertically stretched.

The fix: scale the pixel grid to sample fewer rows than columns. For a character grid of width W, sample approximately W × 0.5 pixels vertically (assuming 2:1 glyph ratio). This produces square pixels in the output.

const charWidth = cols;
const charHeight = Math.floor(cols * 0.5);  // correct for 2:1 glyph aspect

// For each output row and column, sample the pixel at:
//   pixelX = col * (imageWidth / charWidth)
//   pixelY = row * (imageHeight / charHeight)

Without this correction, a face in the input appears vertically squashed in the output.

Implementation in the browser

The Canvas API provides everything needed — no external library:

async function imageToAscii(imgSrc, cols = 80) {
  const img = new Image();
  img.src = imgSrc;

  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);

  const charHeight = Math.floor(cols * 0.5);
  canvas.width = cols;
  canvas.height = charHeight;

  ctx.drawImage(img, 0, 0, cols, charHeight);

  const pixels = ctx.getImageData(0, 0, cols, charHeight).data;
  const ramp = "@#%*+=-:. ";

  let ascii = "";
  for (let y = 0; y < charHeight; y++) {
    for (let x = 0; x < cols; x++) {
      const i = (y * cols + x) * 4;
      const r = pixels[i], g = pixels[i+1], b = pixels[i+2];
      const luminance = 0.299*r + 0.587*g + 0.114*b;
      const charIndex = Math.floor((luminance / 255) * (ramp.length - 1));
      ascii += ramp[charIndex];
    }
    ascii += "\n";
  }
  return ascii;
}

Inverting the brightness

By default, dark pixels map to dense characters and light pixels to sparse ones — white-on-black rendering. Some images look better inverted (light pixels → dense characters, dark pixels → sparse characters):

// Invert: 0.0 brightness (black) → ramp[last] (space)
//         1.0 brightness (white) → ramp[0] (@)
const rampIndex = invert
  ? Math.floor((1 - luminance / 255) * (ramp.length - 1))
  : Math.floor((luminance / 255) * (ramp.length - 1));

Use the Image to ASCII Converter to toggle inversion and try different density ramps — the right combination depends on the input image's contrast and the output font.

Color ASCII art

True color ASCII art maps each pixel to an ANSI color code plus a density character. The text is colored, creating a colored image in a terminal that supports 256-color ANSI:

// Compute color per pixel
const r = pixels[i];
const g = pixels[i+1];
const b = pixels[i+2];

// Map to 256-color ANSI code
const ansiCode = 16 + Math.floor(r/43)*36 + Math.floor(g/43)*6 + Math.floor(b/43);

// Output: \x1b[38;5;${ansiCode}m${char}\x1b[0m

Color ASCII art requires a terminal that supports 24-bit color or 256-color ANSI. GitHub's markdown renderer does not support ANSI codes, so color ASCII art does not render in README files — plain monochrome ASCII art is the only reliable choice for web documents.

What makes a good input image

ASCII art works best with:

  • High contrast images
  • Clear silhouettes and edges
  • Simple compositions with few overlapping tones

It struggles with:

  • Photorealistic images with smooth gradients
  • Low-contrast images
  • Fine detail that gets lost in the character grid

A 200×200 pixel image sampled to 80 columns produces reasonable results. A 1920×1080 image scaled to 80 columns loses significant detail. Resize the input image to a moderate size before conversion for the best balance of detail and grid density.

The Image to ASCII Converter runs entirely in your browser — no image is uploaded or transmitted. Resize your image first with the Image Compressor or Image Format Converter to get it to a manageable size before conversion.