asciify-engine 1.0.40 → 1.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,201 +4,268 @@
4
4
  <a href="https://www.npmjs.com/package/asciify-engine"><img src="https://img.shields.io/npm/v/asciify-engine?color=d4ff00&labelColor=0a0a0a&style=flat-square" alt="npm version" /></a>
5
5
  <a href="https://www.npmjs.com/package/asciify-engine"><img src="https://img.shields.io/npm/dm/asciify-engine?color=d4ff00&labelColor=0a0a0a&style=flat-square" alt="downloads" /></a>
6
6
  <a href="https://github.com/ayangabryl/asciify-engine/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-d4ff00?labelColor=0a0a0a&style=flat-square" alt="MIT license" /></a>
7
- <a href="https://www.buymeacoffee.com/asciify"><img src="https://img.shields.io/badge/buy_me_a_coffee-☕-d4ff00?labelColor=0a0a0a&style=flat-square" alt="Buy Me A Coffee" /></a>
7
+ <a href="https://www.buymeacoffee.com/asciify"><img src="https://img.shields.io/badge/buy_me_a_coffee-%E2%98%95-d4ff00?labelColor=0a0a0a&style=flat-square" alt="Buy Me A Coffee" /></a>
8
8
  </p>
9
9
 
10
- Turn any image, video, or GIF into ASCII art on an HTML canvas — with live animated backgrounds, hover effects, and zero dependencies.
10
+ 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.
11
11
 
