asciify-engine 1.0.34 → 1.0.35

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
@@ -9,6 +9,385 @@
9
9
 
10
10
  > Convert images, videos, and GIFs into ASCII art on HTML canvas. 13 art styles, 4 color modes, 10 animated backgrounds, interactive hover effects — zero dependencies.
11
11
 
12
+ **[▶ Live Playground](https://asciify.org) · [npm](https://www.npmjs.com/package/asciify-engine) · [Support the project ☕](https://www.buymeacoffee.com/asciify)**
13
+
14
+ ## Features
15
+
16
+ - **Media → ASCII** — images, videos, and animated GIFs
17
+ - **13 art styles** — Standard, Blocks, Circles, Braille, Katakana, Dense, and more
18
+ - **4 color modes** — Grayscale, Full Color, Matrix, Accent
19
+ - **10 animated backgrounds** — Wave, Rain, Stars, Pulse, Noise, Grid, Aurora, Silk, Void, Morph
20
+ - **Interactive hover effects** — Spotlight, Flashlight, Magnifier, Force Field, Neon, Fire, Ice, Gravity, Shatter, Ghost
21
+ - **Light & dark mode** — via `colorScheme: 'auto'` or explicit `light` / `dark`
22
+ - **Embed generation** — self-contained HTML output (static or animated)
23
+ - **Zero dependencies** — works with React, Vue, Angular, Svelte, Next.js, or vanilla JS
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install asciify-engine
29
+ ```
30
+
31
+ > **90% of use cases need only two functions:** `asciify()` to convert images/video/GIFs to ASCII art, and `asciiBackground()` for animated backgrounds. Start there.
32
+
33
+ ## Quick Start
34
+
35
+ ### Image → ASCII
36
+
37
+ ```ts
38
+ import { asciify } from 'asciify-engine';
39
+
40
+ const canvas = document.querySelector('canvas')!;
41
+
42
+ // Minimal — just a URL and a canvas, no img.onload boilerplate needed
43
+ await asciify('https://example.com/photo.jpg', canvas);
44
+
45
+ // With an art style preset
46
+ await asciify('photo.jpg', canvas, { artStyle: 'letters' });
47
+
48
+ // Full control
49
+ await asciify('photo.jpg', canvas, {
50
+ fontSize: 8,
51
+ artStyle: 'art',
52
+ options: {
53
+ colorMode: 'fullcolor',
54
+ invert: true,
55
+ hoverEffect: 'spotlight',
56
+ hoverStrength: 0.5,
57
+ },
58
+ });
59
+ ```
60
+
61
+ ### GIF Animation
62
+
63
+ ```ts
64
+ import { asciifyGif } from 'asciify-engine';
65
+
66
+ // Fetches, converts, and starts the animation loop — returns a stop() function
67
+ const stop = await asciifyGif('animation.gif', canvas);
68
+
69
+ // Clean up the animation loop when done
70
+ stop();
71
+ ```
72
+
73
+ ### Video
74
+
75
+ ```ts
76
+ import { asciifyVideo } from 'asciify-engine';
77
+
78
+ // Accepts a URL string or an existing HTMLVideoElement
79
+ const stop = await asciifyVideo('/my-video.mp4', canvas, { artStyle: 'terminal' });
80
+
81
+ // Cancel when unmounting / navigating away
82
+ stop();
83
+ ```
84
+
85
+ ### React
86
+
87
+ ```tsx
88
+ import { useEffect, useRef } from 'react';
89
+ import { asciify } from 'asciify-engine';
90
+
91
+ export function AsciiImage({ src }: { src: string }) {
92
+ const ref = useRef<HTMLCanvasElement>(null);
93
+
94
+ useEffect(() => {
95
+ if (ref.current) asciify(src, ref.current, { artStyle: 'art' });
96
+ }, [src]);
97
+
98
+ return <canvas ref={ref} width={800} height={600} />;
99
+ }
100
+ ```
101
+
102
+ ### Animated GIF or video in React
103
+
104
+ ```tsx
105
+ import { useEffect, useRef } from 'react';
106
+ import { asciifyGif } from 'asciify-engine';
107
+
108
+ export function AsciiGif({ src }: { src: string }) {
109
+ const ref = useRef<HTMLCanvasElement>(null);
110
+
111
+ useEffect(() => {
112
+ let stop: (() => void) | undefined;
113
+ asciifyGif(src, ref.current!).then(fn => { stop = fn; });
114
+ return () => stop?.(); // cancels the rAF loop on unmount
115
+ }, [src]);
116
+
117
+ return <canvas ref={ref} width={800} height={600} />;
118
+ }
119
+ ```
120
+
121
+ ### Vue
122
+
123
+ ```vue
124
+ <template>
125
+ <canvas ref="canvasRef" width="800" height="600" />
126
+ </template>
127
+
128
+ <script setup lang="ts">
129
+ import { useTemplateRef, onMounted } from 'vue';
130
+ import { asciify } from 'asciify-engine';
131
+
132
+ const props = defineProps<{ src: string; artStyle?: string }>();
133
+ const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');
134
+
135
+ onMounted(() => asciify(props.src, canvasRef.value!, { artStyle: props.artStyle as any }));
136
+ </script>
137
+ ```
138
+
139
+ ### Angular
140
+
141
+ ```typescript
142
+ import { Component, ElementRef, ViewChild, AfterViewInit, Input } from '@angular/core';
143
+ import { asciify } from 'asciify-engine';
144
+
145
+ @Component({
146
+ selector: 'app-ascii',
147
+ template: `<canvas #canvas width="800" height="600"></canvas>`,
148
+ })
149
+ export class AsciiComponent implements AfterViewInit {
150
+ @ViewChild('canvas') canvasRef!: ElementRef<HTMLCanvasElement>;
151
+ @Input() src = '';
152
+ @Input() artStyle = 'classic';
153
+
154
+ ngAfterViewInit() {
155
+ asciify(this.src, this.canvasRef.nativeElement, { artStyle: this.artStyle as any });
156
+ }
157
+ }
158
+ ```
159
+
160
+ ## Animated Backgrounds
161
+
162
+ Drop a live ASCII animation into any element with one call.
163
+
164
+ ```ts
165
+ import { asciiBackground } from 'asciify-engine';
166
+
167
+ // Attach to any selector — animates the element's background
168
+ asciiBackground('#hero', { type: 'rain' });
169
+
170
+ // Follows OS dark/light mode automatically
171
+ asciiBackground('#hero', { type: 'aurora', colorScheme: 'auto' });
172
+
173
+ // Force light mode (dark characters on light background)
174
+ asciiBackground('#hero', { type: 'wave', colorScheme: 'light' });
175
+
176
+ // Stop / clean up
177
+ const stop = asciiBackground('#hero', { type: 'stars' });
178
+ stop();
179
+ ```
180
+
181
+ ### Background Types
182
+
183
+ | Type | Description |
184
+ |---|---|
185
+ | `wave` | Flowing sine-wave field with noise turbulence |
186
+ | `rain` | Matrix-style vertical column rain with glowing head and fading tail |
187
+ | `stars` | Parallax star field that reacts to cursor position |
188
+ | `pulse` | Concentric ripple bursts that emanate from the cursor |
189
+ | `noise` | Smooth value-noise field with organic flow |
190
+ | `grid` | Geometric grid that warps and glows at cursor proximity |
191
+ | `aurora` | Sweeping borealis-style colour bands |
192
+ | `silk` | Silky fluid swirls following the cursor |
193
+ | `void` | Gravitational singularity — characters spiral inward toward cursor |
194
+ | `morph` | Characters morph between shapes driven by noise |
195
+
196
+ ### `asciiBackground` Options
197
+
198
+ | Option | Type | Default | Description |
199
+ |---|---|---|---|
200
+ | `type` | `string` | `'wave'` | Which background renderer to use |
201
+ | `colorScheme` | `'auto' \| 'light' \| 'dark'` | `'dark'` | `'auto'` follows OS theme live; `'light'` = dark chars on light bg |
202
+ | `fontSize` | `number` | `13` | Character size in px |
203
+ | `accentColor` | `string` | varies | Head/highlight colour (CSS colour string) |
204
+ | `color` | `string` | — | Override body character colour |
205
+ | `speed` | `number` | `1` | Global animation speed multiplier |
206
+ | `density` | `number` | `0.55` | Fraction of cells active (0–1) |
207
+ | `lightMode` | `boolean` | `false` | Dark characters on light background |
208
+
209
+ Each background type also accepts its own specific options — see the individual type exports (e.g. `RainBackgroundOptions`, `WaveBackgroundOptions`, etc.) for the full list.
210
+
211
+ ### Low-level background renderers
212
+
213
+ All renderers are also exported individually for direct canvas use:
214
+
215
+ ```ts
216
+ import { renderRainBackground } from 'asciify-engine';
217
+
218
+ const canvas = document.getElementById('canvas') as HTMLCanvasElement;
219
+ const ctx = canvas.getContext('2d')!;
220
+ let t = 0;
221
+
222
+ function tick() {
223
+ renderRainBackground(ctx, canvas.width, canvas.height, t, {
224
+ speed: 1.2,
225
+ density: 0.6,
226
+ accentColor: '#00ffcc',
227
+ lightMode: false,
228
+ });
229
+ t += 0.016;
230
+ requestAnimationFrame(tick);
231
+ }
232
+ tick();
233
+ ```
234
+
235
+ ## Low-level API
236
+
237
+ For cases where the one-call API is too limiting — direct frame manipulation, custom render loops, progress callbacks, etc.
238
+
239
+ ### GIF (low-level)
240
+
241
+ ```ts
242
+ import { gifToAsciiFrames, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
243
+
244
+ const buffer = await fetch('animation.gif').then(r => r.arrayBuffer());
245
+ const options = { ...DEFAULT_OPTIONS, fontSize: 8 };
246
+ const { frames, fps } = await gifToAsciiFrames(buffer, options, canvas.width, canvas.height);
247
+
248
+ const ctx = canvas.getContext('2d')!;
249
+ let i = 0;
250
+ let last = performance.now();
251
+ const interval = 1000 / fps;
252
+
253
+ let animId: number;
254
+ const tick = (now: number) => {
255
+ if (now - last >= interval) {
256
+ renderFrameToCanvas(ctx, frames[i], options, canvas.width, canvas.height);
257
+ i = (i + 1) % frames.length;
258
+ last = now;
259
+ }
260
+ animId = requestAnimationFrame(tick);
261
+ };
262
+ animId = requestAnimationFrame(tick);
263
+
264
+ // Clean up → cancelAnimationFrame(animId);
265
+ ```
266
+
267
+ ### Video (low-level)
268
+
269
+ ```ts
270
+ import { videoToAsciiFrames, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
271
+
272
+ const video = document.createElement('video');
273
+ video.crossOrigin = 'anonymous';
274
+ video.src = '/my-video.mp4';
275
+ // Guard against cached video where onloadeddata already fired
276
+ if (video.readyState < 2) {
277
+ await new Promise<void>(r => { video.onloadeddata = () => r(); });
278
+ }
279
+
280
+ const options = { ...DEFAULT_OPTIONS, fontSize: 8 };
281
+ const { frames, fps } = await videoToAsciiFrames(video, options, canvas.width, canvas.height, 12, 10);
282
+
283
+ const ctx = canvas.getContext('2d')!;
284
+ let i = 0;
285
+ let last = performance.now();
286
+ const interval = 1000 / fps;
287
+
288
+ let animId: number;
289
+ const tick = (now: number) => {
290
+ if (now - last >= interval) {
291
+ renderFrameToCanvas(ctx, frames[i], options, canvas.width, canvas.height);
292
+ i = (i + 1) % frames.length;
293
+ last = now;
294
+ }
295
+ animId = requestAnimationFrame(tick);
296
+ };
297
+ animId = requestAnimationFrame(tick);
298
+
299
+ // Clean up → cancelAnimationFrame(animId);
300
+ ```
301
+
302
+ ### Vanilla JS (low-level image)
303
+
304
+ ```html
305
+ <canvas id="ascii" width="800" height="600"></canvas>
306
+ <script type="module">
307
+ import { imageToAsciiFrame, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
308
+
309
+ const canvas = document.getElementById('ascii');
310
+ const ctx = canvas.getContext('2d');
311
+ const options = { ...DEFAULT_OPTIONS, fontSize: 10 };
312
+
313
+ const img = new Image();
314
+ img.crossOrigin = 'anonymous';
315
+ img.src = 'https://picsum.photos/600/400';
316
+ img.onload = () => {
317
+ const { frame } = imageToAsciiFrame(img, options, canvas.width, canvas.height);
318
+ renderFrameToCanvas(ctx, frame, options, canvas.width, canvas.height);
319
+ };
320
+ </script>
321
+ ```
322
+
323
+ ## API Reference
324
+
325
+ ### Core Functions
326
+
327
+ | Function | Returns | Description |
328
+ |---|---|---|
329
+ | `asciify(source, canvas, opts?)` | `Promise<void>` | Convert an image/video/canvas (or URL) to ASCII and render — handles loading automatically |
330
+ | `asciifyGif(source, canvas, opts?)` | `Promise<() => void>` | Fetch a GIF, convert, and start an animation loop — returns `stop()` |
331
+ | `asciifyVideo(source, canvas, opts?)` | `Promise<() => void>` | Convert a video and start an animation loop — returns `stop()` |
332
+ | `imageToAsciiFrame(source, options, w?, h?)` | `{ frame, cols, rows }` | Convert an image, video frame, or canvas to a raw ASCII frame |
333
+ | `renderFrameToCanvas(ctx, frame, options, w, h, time?, hoverPos?)` | `void` | Render a raw ASCII frame to a 2D canvas context |
334
+ | `gifToAsciiFrames(buffer, options, w, h, onProgress?)` | `{ frames, cols, rows, fps }` | Parse an animated GIF `ArrayBuffer` into ASCII frames |
335
+ | `videoToAsciiFrames(video, options, w, h, fps?, maxDuration?, onProgress?)` | `{ frames, cols, rows, fps }` | Extract and convert video frames to ASCII |
336
+ | `asciiBackground(selector, options)` | `() => void` | Mount a live animated ASCII background; returns a cleanup function |
337
+ | `generateEmbedCode(frame, options)` | `string` | Self-contained static HTML embed |
338
+ | `generateAnimatedEmbedCode(frames, options, fps)` | `string` | Self-contained animated HTML embed |
339
+
340
+ ### Simple API Options (`AsciifySimpleOptions`)
341
+
342
+ Used by `asciify()`, `asciifyGif()`, and `asciifyVideo()`:
343
+
344
+ | Option | Type | Default | Description |
345
+ |---|---|---|---|
346
+ | `fontSize` | `number` | `10` | Character cell size in px |
347
+ | `artStyle` | `ArtStyle` | `'classic'` | Art style preset — sets charset, render mode, and color mode together |
348
+ | `options` | `Partial<AsciiOptions>` | `{}` | Fine-grained overrides applied on top of the preset |
349
+
350
+ ### Key Options (`AsciiOptions`)
351
+
352
+ | Option | Type | Default | Description |
353
+ |---|---|---|---|
354
+ | `fontSize` | `number` | `10` | Character cell size in px |
355
+ | `colorMode` | `'grayscale' \| 'fullcolor' \| 'matrix' \| 'accent'` | `'grayscale'` | Color output mode |
356
+ | `renderMode` | `'ascii' \| 'dots'` | `'ascii'` | Render as characters or dot particles |
357
+ | `charset` | `string` | `' .:-=+*#%@'` | Custom character density ramp (light → dark) |
358
+ | `brightness` | `number` | `0` | Brightness adjustment (−1 → 0 → 1) |
359
+ | `contrast` | `number` | `0` | Contrast boost (0 = unchanged, positive = more contrast) |
360
+ | `invert` | `boolean` | `false` | Invert luminance mapping |
361
+ | `animationStyle` | `AnimationStyle` | `'none'` | Per-character animation driven over time |
362
+ | `hoverEffect` | `HoverEffect` | `'spotlight'` | Cursor interaction style |
363
+ | `hoverStrength` | `number` | `0` | Effect intensity (0 = disabled) |
364
+ | `hoverRadius` | `number` | `0.2` | Effect radius as a fraction of canvas size |
365
+ | `artStyle` | `ArtStyle` | `'classic'` | Art style preset (see `ART_STYLE_PRESETS`) |
366
+ | `ditherStrength` | `number` | `0` | Floyd-Steinberg dither intensity (0–1) |
367
+ | `dotSizeRatio` | `number` | `0.8` | Dot size when `renderMode === 'dots'` (fraction of cell) |
368
+
369
+ ### Background Options
370
+
371
+ | Option | Type | Default | Description |
372
+ |---|---|---|---|
373
+ | `type` | `string` | `'wave'` | Background renderer |
374
+ | `colorScheme` | `'auto' \| 'light' \| 'dark'` | `'dark'` | Theme; `'auto'` follows OS preference |
375
+ | `fontSize` | `number` | `13` | Character size |
376
+ | `speed` | `number` | `1` | Animation speed multiplier |
377
+ | `density` | `number` | `0.55` | Fraction of cells active (0–1) |
378
+ | `accentColor` | `string` | varies | Highlight / head colour |
379
+
380
+ ## License
381
+
382
+ MIT © [asciify.org](https://asciify.org)
383
+
384
+ ---
385
+
386
+ <p align="left">
387
+ <a href="https://www.buymeacoffee.com/asciify">☕ Buy me a coffee — if this saved you time, I'd appreciate it!</a>
388
+ </p>
389
+
390
+
12
391
  **[▶ Live Playground](https://asciify.org) · [npm](https://www.npmjs.com/package/asciify-engine) · [Support the project ☕](https://www.buymeacoffee.com/asciify)**
13
392
 
14
393
  ## Features
package/dist/index.cjs CHANGED
@@ -970,7 +970,7 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
970
970
  }
971
971
 
972
972
  // src/core/simple-api.ts
973
- async function asciify(source, canvas, { fontSize = 10, style = "classic", options = {} } = {}) {
973
+ async function asciify(source, canvas, { fontSize = 10, artStyle = "classic", options = {} } = {}) {
974
974
  let el;
975
975
  if (typeof source === "string") {
976
976
  const img = new Image();
@@ -990,16 +990,16 @@ async function asciify(source, canvas, { fontSize = 10, style = "classic", optio
990
990
  } else {
991
991
  el = source;
992
992
  }
993
- const preset = ART_STYLE_PRESETS[style];
993
+ const preset = ART_STYLE_PRESETS[artStyle];
994
994
  const merged = { ...DEFAULT_OPTIONS, ...preset, ...options, fontSize };
995
995
  const ctx = canvas.getContext("2d");
996
996
  if (!ctx) throw new Error("Could not get 2d context from canvas");
997
997
  const { frame } = imageToAsciiFrame(el, merged, canvas.width, canvas.height);
998
998
  renderFrameToCanvas(ctx, frame, merged, canvas.width, canvas.height);
999
999
  }
1000
- async function asciifyGif(source, canvas, { fontSize = 10, style = "classic", options = {} } = {}) {
1000
+ async function asciifyGif(source, canvas, { fontSize = 10, artStyle = "classic", options = {} } = {}) {
1001
1001
  const buffer = typeof source === "string" ? await fetch(source).then((r) => r.arrayBuffer()) : source;
1002
- const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[style], ...options, fontSize };
1002
+ const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[artStyle], ...options, fontSize };
1003
1003
  const ctx = canvas.getContext("2d");
1004
1004
  if (!ctx) throw new Error("Could not get 2d context from canvas");
1005
1005
  const { frames, fps } = await gifToAsciiFrames(buffer, merged, canvas.width, canvas.height);
@@ -1023,20 +1023,22 @@ async function asciifyGif(source, canvas, { fontSize = 10, style = "classic", op
1023
1023
  cancelAnimationFrame(animId);
1024
1024
  };
1025
1025
  }
1026
- async function asciifyVideo(source, canvas, { fontSize = 10, style = "classic", options = {} } = {}) {
1026
+ async function asciifyVideo(source, canvas, { fontSize = 10, artStyle = "classic", options = {} } = {}) {
1027
1027
  let video;
1028
1028
  if (typeof source === "string") {
1029
1029
  video = document.createElement("video");
1030
1030
  video.crossOrigin = "anonymous";
1031
1031
  video.src = source;
1032
- await new Promise((resolve, reject) => {
1033
- video.onloadeddata = () => resolve();
1034
- video.onerror = () => reject(new Error(`Failed to load video: ${source}`));
1035
- });
1032
+ if (video.readyState < 2) {
1033
+ await new Promise((resolve, reject) => {
1034
+ video.onloadeddata = () => resolve();
1035
+ video.onerror = () => reject(new Error(`Failed to load video: ${source}`));
1036
+ });
1037
+ }
1036
1038
  } else {
1037
1039
  video = source;
1038
1040
  }
1039
- const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[style], ...options, fontSize };
1041
+ const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[artStyle], ...options, fontSize };
1040
1042
  const ctx = canvas.getContext("2d");
1041
1043
  if (!ctx) throw new Error("Could not get 2d context from canvas");
1042
1044
  const { frames, fps } = await videoToAsciiFrames(video, merged, canvas.width, canvas.height);