ASCII ENGINE

A free, dependency-free JavaScript library that turns images, videos, GIFs and live audio into ASCII art — 100% client-side, in any browser tab. Drop it into your own site or game with one <script> tag.

MIT License Zero dependencies No server, no API key v… ★ GitHub

Quick start

No build step, no bundler, no npm install. Just load the file and call a function:

<script src="https://ascii-generator.eu/ascii-engine.js"></script>
<script>
  const img = document.querySelector("img");
  const { plain } = AsciiEngine.renderRamp(img, 100, " .:-=+*#%@");
  console.log(plain); // ASCII-art string — drop it in a <pre>
</script>

The source argument accepts any CanvasImageSource: <img>, <video>, <canvas>, ImageBitmap, VideoFrame, OffscreenCanvas — so this works just as well inside a game loop (feed it your own game canvas every frame) as it does with an uploaded photo. Everything runs synchronously on a single reused <canvas>: no network calls, no server, no rate limits.

Using TypeScript? Grab ascii-engine.d.ts alongside the script for full autocompletion and type-checking — no install needed, just drop it next to your source or reference it via tsconfig.json's "types".

Live demo

This runs ascii-engine.js directly, with no other code from ascii-generator.eu — exactly what you'd get embedding it yourself.

Click or drop an image here

  

API reference

AsciiEngine.renderRamp(source, cols, ramp, opts?)

Converts a source into an ASCII grid using a character density ramp (darkest → brightest). The general-purpose renderer.

renderRamp(source, cols, ramp, { color, charAspect, srcWidth, srcHeight, contrast, brightness, invert, dither }) → { plain, rows, colorGrid, html }
sourceAny CanvasImageSource (<img>, <video>, <canvas>, ImageBitmap, VideoFrame...).
colsOutput width in characters. Row count is derived from the source's aspect ratio.
rampCharacters from darkest/sparsest (index 0) to brightest/densest (last). Any length ≥ 1, e.g. " .:-=+*#%@".
opts.colorAlso return per-cell RGB + an HTML string with colored glyphs. Default false.
opts.contrast / brightness-100..100 manual adjustment, applied after auto-contrast.
opts.invertFlip dark/light.
opts.ditherFloyd-Steinberg dithering across the ramp's levels, for smoother gradients.
returnsplain (string), rows (string[]), colorGrid/html (only when color:true).

AsciiEngine.renderBraille(source, cols, opts?)

Like renderRamp, but renders via Unicode Braille characters — each glyph encodes a 2×4 dot sub-grid with dithering, giving ~4x the effective resolution at the same column count. Monochrome only.

renderBraille(source, cols, { charAspect, srcWidth, srcHeight, contrast, brightness, invert }) → { plain, rows, colorGrid: null, html: null }

AsciiEngine.renderEdges(source, cols, opts?)

Renders clean line art via Sobel edge detection instead of a density ramp: draws a directional character (| - / \) where an edge is detected, a space elsewhere.

renderEdges(source, cols, { charAspect, srcWidth, srcHeight, contrast, brightness, edgeThreshold=60 }) → { plain, rows, colorGrid: null, html: null }

AsciiEngine.renderAudioGrid(data, cols, rows, mode?, opts?)

Renders live Web Audio analyser data as an ASCII grid: a classic oscilloscope trace ("wave"), a filled oscilloscope band ("wavefill"), a spectrum analyzer ("bars"), a center-mirrored spectrum ("mirror"), a radial/circular spectrum ("circle"), or a volume-reactive particle scatter ("dots"). Pure data-in/grid-out — this function never touches microphones, AudioContext, or any audio API; you own that lifecycle entirely (requesting the mic, an AnalyserNode, stopping tracks on cleanup).

renderAudioGrid(data, cols, rows, mode="bars", { ramp, color, gain=2.6, curve=0.5 }) → { plain, rows, colorGrid, html }
dataFor "wave"/"wavefill": time-domain samples (0-255, 128=silence) — exactly what analyser.getByteTimeDomainData() produces. For "bars"/"mirror"/"circle"/"dots": frequency-domain magnitudes (0-255) — analyser.getByteFrequencyData().
cols / rowsOutput size in characters.
opts.gainSensitivity multiplier applied before mapping to visual intensity, so quiet sounds still produce a visible reaction (clamped after). Default 2.6.
opts.curveExponent applied after gain — values <1 compress the dynamic range, boosting quiet signals proportionally more than loud ones (perceived loudness is roughly logarithmic, so linear gain alone still feels flat). Default 0.5.
opts.colorDecorative rainbow hue gradient across columns (not tied to actual frequency in Hz). Default false.
const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
analyser.fftSize = 1024;
ctx.createMediaStreamSource(await navigator.mediaDevices.getUserMedia({ audio: true }))
   .connect(analyser);

const data = new Uint8Array(analyser.frequencyBinCount); // "bars"/"mirror"/"circle"/"dots"
function frame(){
  analyser.getByteFrequencyData(data);
  const { plain } = AsciiEngine.renderAudioGrid(data, 100, 30, "bars");
  output.textContent = plain;
  requestAnimationFrame(frame);
}
frame();

AsciiEngine.renderValueGrid(grid, ramp, opts?)

