npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

asciify-engine

v4.1.0

Published

Framework-agnostic ASCII art engine. Convert images, videos, and GIFs into ASCII art rendered on canvas.

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, interactive hover effects, living-image motion, 15 Studio render styles, and export.

Media first, optional backgrounds

Use asciify-engine/core for image, video, GIF, webcam, text, and interactive rendering. It excludes the procedural background generators. Root imports expose the same media API. Procedural scenes are downloadable templates at https://asciify.org/backgrounds.

import { asciifyVideo } from 'asciify-engine/core';

const stop = await asciifyVideo('/film.mp4', canvas, {
  fitTo: canvas.parentElement,
  objectFit: 'cover',
  maxRenderDimension: 960,
  fontSize: 10,
  fps: 30,
  options: {
    colorMode: 'accent', accentColor: '#a9b39a',
    hoverEffect: 'trail', hoverStrength: 0.65,
    hoverRadius: 0.18, hoverColor: '#d4ff00',
  },
});
// On unmount / when replacing the source:
stop();

The trail follows elapsed time, fades after the pointer leaves, and changes character density through a bounded cursor wake. Image hover redraws a cached region; live video samples decoded frames while pointer motion can render between source frames. The standard image/GIF/live-video loops pause rendering outside the viewport and honor reduced motion. Small fonts keep real glyphs, and dense grids no longer silently disable hover.

Use your own CSS and source media for atmosphere; see the copyable media background example. A background generator is optional. GIF decoding loads only when a GIF conversion API is called. Source maps remain available in local builds and are excluded from the npm archive.

waveField now animates the supplied media rather than replacing it with a generated scene. For the previous procedural scene, download the Wave template from https://asciify.org/backgrounds.

Prepared AsciiFrame objects are treated as immutable for drawing caches. Replace the frame object after editing cells, or call clearAsciifyCaches(). Always call the returned cleanup function, including when an async mount resolves after a component has unmounted.

▶ Live Playground · npm


Overview

asciify-engine works in two stages:

  1. 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.
  2. 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-engine

Works with any modern bundler (Vite, webpack, esbuild, Rollup) and any framework — React, Vue, Svelte, Angular, Next.js, or vanilla JS.

Website hovers, included in npm

Since 1.2.0, import the enhanced interactions through asciify-engine/hover. No extra download is needed. This optional entry leaves root and /core browser bundles unchanged.

import { asciify } from 'asciify-engine/core';
import { mountHover } from 'asciify-engine/hover';

// <div id="art" style="position:relative;width:fit-content">
//   <canvas id="ascii" style="display:block"></canvas>
// </div>
const host = document.querySelector('#art');
const canvas = document.querySelector('#ascii');
const charset = ' .:-=+*#%@';
const stop = await asciify('/image.jpg', canvas, {
  fontSize: 7,
  options: { charset, charAspect: 0.58, hoverStrength: 0, animationStyle: 'none' },
});
const hover = mountHover(host, canvas, {
  effect: 'trail', strength: 0.55, radius: 0.2,
  fontSize: 7, charAspect: 0.58, charset,
});
// Switch: hover.update({ effect: 'water' });
// Redraw a still: hover.invalidate();
// On unmount: hover.destroy(); stop();

Effects: Trail, Water, Contour, Dissolve, Silk, Vortex, and none. Pass effect to mountHover, rather than passing these names to the legacy engine hoverEffect option. Keep engine hover and animation disabled to avoid stacking two renderers. Match fontSize, charAspect, charSpacing, charset, customText, and dot settings to the backing canvas.

For video/GIF, set animated: true, fps: 24 and coordinate paused with playback. For a still, optional motion: 'print' | 'current' | 'reform' adds living-image motion; fineDither: true enables stationary grain. mountStudioHover remains an alias for the existing standalone integration. The module observes reduced motion, sleeps when idle/offscreen, and uses Canvas 2D when WebGL is unavailable (simpler spatial refraction). Call destroy() on unmount; use hover.canvas for overlay layering or snapshots.

See the complete integration and lifecycle guide. A standalone ES module remains available for projects without a bundler.

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-engine

Use the skill when building ASCII images, videos, GIFs, webcam effects, scroll-scrubbed heroes, hover interactions, or performance-sensitive ASCII media.