12
- **[ Try the live playground](https://asciify.org) · [npm](https://www.npmjs.com/package/asciify-engine)**
12
+ **[&#9654; Live Playground](https://asciify.org) &middot; [npm](https://www.npmjs.com/package/asciify-engine)**
13
13
 
14
14
  ---
15
15
 
16
- ## Install
16
+ ## Overview
17
+
18
+ asciify-engine works in two stages:
19
+
20
+ 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.
21
+ 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.
22
+
23
+ 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.
24
+
25
+ ---
26
+
27
+ ## Installation
17
28
 
18
29
  ```bash
19
30
  npm install asciify-engine
20
31
  ```
21
32
 
33
+ Works with any modern bundler (Vite, webpack, esbuild, Rollup) and any framework — React, Vue, Svelte, Angular, Next.js, or vanilla JS.
34
+
22
35
  ---
23
36
 
24
- ## The 30-second version
37
+ ## Converting Media to ASCII
38
+
39
+ ### Images
40
+
41
+ `imageToAsciiFrame` accepts any `HTMLImageElement`, `HTMLVideoElement`, or `HTMLCanvasElement` and returns a single ASCII frame.
25
42
 
26
43
  ```ts
27
44
  import { imageToAsciiFrame, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
28
45
 
29
46
  const img = new Image();
30
- img.src = 'your-image.jpg';
47
+ img.crossOrigin = 'anonymous';
48
+ img.src = 'photo.jpg';
49
+
31
50
  img.onload = () => {
32
51
  const canvas = document.getElementById('ascii') as HTMLCanvasElement;
33
- const { frame } = imageToAsciiFrame(img, DEFAULT_OPTIONS, canvas.width, canvas.height);
34
- renderFrameToCanvas(canvas.getContext('2d')!, frame, DEFAULT_OPTIONS, canvas.width, canvas.height);
52
+ const ctx = canvas.getContext('2d')!;
53
+ const opts = { ...DEFAULT_OPTIONS, fontSize: 10, colorMode: 'fullcolor' as const };
54
+
55
+ const { frame } = imageToAsciiFrame(img, opts, canvas.width, canvas.height);
56
+ renderFrameToCanvas(ctx, frame, opts, canvas.width, canvas.height);
35
57
  };
36
58
  ```
37
59
 
38
- That's it — one function to convert, one to draw. Everything else is optional.
60
+ ### Animated GIFs
39
61
 
40
- ---
62
+ `gifToAsciiFrames` parses a GIF `ArrayBuffer` and returns one `AsciiFrame` per GIF frame, preserving the original frame rate.
41
63
 
42
- ## Animated backgrounds
64
+ ```ts
65
+ import { gifToAsciiFrames, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
43
66
 
44
- Add a living ASCII animation to any element with one line:
67
+ const buffer = await fetch('animation.gif').then(r => r.arrayBuffer());
68
+ const canvas = document.getElementById('ascii') as HTMLCanvasElement;
69
+ const opts = { ...DEFAULT_OPTIONS, fontSize: 8 };
45
70
 
46
- ```ts
47
- import { asciiBackground } from 'asciify-engine';
71
+ const { frames, fps } = await gifToAsciiFrames(buffer, opts, canvas.width, canvas.height);
48
72
 
49
- asciiBackground('#hero', { type: 'rain' });
73
+ let frameIndex = 0;
74
+ setInterval(() => {
75
+ renderFrameToCanvas(canvas.getContext('2d')!, frames[frameIndex], opts, canvas.width, canvas.height);
76
+ frameIndex = (frameIndex + 1) % frames.length;
77
+ }, 1000 / fps);
50
78
  ```
51
79
 
52
- Returns a cleanup function when you're done:
80
+ ### Video live (recommended)
81
+
82
+ `asciifyLiveVideo` streams a video as ASCII art in real time. Pass a URL and a canvas — it handles everything else.
83
+
84
+ > ⚠️ Never set the `<video>` element to `display: none`. Browsers skip GPU frame decoding for hidden elements — you get a blank canvas. `asciifyLiveVideo` handles this automatically.
53
85
 
54
86
  ```ts
55
- const stop = asciiBackground('#hero', { type: 'aurora', colorScheme: 'auto' });
56
- // later…
87
+ import { asciifyLiveVideo } from 'asciify-engine';
88
+
89
+ const canvas = document.getElementById('ascii') as HTMLCanvasElement;
90
+ const stop = await asciifyLiveVideo('/clip.mp4', canvas);
91
+
92
+ // With options:
93
+ const stop = await asciifyLiveVideo('/clip.mp4', canvas, {
94
+ fontSize: 6,
95
+ artStyle: 'matrix',
96
+ });
97
+
98
+ // Clean up:
57
99
  stop();
58
100
  ```
59
101
 
60
- **Available types:** `wave` · `rain` · `stars` · `pulse` · `noise` · `grid` · `aurora` · `silk` · `void` · `morph`
102
+ ### Video pre-extracted frames
61
103
 
62
- **Common options:**
104
+ `asciifyVideo` seeks through the clip frame by frame and returns a frame sequence. Good for short clips where you want frame-perfect control, but requires up-front processing time.
63
105
 
64
106
  ```ts
65
- asciiBackground('#el', {
66
- type: 'stars',
67
- colorScheme: 'auto', // 'auto' | 'light' | 'dark' — 'auto' follows OS theme
68
- fontSize: 13, // character size in px
69
- speed: 1.2, // animation speed multiplier
70
- density: 0.6, // how many cells are active (0–1)
71
- accentColor: '#d4ff00' // highlight colour
72
- });
107
+ import { asciifyVideo } from 'asciify-engine';
108
+
109
+ const canvas = document.getElementById('ascii') as HTMLCanvasElement;
110
+ const stop = await asciifyVideo('/clip.mp4', canvas, { fontSize: 8 });
111
+ stop();
73
112
  ```
74
113
 
75
114
  ---
76
115
 
77
- ## More recipes
116
+ ## Rendering Options
78
117
 
79
- ### React
118
+ All conversion and render functions accept an `AsciiOptions` object. Spread `DEFAULT_OPTIONS` as a base and override what you need.
80
119
 
81
- ```tsx
82
- import { useEffect, useRef } from 'react';
83
- import { imageToAsciiFrame, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
120
+ | Option | Type | Default | Description |
121
+ |---|---|---|---|
122
+ | `fontSize` | `number` | `10` | Character cell size in pixels. Smaller values increase density and detail. |
123
+ | `colorMode` | `'grayscale' \| 'fullcolor' \| 'matrix' \| 'accent'` | `'grayscale'` | Determines how pixel color is mapped to character color. |
124
+ | `charset` | `string` | Standard ramp | Characters ordered from dense to sparse, representing brightness levels. |
125
+ | `brightness` | `number` | `0` | Brightness adjustment from `-1` (darker) to `1` (lighter). |
126
+ | `contrast` | `number` | `1` | Contrast multiplier applied before character mapping. |
127
+ | `invert` | `boolean` | `false` | Inverts the luminance mapping — light areas become dense, dark areas sparse. |
128
+ | `renderMode` | `'ascii' \| 'dots'` | `'ascii'` | Render as text characters or circular dot particles. |
129
+ | `hoverEffect` | `string` | `'none'` | Interactive effect driven by cursor position. See hover effects below. |
130
+ | `hoverStrength` | `number` | `0.8` | Effect intensity (0–1). |
131
+ | `hoverRadius` | `number` | `0.3` | Effect radius relative to canvas size (0–1). |
84
132
 
85
- export function AsciiImage({ src }: { src: string }) {
86
- const ref = useRef<HTMLCanvasElement>(null);
133
+ ### Color Modes
87
134
 
88
- useEffect(() => {
89
- const img = new Image();
90
- img.crossOrigin = 'anonymous';
91
- img.src = src;
92
- img.onload = () => {
93
- const canvas = ref.current!;
94
- const opts = { ...DEFAULT_OPTIONS, fontSize: 10, colorMode: 'fullcolor' as const };
95
- const { frame } = imageToAsciiFrame(img, opts, canvas.width, canvas.height);
96
- renderFrameToCanvas(canvas.getContext('2d')!, frame, opts, canvas.width, canvas.height);
97
- };
98
- }, [src]);
135
+ | Mode | Description |
136
+ |---|---|
137
+ | `grayscale` | Classic monochrome ASCII. Character brightness maps to source luminance. |
138
+ | `fullcolor` | Each character inherits the original pixel color from the source. |
139
+ | `matrix` | Monochrome green — inspired by classic terminal aesthetics. |
140
+ | `accent` | Single accent color applied uniformly across all characters. |
99
141
 
100
- return <canvas ref={ref} width={800} height={600} />;
101
- }
102
- ```
142
+ ### Hover Effects
103
143
 
104
- ### Animated GIF
144
+ Interactive effects that respond to cursor movement. Pass the effect name to `hoverEffect` and supply the cursor position to `renderFrameToCanvas` at render time.
105
145
 
106
- ```ts
107
- import { gifToAsciiFrames, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
146
+ Available effects: `spotlight` · `flashlight` · `magnifier` · `force-field` · `neon` · `fire` · `ice` · `gravity` · `shatter` · `ghost`
108
147
 
109
- const buf = await fetch('animation.gif').then(r => r.arrayBuffer());
110
- const canvas = document.getElementById('ascii') as HTMLCanvasElement;
111
- const opts = { ...DEFAULT_OPTIONS, fontSize: 8 };
148
+ ```ts
149
+ canvas.addEventListener('mousemove', (e) => {
150
+ const rect = canvas.getBoundingClientRect();
151
+ renderFrameToCanvas(ctx, frame, opts, canvas.width, canvas.height, Date.now() / 1000, {
152
+ x: e.clientX - rect.left,
153
+ y: e.clientY - rect.top,
154
+ });
155
+ });
156
+ ```
112
157
 
113
- const { frames, fps } = await gifToAsciiFrames(buf, opts, canvas.width, canvas.height);
158
+ ---
114
159
 
115
- let i = 0;
116
- setInterval(() => {
117
- renderFrameToCanvas(canvas.getContext('2d')!, frames[i], opts, canvas.width, canvas.height);
118
- i = (i + 1) % frames.length;
119
- }, 1000 / fps);
120
- ```
160
+ ## Animated Backgrounds
121
161
 
122
- ### Video
162
+ `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.
123
163
 
124
164
  ```ts
125
- import { videoToAsciiFrames, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
165
+ import { asciiBackground } from 'asciify-engine';
126
166
 
127
- const video = document.createElement('video');
128
- video.src = '/clip.mp4';
129
- await new Promise(r => (video.onloadeddata = r));
167
+ const stop = asciiBackground('#hero', {
168
+ type: 'rain',
169
+ colorScheme: 'auto', // follows OS dark/light mode
170
+ speed: 1.0,
171
+ density: 0.55,
172
+ accentColor: '#d4ff00',
173
+ });
130
174
 
131
- const canvas = document.getElementById('ascii') as HTMLCanvasElement;
132
- const opts = { ...DEFAULT_OPTIONS, fontSize: 8 };
175
+ // Stop and clean up when no longer needed
176
+ stop();
177
+ ```
133
178
 
134
- const { frames, fps } = await videoToAsciiFrames(video, opts, canvas.width, canvas.height);
179
+ ### Available Background Types
135
180
 
136
- let i = 0;
137
- setInterval(() => {
138
- renderFrameToCanvas(canvas.getContext('2d')!, frames[i], opts, canvas.width, canvas.height);
139
- i = (i + 1) % frames.length;
140
- }, 1000 / fps);
141
- ```
181
+ | Type | Description |
182
+ |---|---|
183
+ | `wave` | Flowing sine-wave field with layered noise turbulence |
184
+ | `rain` | Vertical column rain with a glowing leading character and fading trail |
185
+ | `stars` | Parallax star field that reacts to cursor position |
186
+ | `pulse` | Concentric ripple bursts emanating from the cursor |
187
+ | `noise` | Smooth value-noise field with organic, fluid motion |
188
+ | `grid` | Geometric grid that warps and brightens near the cursor |
189
+ | `aurora` | Sweeping borealis-style color bands drifting across the field |
190
+ | `silk` | Fluid swirl simulation following cursor movement |
191
+ | `void` | Gravitational singularity — characters spiral inward toward the cursor |
192
+ | `morph` | Characters morph between shapes over time, driven by noise |
193
+
194
+ ### Background Options
195
+
196
+ | Option | Type | Default | Description |
197
+ |---|---|---|---|
198
+ | `type` | `string` | `'wave'` | Which background renderer to use. |
199
+ | `colorScheme` | `'auto' \| 'light' \| 'dark'` | `'dark'` | `'auto'` reacts to OS theme changes in real time. |
200
+ | `fontSize` | `number` | `13` | Character size in pixels. |
201
+ | `speed` | `number` | `1` | Global animation speed multiplier. |
202
+ | `density` | `number` | `0.55` | Fraction of grid cells that are active (0–1). |
203
+ | `accentColor` | `string` | varies | Highlight or leading-character color (any CSS color string). |
204
+ | `color` | `string` | — | Override the body character color. |
142
205
 
143
206
  ---
144
207
 
145
- ## Tweaking the output
208
+ ## React Integration
146
209
 
147
- Pass options to any function to change the look:
210
+ ```tsx
211
+ import { useEffect, useRef } from 'react';
212
+ import { imageToAsciiFrame, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
148
213
 
149
- ```ts
150
- const opts = {
151
- ...DEFAULT_OPTIONS,
152
- fontSize: 8, // smaller = more detail
153
- colorMode: 'matrix', // 'grayscale' | 'fullcolor' | 'matrix' | 'accent'
154
- charset: '@#S%?*+;:,. ', // custom brightness ramp (dense → light)
155
- brightness: 0.1, // -1 to 1
156
- contrast: 1.2,
157
- invert: false,
158
- hoverEffect: 'spotlight', // interactive effect on cursor move
159
- };
160
- ```
214
+ export function AsciiImage({ src }: { src: string }) {
215
+ const canvasRef = useRef<HTMLCanvasElement>(null);
161
216
 
162
- **Color modes at a glance:**
217
+ useEffect(() => {
218
+ const canvas = canvasRef.current;
219
+ if (!canvas) return;
163
220
 
164
- | Mode | What it does |
165
- |---|---|
166
- | `grayscale` | Classic monochrome ASCII |
167
- | `fullcolor` | Preserves original pixel colours |
168
- | `matrix` | Everything in green, like the film |
169
- | `accent` | Single highlight colour |
221
+ const img = new Image();
222
+ img.crossOrigin = 'anonymous';
223
+ img.src = src;
224
+ img.onload = () => {
225
+ const opts = { ...DEFAULT_OPTIONS, fontSize: 10, colorMode: 'fullcolor' as const };
226
+ const { frame } = imageToAsciiFrame(img, opts, canvas.width, canvas.height);
227
+ renderFrameToCanvas(canvas.getContext('2d')!, frame, opts, canvas.width, canvas.height);
228
+ };
229
+ }, [src]);
170
230
 
171
- **Hover effects:** `spotlight` · `flashlight` · `magnifier` · `force-field` · `neon` · `fire` · `ice` · `gravity` · `shatter` · `ghost`
231
+ return <canvas ref={canvasRef} width={800} height={600} />;
232
+ }
233
+ ```
172
234
 
173
235
  ---
174
236
 
175
- ## Embed generation
237
+ ## Embed Generation
176
238
 
177
- Export your ASCII art as a self-contained HTML file:
239
+ Generate self-contained HTML that can be hosted anywhere or dropped directly into a page no runtime dependency required.
178
240
 
179
241
  ```ts
180
242
  import { generateEmbedCode, generateAnimatedEmbedCode } from 'asciify-engine';
181
243
 
182
- // Static image
183
- const html = generateEmbedCode(frame, options);
244
+ // Static — produces a single-file HTML with the ASCII art baked in
245
+ const staticHtml = generateEmbedCode(frame, options);
184
246
 
185
- // Animated
186
- const html = generateAnimatedEmbedCode(frames, options, fps);
247
+ // Animated — produces a self-running HTML animation
248
+ const animatedHtml = generateAnimatedEmbedCode(frames, options, fps);
187
249
  ```
188
250
 
189
251
  ---
190
252
 
191
- ## API summary
192
-
193
- | Function | Description |
194
- |---|---|
195
- | `imageToAsciiFrame(img, opts, w, h)` | Convert an image/video/canvas element to an ASCII frame |
196
- | `renderFrameToCanvas(ctx, frame, opts, w, h, time?, pos?)` | Draw an ASCII frame onto a canvas 2D context |
197
- | `gifToAsciiFrames(buffer, opts, w, h)` | Parse an animated GIF into ASCII frames |
198
- | `videoToAsciiFrames(video, opts, w, h, fps?, maxSec?)` | Extract video frames and convert them to ASCII |
199
- | `asciiBackground(selector, opts)` | Mount a live animated ASCII background |
200
- | `generateEmbedCode(frame, opts)` | Self-contained static HTML snippet |
201
- | `generateAnimatedEmbedCode(frames, opts, fps)` | Self-contained animated HTML snippet |
253
+ ## API Reference
254
+
255
+ | Function | Signature | Returns |
256
+ |---|---|---|
257
+ | `asciify` | `(source, canvas, options?)` | `Promise<void>` |
258
+ | `asciifyLiveVideo` | `(source, canvas, options?)` | `Promise<() => void>` |
259
+ | `asciifyVideo` | `(source, canvas, options?)` | `Promise<() => void>` |
260
+ | `asciifyGif` | `(source, canvas, options?)` | `Promise<() => void>` |
261
+ | `asciifyWebcam` | `(canvas, options?)` | `Promise<() => void>` |
262
+ | `asciiBackground` | `(selector, options)` | `() => void` |
263
+ | `imageToAsciiFrame` | `(source, options, w?, h?)` | `{ frame, cols, rows }` |
264
+ | `renderFrameToCanvas` | `(ctx, frame, options, w, h, time?, hoverPos?)` | `void` |
265
+ | `gifToAsciiFrames` | `(buffer, options, w, h, onProgress?)` | `Promise<{ frames, cols, rows, fps }>` |
266
+ | `videoToAsciiFrames` | `(video, options, w, h, fps?, maxSec?, onProgress?)` | `Promise<{ frames, cols, rows, fps }>` |
267
+ | `generateEmbedCode` | `(frame, options)` | `string` |
268
+ | `generateAnimatedEmbedCode` | `(frames, options, fps)` | `string` |
202
269
 
203
270
  ---
204
271
 
package/dist/index.cjs CHANGED
@@ -1077,6 +1077,62 @@ async function asciifyVideo(source, canvas, { fontSize = 10, artStyle = "classic
1077
1077
  cancelAnimationFrame(animId);
1078
1078
  };
1079
1079
  }
1080
+ async function asciifyLiveVideo(source, canvas, { fontSize = 10, artStyle = "classic", options = {} } = {}) {
1081
+ let video;
1082
+ let ownedVideo = false;
1083
+ if (typeof source === "string") {
1084
+ video = document.createElement("video");
1085
+ video.src = source;
1086
+ video.muted = true;
1087
+ video.loop = true;
1088
+ video.playsInline = true;
1089
+ video.setAttribute("playsinline", "");
1090
+ Object.assign(video.style, {
1091
+ position: "fixed",
1092
+ top: "0",
1093
+ left: "0",
1094
+ width: "1px",
1095
+ height: "1px",
1096
+ opacity: "0",
1097
+ pointerEvents: "none",
1098
+ zIndex: "-1"
1099
+ });
1100
+ document.body.appendChild(video);
1101
+ ownedVideo = true;
1102
+ await new Promise((resolve, reject) => {
1103
+ video.onloadedmetadata = () => resolve();
1104
+ video.onerror = () => reject(new Error(`asciifyLiveVideo: failed to load "${source}"`));
1105
+ });
1106
+ video.play().catch(() => {
1107
+ });
1108
+ } else {
1109
+ video = source;
1110
+ if (video.paused) video.play().catch(() => {
1111
+ });
1112
+ }
1113
+ const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[artStyle], ...options, fontSize };
1114
+ const ctx = canvas.getContext("2d");
1115
+ if (!ctx) throw new Error("asciifyLiveVideo: could not get 2d context from canvas.");
1116
+ let cancelled = false;
1117
+ let animId;
1118
+ const tick = () => {
1119
+ if (cancelled) return;
1120
+ animId = requestAnimationFrame(tick);
1121
+ if (video.readyState < 2 || canvas.width === 0 || canvas.height === 0) return;
1122
+ const { frame } = imageToAsciiFrame(video, merged, canvas.width, canvas.height);
1123
+ if (frame.length > 0) renderFrameToCanvas(ctx, frame, merged, canvas.width, canvas.height, 0, null);
1124
+ };
1125
+ animId = requestAnimationFrame(tick);
1126
+ return () => {
1127
+ cancelled = true;
1128
+ cancelAnimationFrame(animId);
1129
+ if (ownedVideo) {
1130
+ video.pause();
1131
+ video.src = "";
1132
+ document.body.removeChild(video);
1133
+ }
1134
+ };
1135
+ }
1080
1136
 
1081
1137
  // src/backgrounds/rain.ts
1082
1138
  function renderRainBackground(ctx, width, height, time, options = {}) {
@@ -2468,6 +2524,7 @@ exports.asciiText = asciiText;
2468
2524
  exports.asciiTextAnsi = asciiTextAnsi;
2469
2525
  exports.asciify = asciify;
2470
2526
  exports.asciifyGif = asciifyGif;
2527
+ exports.asciifyLiveVideo = asciifyLiveVideo;
2471
2528
  exports.asciifyVideo = asciifyVideo;
2472
2529
  exports.asciifyWebcam = asciifyWebcam;
2473
2530
  exports.buildTextFrame = buildTextFrame;