Renders a plain 2D grid of already-computed values (0-1) as an ASCII grid — for generative sources (cellular automata, reaction-diffusion, particle simulations, or anything else that's already a grid of numbers) where there's no image to sample, unlike renderRamp/renderBraille/renderEdges. Pure data-in/grid-out — this function never runs any simulation itself, you own that entirely.

renderValueGrid(grid, ramp, { color }) → { plain, rows, colorGrid, html }
gridgrid[row][col], each value clamped to 0-1.
rampDensity ramp, darkest/sparsest first (index 0) to brightest/densest (last) — same convention as renderRamp.
opts.colorMaps each cell's value to a data-driven dim-amber → warm-white heat gradient (not a decorative rainbow) instead of monochrome. Default false.
// Conway's Game of Life, one generation -> ASCII
const grid = nextGeneration(cells); // your own simulation, values already 0 or 1
const { plain } = AsciiEngine.renderValueGrid(grid, " .:-=+*#%@");
output.textContent = plain;

AsciiEngine.drawGrid(ctx, rows, x, y, w, h, opts?)

Draws an ASCII grid (from any render* function's .rows) onto a canvas, centered and sized to fit a box.

drawGrid(ctx, rows, x, y, w, h, { colorMode, colorGrid, monoColor, glow, glowIntensity, charWidthRatio, lineHeightRatio, fontFamily }) → { x, y, width, height, fontPx }

AsciiEngine.applyPostFx(ctx, x, y, w, h, opts?)

CRT-style post-processing on an already-drawn canvas region (call right after drawGrid): chromatic aberration, scanlines, vignette.

applyPostFx(ctx, x, y, w, h, { scanlines, // 0-100 scanlineSpacing, // px, default 3 vignette, // 0-100 chromaticAberration // 0-20 px })

AsciiEngine.fitGridFontPx / fitFontPxForLines

Computes the largest font size that fits a grid (or variable-width text lines, e.g. FIGlet banners) inside a box without overflowing.

fitGridFontPx(cols, rowCount, boxWidthPx, boxHeightPx, opts?) → number fitFontPxForLines(ctx, lines, availW, availH, lineHeightRatio?, fontFamily?) → number

AsciiEngine.measureMaxCharWidthRatio(opts?)

Measures the actual rendered width/height ratio of the widest glyph among reference characters — more reliable than assuming a fixed monospace ratio (some glyphs, Braille especially, aren't perfectly monospace in every browser's fallback font). Cached.

measureMaxCharWidthRatio({ fontFamily, refChars }) → number

AsciiEngine.isGifDecoderSupported

Boolean. true when the browser supports the WebCodecs ImageDecoder API (Chrome/Edge; not Firefox/Safari as of this writing), required by openGifFrameSource.

AsciiEngine.openGifFrameSource(file)

Opens an animated GIF for frame-by-frame decoding via WebCodecs — necessary because a canvas cannot reliably capture an animated <img> GIF's current frame via drawImage() (it stays stuck on frame 0). Returns null if unsupported.

await openGifFrameSource(file) → null | { frameCount, frameIndex, width, height, currentFrame(), // → VideoFrame, drawable by render* currentFrameDurationMs(), advance(), // background-decode next frame (live playback) seekFrame(index), // await a specific frame (export loops) close(), }

AsciiEngine.createGifEncoder(opts)

Thin wrapper around the third-party gif.js library for encoding canvas frames into an animated GIF. Doesn't load gif.js itself — load it yourself and pass your worker script URL. Note: new Worker(url) requires a same-origin script, so if loading gif.js from a CDN, fetch the worker file yourself and pass a URL.createObjectURL(blob).

createGifEncoder({ width, height, workerScript, workers=2, quality=10, background }) → null | { addFrame(canvas, delayMs?), render(): Promise<Blob>, abort(), }

AsciiEngine.isMp4EncoderSupported

Boolean. true when the browser supports the WebCodecs VideoEncoder API (Chrome/Edge, Safari 16.4+; not Firefox as of this writing), required by createMp4Encoder.

AsciiEngine.createMp4Encoder(opts)

Thin wrapper around the third-party Mediabunny library for encoding a sequence of canvas frames into a real MP4 (H.264) video — useful where animated GIF isn't accepted (Instagram/Facebook feed and story composers only take photo or video). Doesn't load Mediabunny itself (it ships as an ES module, not a plain browser global): import() it yourself and assign the namespace to window.Mediabunny first. Returns null if that hasn't been done, or if isMp4EncoderSupported is false.

Unlike createGifEncoder, the canvas is bound once at construction (Mediabunny's CanvasSource captures whatever is currently drawn on that exact element each time you call addFrame) — redraw opts.canvas in place before each call, don't pass a fresh canvas per frame.

createMp4Encoder({ canvas, frameRate=10, bitrate=1000000 }) → null | { addFrame(delayMs?): Promise<void>, // reads the canvas's current pixels finalize(): Promise<Blob>, // resolves to the final video/mp4 Blob cancel(): Promise<void>, }
const encoder = AsciiEngine.createMp4Encoder({ canvas: myCanvas, frameRate: 12 });
for (const frame of frames){
  drawFrameToCanvas(myCanvas, frame);      // redraw the SAME canvas each time
  await encoder.addFrame(83);              // ~83ms per frame at 12fps
}
const mp4Blob = await encoder.finalize();

AsciiEngine.autoContrastRange(values) / stretch(v, lo, hi)

Low-level helpers used internally by every render* function: autoContrastRange finds the [min,max] luminance bounds in a sample; stretch remaps a value from that range to 0-255.

License

MIT. Use it in commercial or personal projects, modify it, ship it in a game — no attribution required, no strings attached. See LICENSE for the full text.

Built and maintained as part of ascii-generator.eu. This file is the exact same engine that site runs — updates ship here first. Source on GitHub — issues and pull requests welcome.