The current hosted skill is available at asciify.org/skill, including a layered HTML/ASCII hero example. The hosted copy can receive documentation updates between npm releases.


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 to display: none — browsers skip GPU frame decoding. When given a URL string, asciifyVideo handles 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 | 7 (low-level); 10 (one-call) | 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 CSS scale, preserving existing transforms
  • width / height: CSS lengths or pixel numbers for the visible canvas
  • bleed: 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 lighting
  • 60–80 — broader key, wrinkled fabric or uneven lighting
  • 80–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,
  });
});

Downloadable Background Templates

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.

// Download a template from https://asciify.org/backgrounds
import { asciiBackground } from './backgrounds.js';

const { destroy } = 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
destroy();

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 (downloaded template) | (selector, options) | { destroy() } | | 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!

Hero finish

Version 1.3.0 adds text-shaped charcoal cutouts (textMask), peripheral scanline/fringe control (edgeEffect), fixed accent ink, and velocity-driven Trail source flow. These additions ship in npm 1.3.0. Upgrade older applications before using them. See the complete API, layering example, lifecycle, and fallback limits.

Studio composition tools

The asciify-engine/studio entry adds a reusable composition pipeline: alternate render styles, palette dithering, crop and masks, source/blurred/gradient backdrops, combined optical effects, saved looks, text fonts, and PNG/JPG/GIF/MP4/WebM export. Existing ASCII defaults and /core and /hover imports stay unchanged. Fonts and video encoders load on demand.

import { mountStudio } from 'asciify-engine/studio';

const studio = await mountStudio(canvas, '/your-video.mp4', {
  settings: {
    style: 'ascii',
    backdrop: { mode: 'blurred', opacity: 0.4 },
  },
});
studio.update({ hover: { effect: 'water', radius: 0.35 } });
// On unmount:
studio.destroy();

See the Studio API guide for complete settings, import/export, media ownership, browser requirements, and performance budgets.

2.0.1 size and export fixes

studioGrid exposes the effective grid and minimum cell size for a bounded preview. Dither uses dither.scale; other styles use cellSize. Pass time to exportStudio for a PNG/JPEG of the current frame. Animation exports continue to start at zero. See the Studio guide for the shared preview/export density contract.

Still-image motion — 3.0

Use motion: { type: 'current', speed: 1 } with /studio, motion: 'current' with /hover, or animationStyle: 'current' with core. Choose one motion owner. The curated set is Off (none), Caustics (caustics, fixed-grid light), Slow Current (current, local flow), and Reveal & Reform (reform, staggered dissolve). All loop every 12 / speed seconds and preserve your chosen colors. Breathe, Fire, Rainbow and other old ambient motion IDs are retired. Old Studio presets restore with motion Off; update application-owned core options explicitly. Hover effects and optical finishes are independent. See the 3.0 migration guide.

4.0: optional background templates

Procedural backgrounds no longer ship in the npm package or root exports. Download only the template you want from the background gallery; its copied code and lifecycle belong to your application. Existing 3.x applications can remain pinned while migrating. /backgrounds, asciiBackground, BACKGROUND_TYPES, and procedural render*Background exports are removed; text/media background APIs remain. The default import and /core now expose the same media-first API. CHARACTER_SETS is the lightweight curated catalog shared by the hero and editor.

4.1.0: current catalogs only

This release removes CHARSETS, ART_STYLE_PRESETS, CHARSET_SEQUENCES, LIVING_STYLE_PRESETS, and their key types. Although numbered 4.1.0 at the maintainer's request, these removals are breaking changes. Update imports before upgrading. Removed core artStyle shortcuts throw with migration guidance.

Use STUDIO_STYLES from asciify-engine/studio for the 15 current render styles: ASCII, Braille, Dots, Lines, Blocks, Dither, Cross, Diagonal, Diamond, Mixed, Pixel, Mosaic, Lego, Voxel and Disco. Set settings.style with mountStudio / createStudioRenderer; these styles are actual rendering methods, not character ramps. Core media APIs remain ASCII-first.

Use CHARACTER_SETS.standard.chars (or another curated entry) from root or /core for an ASCII ramp; custom options.charset and charsetFrames arrays remain supported. Use explicit options.colorMode, renderMode, and hover/motion settings instead of the removed combined presets. No legacy catalog has been moved to a hidden compatibility bundle.