A framework-agnostic ASCII art rendering engine for the browser. Convert images, animated GIFs, and video into character-based art rendered onto an HTML canvas — with full color support, animated backgrounds, interactive hover effects, and embed generation. Zero runtime dependencies.
asciify-engine works in two stages:
- Convert — a source (image, GIF buffer, video element) is sampled and converted into an
AsciiFrame: a 2D array of character cells, each carrying a character and RGBA color data. - Render — the frame is drawn onto a
<canvas>element via a 2D context, with full support for color modes, font sizes, hover effects, and time-based animations.
This separation means you can pre-compute frames once and render them at any frame rate, making it efficient for both static images and smooth animations.
npm install asciify-engineWorks with any modern bundler (Vite, webpack, esbuild, Rollup) and any framework — React, Vue, Svelte, Angular, Next.js, or vanilla JS.
imageToAsciiFrame accepts any HTMLImageElement, HTMLVideoElement, or HTMLCanvasElement and returns a single ASCII frame.
import { imageToAsciiFrame, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'photo.jpg';
img.onload = () => {
const canvas = document.getElementById('ascii') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;
const opts = { ...DEFAULT_OPTIONS, fontSize: 10, colorMode: 'fullcolor' as const };
const { frame } = imageToAsciiFrame(img, opts, canvas.width, canvas.height);
renderFrameToCanvas(ctx, frame, opts, canvas.width, canvas.height);
};gifToAsciiFrames parses a GIF ArrayBuffer and returns one AsciiFrame per GIF frame, preserving the original frame rate.
import { gifToAsciiFrames, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
const buffer = await fetch('animation.gif').then(r => r.arrayBuffer());
const canvas = document.getElementById('ascii') as HTMLCanvasElement;
const opts = { ...DEFAULT_OPTIONS, fontSize: 8 };
const { frames, fps } = await gifToAsciiFrames(buffer, opts, canvas.width, canvas.height);
let frameIndex = 0;
setInterval(() => {
renderFrameToCanvas(canvas.getContext('2d')!, frames[frameIndex], opts, canvas.width, canvas.height);
frameIndex = (frameIndex + 1) % frames.length;
}, 1000 / fps);asciifyVideo streams video as live ASCII art in real time. Instant start, constant memory, unlimited duration.
⚠️ Never set the backing<video>element todisplay: none— browsers skip GPU frame decoding. When given a URL string,asciifyVideohandles this automatically.
import { asciifyVideo } from 'asciify-engine';
const canvas = document.getElementById('ascii') as HTMLCanvasElement;
// Minimal
const stop = await asciifyVideo('/clip.mp4', canvas);
// Fit canvas to a container and re-size automatically on resize:
const stop = await asciifyVideo('/clip.mp4', canvas, {
fitTo: '#hero', // or an HTMLElement
});
// Lifecycle hooks — ready state, timers, etc.:
const stop = await asciifyVideo('/clip.mp4', canvas, {
fitTo: '#hero',
fontSize: 6,
onReady: () => setLoading(false),
onFrame: () => setElapsed(t => t + 1),
});
// Pre-extract all frames before playback (frame-perfect loops, short clips):
const stop = await asciifyVideo('/clip.mp4', canvas, { preExtract: true });
// Clean up:
stop();All conversion and render functions accept an AsciiOptions object. Spread DEFAULT_OPTIONS as a base and override what you need.
| Option | Type | Default | Description |
|---|---|---|---|
fontSize |
number |
10 |
Character cell size in pixels. Smaller values increase density and detail. |
colorMode |
'grayscale' | 'fullcolor' | 'matrix' | 'accent' |
'grayscale' |
Determines how pixel color is mapped to character color. |
charset |
string |
Standard ramp | Characters ordered from dense to sparse, representing brightness levels. |
brightness |
number |
0 |
Brightness adjustment from -1 (darker) to 1 (lighter). |
contrast |
number |
1 |
Contrast multiplier applied before character mapping. |
invert |
boolean | 'auto' |
false |
Inverts the luminance mapping — light areas become dense, dark areas sparse. Set to 'auto' to auto-detect from OS color scheme (light mode → invert, dark mode → normal). |
renderMode |
'ascii' | 'dots' |
'ascii' |
Render as text characters or circular dot particles. |
hoverEffect |
string |
'none' |
Interactive effect driven by cursor position. See hover effects below. |
hoverStrength |
number |
0 |
Effect intensity (0–1). 0 = hover disabled. |
hoverRadius |
number |
0.2 |
Effect radius relative to canvas size (0–1). |
chromaKey |
true | 'blue-screen' | {r,g,b} | string | null |
null |
Remove a background colour. true = heuristic green screen (any shade). 'blue-screen' = heuristic blue screen. Custom: {r,g,b} or any CSS hex string keyed by Euclidean distance. null to disable. |
chromaKeyTolerance |
number |
60 |
Euclidean RGB distance threshold for chroma-key detection. 0 = exact match, higher = more pixels removed (max useful ~100). |
Remove a solid background colour from any source — images, GIFs, or video — so the canvas background shows through keyed pixels.
import { asciify, DEFAULT_OPTIONS } from 'asciify-engine';
// Green screen — zero config, just set true:
asciify(img, canvas, {
options: { ...DEFAULT_OPTIONS, chromaKey: true, colorMode: 'fullcolor' },
});
// Blue screen
asciify(img, canvas, {
options: { ...DEFAULT_OPTIONS, chromaKey: 'blue-screen', chromaKeyTolerance: 70 },
});
// Custom RGB key
asciify(img, canvas, {
options: { ...DEFAULT_OPTIONS, chromaKey: { r: 0, g: 180, b: 90 }, chromaKeyTolerance: 50 },
});
// Live video with green screen
asciifyVideo('/footage.mp4', canvas, {
fitTo: '#container',
options: { ...DEFAULT_OPTIONS, chromaKey: true, colorMode: 'fullcolor' },
});Tolerance guide:
40–60— tight key, natural green screen under good lighting60–80— broader key, wrinkled fabric or uneven lighting80–120— aggressive; expect some spill into the subject
| Mode | Description |
|---|---|
grayscale |
Classic monochrome ASCII. Character brightness maps to source luminance. |
fullcolor |
Each character inherits the original pixel color from the source. |
matrix |
Monochrome green — inspired by classic terminal aesthetics. |
accent |
Single accent color applied uniformly across all characters. |
Interactive effects that respond to cursor movement. Pass the effect name to hoverEffect and supply the cursor position to renderFrameToCanvas at render time.
Available effects: spotlight · flashlight · magnifier · force-field · neon · fire · ice · gravity · shatter · ghost
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
renderFrameToCanvas(ctx, frame, opts, canvas.width, canvas.height, Date.now() / 1000, {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
});asciiBackground mounts a self-animating ASCII renderer onto any DOM element — ideal for hero sections, banners, or full-page backgrounds. It manages its own canvas, animation loop, and resize handling internally.
import { asciiBackground } from 'asciify-engine';
const stop = asciiBackground('#hero', {
type: 'rain',
colorScheme: 'auto', // follows OS dark/light mode
speed: 1.0,
density: 0.55,
accentColor: '#d4ff00',
});
// Stop and clean up when no longer needed
stop();| Type | Description |
|---|---|
wave |
Flowing sine-wave field with layered noise turbulence |
rain |
Vertical column rain with a glowing leading character and fading trail |
stars |
Parallax star field that reacts to cursor position |
pulse |
Concentric ripple bursts emanating from the cursor |
noise |
Smooth value-noise field with organic, fluid motion |
grid |
Geometric grid that warps and brightens near the cursor |
aurora |
Sweeping borealis-style color bands drifting across the field |
silk |
Fluid swirl simulation following cursor movement |
void |
Gravitational singularity — characters spiral inward toward the cursor |
morph |
Characters morph between shapes over time, driven by noise |
| Option | Type | Default | Description |
|---|---|---|---|
type |
string |
'wave' |
Which background renderer to use. |
colorScheme |
'auto' | 'light' | 'dark' |
'dark' |
'auto' reacts to OS theme changes in real time. |
fontSize |
number |
13 |
Character size in pixels. |
speed |
number |
1 |
Global animation speed multiplier. |
density |
number |
0.55 |
Fraction of grid cells that are active (0–1). |
accentColor |
string | 'auto' |
varies | Highlight or leading-character color (any CSS hex string). Set to 'auto' to auto-detect: probes --accent-color, --color-accent, --accent, --primary CSS variables on :root, then falls back to OS color scheme (dark ink in light mode, light ink in dark mode). |
color |
string |
— | Override the body character color. |
import { useEffect, useRef } from 'react';
import { imageToAsciiFrame, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
export function AsciiImage({ src }: { src: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = src;
img.onload = () => {
const opts = { ...DEFAULT_OPTIONS, fontSize: 10, colorMode: 'fullcolor' as const };
const { frame } = imageToAsciiFrame(img, opts, canvas.width, canvas.height);
renderFrameToCanvas(canvas.getContext('2d')!, frame, opts, canvas.width, canvas.height);
};
}, [src]);
return <canvas ref={canvasRef} width={800} height={600} />;
}Generate self-contained HTML that can be hosted anywhere or dropped directly into a page — no runtime dependency required.
import { generateEmbedCode, generateAnimatedEmbedCode } from 'asciify-engine';
// Static — produces a single-file HTML with the ASCII art baked in
const staticHtml = generateEmbedCode(frame, options);
// Animated — produces a self-running HTML animation
const animatedHtml = generateAnimatedEmbedCode(frames, options, fps);| Function | Signature | Returns |
|---|---|---|
asciify |
(source, canvas, options?) |
Promise<void> |
asciifyVideo |
(source, canvas, options?) |
Promise<() => void> |
asciifyGif |
(source, canvas, options?) |
Promise<() => void> |
asciifyWebcam |
(canvas, options?) |
Promise<() => void> |
asciiBackground |
(selector, options) |
() => void |
imageToAsciiFrame |
(source, options, w?, h?) |
{ frame, cols, rows } |
renderFrameToCanvas |
(ctx, frame, options, w, h, time?, hoverPos?) |
void |
gifToAsciiFrames |
(buffer, options, w, h, onProgress?) |
Promise<{ frames, cols, rows, fps }> |
videoToAsciiFrames |
(video, options, w, h, fps?, maxSec?, onProgress?) |
Promise<{ frames, cols, rows, fps }> |
generateEmbedCode |
(frame, options) |
string |
generateAnimatedEmbedCode |
(frames, options, fps) |
string |
asciifyVideo options: fitTo (HTMLElement/selector — fits canvas to container + ResizeObserver), preExtract (pre-decode all frames, default false), trim: { start?: number; end?: number } (loop a time slice in seconds, accepts floats), onReady(video), onFrame()
MIT © asciify.org
☕ Buy me a coffee — if this saved you time, I'd appreciate it!