asciify-engine
v1.0.118
Published
Framework-agnostic ASCII art engine. Convert images, videos, and GIFs into ASCII art rendered on canvas.
Maintainers
Keywords
Readme
asciify-engine
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, living charset motion presets, and embed generation.
Overview
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.
Installation
npm install asciify-engineWorks with any modern bundler (Vite, webpack, esbuild, Rollup) and any framework — React, Vue, Svelte, Angular, Next.js, or vanilla JS.
Agent Skill
This repository ships a standards-friendly Agent Skill at skills/asciify-engine. Install it with the current skills CLI:
npx skills add https://github.com/ayangabryl/asciify-engine --skill asciify-engineUse the skill when building ASCII images, videos, GIFs, webcam effects, scroll-scrubbed heroes, hover interactions, or performance-sensitive ASCII media.
Converting Media to ASCII
Images
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);
};Animated GIFs
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);Video
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
});
// Full-bleed hero framing without custom CSS width hacks:
const stop = await asciifyVideo('/hero.mp4', canvas, {
fitTo: '#hero',
objectFit: 'contain',
objectPosition: 'center bottom',
width: '100vw',
height: '100%',
bleed: { x: '2vw' },
});
// 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 });
// Scroll-scrub video time using native browser scroll:
const stop = await asciifyVideo('/hero.mp4', canvas, {
fitTo: '#hero',
scroll: true,
});
// Use GSAP ScrollTrigger when it is already in your app:
const stop = await asciifyVideo('/hero.mp4', canvas, {
fitTo: '#hero',
scroll: {
gsap,
ScrollTrigger,
start: 'top bottom',
end: 'bottom top',
scrub: 1,
speed: 1.5,
},
});
// Clean up:
stop();Rendering Options
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). |
| chromaKeyTrimPadding | number | 0.002 | Normalized safety padding around auto-trimmed keyed video foregrounds. Use 0 for edge-tight marketing heroes. |
| chromaKeyTrimLuminanceThreshold | number | 0 | Minimum source luminance counted as auto-trim foreground. Raise slightly when invisible edge noise prevents keyed subjects from filling a preserved-ratio layout. |
| chromaKeyTrimMode | 'range' \| 'frame' \| 'off' | 'range' | 'range' keeps one stable crop for the playback range. 'frame' remeasures each rendered frame and expands back to the render aspect for moving scrubbed heroes. 'off' disables keyed auto-trim. |
Source Crop
Use sourceCrop to remove empty source edges before ASCII sampling. The recommended API is CSS-like insets; the engine sizes asciifyVideo from the resolved crop aspect so the output does not stretch:
await asciifyVideo('/hero.mp4', canvas, {
options: {
sourceCrop: { top: 0.08, bottom: 0.22 },
},
});Crop from the sides the same way:
await asciifyVideo('/hero.mp4', canvas, {
options: {
sourceCrop: { left: 0.1, right: 0.1 },
},
});Use any side together:
sourceCrop: { top: 0.08, right: 0.12, bottom: 0.22, left: 0.12 }sourceCrop: { height: 0.7 } keeps 70% of the source centered. preserveAspect only applies to single-dimension width/height crops. Side insets (top, right, bottom, left) define the crop window directly, then asciifyVideo uses that crop aspect for layout and render dimensions so a top/bottom crop can become a wide hero band without stretching.
When sourceCrop and chromaKey are both enabled for video, asciifyVideo performs a one-time foreground trim inside the crop window. For scrubbed or trimmed video it samples the playback range and uses the union of the keyed foreground bounds. Keyed empty margins are removed before layout sizing, so green-screen or transparent-looking subjects can fill wide hero canvases without custom CSS hacks or per-frame zoom.
Use chromaKeyTrimMode: 'frame' when the subject moves across a scroll-scrubbed hero and a single range crop still leaves an edge visually short. The engine remeasures the visible foreground per rendered frame and expands the crop to the render aspect, so the result stays proportionate while following the actual subject.
Canvas Layout
Use layout options on asciifyVideo when the ASCII canvas needs to fill a hero, preview frame, or fixed viewport. These affect only the displayed canvas box; they do not change source sampling or ASCII detail.
await asciifyVideo('/hero.mp4', canvas, {
fitTo: '#hero',
objectFit: 'contain',
objectPosition: 'center bottom',
width: '100vw',
height: '100%',
bleed: { x: '2vw' },
options: {
sourceCrop: { top: 0.1, bottom: 0.18 },
},
});objectFit:'contain' | 'cover' | 'fill' | 'none' | 'scale-down'objectPosition: any CSS object-position value, such as'center bottom'scale: visual overfill using CSSscale, preserving existing transformswidth/height: CSS lengths or pixel numbers for the visible canvasbleed: extra layout overfill around the fitted canvas. Use{ x: '2vw' }for full-bleed ASCII heroes where glyph side bearings or character-cell quantization leave a thin visual edge gap.
Use sourceCrop to reframe the input media. Use objectFit, objectPosition, scale, and bleed to place the rendered canvas in the layout. For high-detail scroll scrubbers, keep preExtract off and set maxCachedFrames to bound memory while the engine caches nearby text frames.
Chroma Key (Green/Blue Screen)
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
Color Modes
| 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. |
Hover Effects
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,
});
});Animated Backgrounds
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();Available Background Types
| 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 |
Background Options
| 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. |
React Integration
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} />;
}Embed Generation
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);API Reference
| 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), scroll (true for native scroll scrub, or { gsap, ScrollTrigger, trigger, start, end, scrub } for GSAP), onReady(video), onFrame()
Scroll scrubbing works in live video mode. Leave preExtract off when using scroll.
License
MIT © asciify.org
☕ Buy me a coffee — if this saved you time, I'd appreciate it!
