asciify-engine 1.0.38 → 1.0.40

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,23 +4,14 @@
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-support-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-☕-d4ff00?labelColor=0a0a0a&style=flat-square" alt="Buy Me A Coffee" /></a>
8
8
  </p>
9
9
 
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.
10
+ Turn any image, video, or GIF into ASCII art on an HTML canvas with live animated backgrounds, hover effects, and 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)**
12
+ **[▶ Try the live playground](https://asciify.org) · [npm](https://www.npmjs.com/package/asciify-engine)**
13
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
- - **14 animated backgrounds** — Wave, Rain, Stars, Pulse, Noise, Grid, Aurora, Silk, Void, Morph, Fire, DNA, Terrain, Circuit
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
14
+ ---
24
15
 
25
16
  ## Install
26
17
 
@@ -28,401 +19,191 @@
28
19
  npm install asciify-engine
29
20
  ```
30
21
 
31
- ## Quick Start
22
+ ---
32
23
 
33
- ### Image ASCII
24
+ ## The 30-second version
34
25
 
35
26
  ```ts
36
- import { asciify } from 'asciify-engine';
37
-
38
- const canvas = document.querySelector('canvas')!;
39
-
40
- // Minimal — just a URL and a canvas, no img.onload boilerplate needed
41
- await asciify('https://example.com/photo.jpg', canvas);
42
-
43
- // With an art style preset
44
- await asciify('photo.jpg', canvas, { artStyle: 'letters' });
45
-
46
- // Full control
47
- await asciify('photo.jpg', canvas, {
48
- fontSize: 8,
49
- artStyle: 'art',
50
- options: {
51
- colorMode: 'fullcolor',
52
- invert: true,
53
- hoverEffect: 'spotlight',
54
- hoverStrength: 0.5,
55
- },
56
- });
27
+ import { imageToAsciiFrame, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
28
+
29
+ const img = new Image();
30
+ img.src = 'your-image.jpg';
31
+ img.onload = () => {
32
+ 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);
35
+ };
57
36
  ```
58
37
 
59
- > **Canvas sizing:** Set `width` and `height` as HTML attributes on the `<canvas>` element these control the pixel grid. CSS sizing alone (`style="width:100%"`) won't work (the canvas will be 0×0). Use `canvas.width = el.clientWidth` to fill a container.
38
+ That's itone function to convert, one to draw. Everything else is optional.
60
39
 
61
- ### Webcam → ASCII
40
+ ---
62
41
 
63
- ```ts
64
- import { asciifyWebcam } from 'asciify-engine';
42
+ ## Animated backgrounds
65
43
 
66
- const canvas = document.querySelector('canvas')!;
44
+ Add a living ASCII animation to any element with one line:
67
45
 
68
- // Requests camera access, starts a live rAF loop, returns stop()
69
- const stop = await asciifyWebcam(canvas, {
70
- artStyle: 'terminal',
71
- mirror: true, // horizontal flip (selfie mode)
72
- });
46
+ ```ts
47
+ import { asciiBackground } from 'asciify-engine';
73
48
 
74
- // Release camera + cancel loop
75
- stop();
49
+ asciiBackground('#hero', { type: 'rain' });
76
50
  ```
77
51
 
78
- ### GIF Animation
52
+ Returns a cleanup function when you're done:
79
53
 
80
54
  ```ts
81
- import { asciifyGif } from 'asciify-engine';
82
-
83
- // Fetches, converts, and starts the animation loop — returns a stop() function
84
- const stop = await asciifyGif('animation.gif', canvas);
85
-
86
- // Clean up the animation loop when done
55
+ const stop = asciiBackground('#hero', { type: 'aurora', colorScheme: 'auto' });
56
+ // later…
87
57
  stop();
88
58
  ```
89
59
 
90
- ### Video
60
+ **Available types:** `wave` · `rain` · `stars` · `pulse` · `noise` · `grid` · `aurora` · `silk` · `void` · `morph`
61
+
62
+ **Common options:**
91
63
 
92
64
  ```ts
93
- import { asciifyVideo } from 'asciify-engine';
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
+ });
73
+ ```
94
74
 
95
- // Accepts a URL string or an existing HTMLVideoElement
96
- const stop = await asciifyVideo('/my-video.mp4', canvas, { artStyle: 'terminal' });
75
+ ---
97
76
 
98
- // Cancel when unmounting / navigating away
99
- stop();
100
- ```
77
+ ## More recipes
101
78
 
102
79
  ### React
103
80
 
104
81
  ```tsx
105
82
  import { useEffect, useRef } from 'react';
106
- import { asciify } from 'asciify-engine';
83
+ import { imageToAsciiFrame, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
107
84
 
108
85
  export function AsciiImage({ src }: { src: string }) {
109
86
  const ref = useRef<HTMLCanvasElement>(null);
110
87
 
111
88
  useEffect(() => {
112
- if (ref.current) asciify(src, ref.current, { artStyle: 'art' });
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
+ };
113
98
  }, [src]);
114
99
 
115
100
  return <canvas ref={ref} width={800} height={600} />;
116
101
  }
117
102
  ```
118
103
 
119
- ### Animated GIF or video in React
104
+ ### Animated GIF
120
105
 
121
- ```tsx
122
- import { useEffect, useRef } from 'react';
123
- import { asciifyGif } from 'asciify-engine';
106
+ ```ts
107
+ import { gifToAsciiFrames, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
124
108
 
125
- export function AsciiGif({ src }: { src: string }) {
126
- const ref = useRef<HTMLCanvasElement>(null);
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 };
127
112
 
128
- useEffect(() => {
129
- let stop: (() => void) | undefined;
130
- asciifyGif(src, ref.current!).then(fn => { stop = fn; });
131
- return () => stop?.(); // cancels the rAF loop on unmount
132
- }, [src]);
113
+ const { frames, fps } = await gifToAsciiFrames(buf, opts, canvas.width, canvas.height);
133
114
 
134
- return <canvas ref={ref} width={800} height={600} />;
135
- }
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);
136
120
  ```
137
121
 
138
- ### Vue
139
-
140
- ```vue
141
- <template>
142
- <canvas ref="canvasRef" width="800" height="600" />
143
- </template>
144
-
145
- <script setup lang="ts">
146
- import { useTemplateRef, onMounted } from 'vue';
147
- import { asciify } from 'asciify-engine';
148
-
149
- const props = defineProps<{ src: string; artStyle?: string }>();
150
- const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');
122
+ ### Video
151
123
 
152
- onMounted(() => asciify(props.src, canvasRef.value!, { artStyle: props.artStyle as any }));
153
- </script>
154
- ```
124
+ ```ts
125
+ import { videoToAsciiFrames, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
155
126
 
156
- ### Angular
127
+ const video = document.createElement('video');
128
+ video.src = '/clip.mp4';
129
+ await new Promise(r => (video.onloadeddata = r));
157
130
 
158
- ```typescript
159
- import { Component, ElementRef, ViewChild, AfterViewInit, Input } from '@angular/core';
160
- import { asciify } from 'asciify-engine';
131
+ const canvas = document.getElementById('ascii') as HTMLCanvasElement;
132
+ const opts = { ...DEFAULT_OPTIONS, fontSize: 8 };
161
133
 
162
- @Component({
163
- selector: 'app-ascii',
164
- template: `<canvas #canvas width="800" height="600"></canvas>`,
165
- })
166
- export class AsciiComponent implements AfterViewInit {
167
- @ViewChild('canvas') canvasRef!: ElementRef<HTMLCanvasElement>;
168
- @Input() src = '';
169
- @Input() artStyle = 'classic';
134
+ const { frames, fps } = await videoToAsciiFrames(video, opts, canvas.width, canvas.height);
170
135
 
171
- ngAfterViewInit() {
172
- asciify(this.src, this.canvasRef.nativeElement, { artStyle: this.artStyle as any });
173
- }
174
- }
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);
175
141
  ```
176
142
 
177
- ## Animated Backgrounds
178
-
179
- Drop a live ASCII animation into any element with one call.
180
-
181
- ```ts
182
- import { asciiBackground } from 'asciify-engine';
183
-
184
- // Attach to any selector — animates the element's background
185
- asciiBackground('#hero', { type: 'rain' });
143
+ ---
186
144
 
187
- // Follows OS dark/light mode automatically
188
- asciiBackground('#hero', { type: 'aurora', colorScheme: 'auto' });
145
+ ## Tweaking the output
189
146
 
190
- // Force light mode (dark characters on light background)
191
- asciiBackground('#hero', { type: 'wave', colorScheme: 'light' });
147
+ Pass options to any function to change the look:
192
148
 
193
- // Stop / clean up
194
- const { destroy } = asciiBackground('#hero', { type: 'stars' });
195
- destroy();
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
+ };
196
160
  ```
197
161
 
198
- ### Background Types
162
+ **Color modes at a glance:**
199
163
 
200
- | Type | Description |
164
+ | Mode | What it does |
201
165
  |---|---|
202
- | `wave` | Flowing sine-wave field with noise turbulence |
203
- | `rain` | Matrix-style vertical column rain with glowing head and fading tail |
204
- | `stars` | Parallax star field that reacts to cursor position |
205
- | `pulse` | Concentric ripple bursts that emanate from the cursor |
206
- | `noise` | Smooth value-noise field with organic flow |
207
- | `grid` | Geometric grid that warps and glows at cursor proximity |
208
- | `aurora` | Sweeping borealis-style colour bands |
209
- | `silk` | Silky fluid swirls following the cursor |
210
- | `void` | Gravitational singularity — characters spiral inward toward cursor |
211
- | `morph` | Characters morph between shapes driven by noise |
212
- | `fire` | Upward-drifting flame columns with heat-gradient character mapping |
213
- | `dna` | Rotating double-helix strands with base-pair characters |
214
- | `terrain` | Procedural heightmap landscape with parallax depth layers |
215
- | `circuit` | PCB trace network that pulses with traveling electric signals |
216
-
217
- ### `asciiBackground` Options
218
-
219
- | Option | Type | Default | Description |
220
- |---|---|---|---|
221
- | `type` | `string` | `'wave'` | Which background renderer to use |
222
- | `colorScheme` | `'auto' \| 'light' \| 'dark'` | `'dark'` | `'auto'` follows OS theme live; `'light'` = dark chars on light bg |
223
- | `fontSize` | `number` | `13` | Character size in px |
224
- | `accentColor` | `string` | varies | Head/highlight colour (CSS colour string) |
225
- | `color` | `string` | — | Override body character colour |
226
- | `speed` | `number` | `1` | Global animation speed multiplier |
227
- | `density` | `number` | `0.55` | Fraction of cells active (0–1) |
228
- | `lightMode` | `boolean` | `false` | Dark characters on light background |
229
-
230
- Each background type also accepts its own specific options — see the individual type exports (e.g. `RainBackgroundOptions`, `WaveBackgroundOptions`, etc.) for the full list.
231
-
232
- ### Low-level background renderers
233
-
234
- All renderers are also exported individually for direct canvas use:
166
+ | `grayscale` | Classic monochrome ASCII |
167
+ | `fullcolor` | Preserves original pixel colours |
168
+ | `matrix` | Everything in green, like the film |
169
+ | `accent` | Single highlight colour |
235
170
 
236
- ```ts
237
- import { renderRainBackground } from 'asciify-engine';
238
-
239
- const canvas = document.getElementById('canvas') as HTMLCanvasElement;
240
- const ctx = canvas.getContext('2d')!;
241
- let t = 0;
242
-
243
- function tick() {
244
- renderRainBackground(ctx, canvas.width, canvas.height, t, {
245
- speed: 1.2,
246
- density: 0.6,
247
- accentColor: '#00ffcc',
248
- lightMode: false,
249
- });
250
- t += 0.016;
251
- requestAnimationFrame(tick);
252
- }
253
- tick();
254
- ```
171
+ **Hover effects:** `spotlight` · `flashlight` · `magnifier` · `force-field` · `neon` · `fire` · `ice` · `gravity` · `shatter` · `ghost`
255
172
 
256
- ## Low-level API
173
+ ---
257
174
 
258
- For cases where the one-call API is too limiting — direct frame manipulation, custom render loops, progress callbacks, etc.
175
+ ## Embed generation
259
176
 
260
- ### GIF (low-level)
177
+ Export your ASCII art as a self-contained HTML file:
261
178
 
262
179
  ```ts
263
- import { gifToAsciiFrames, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
264
-
265
- const buffer = await fetch('animation.gif').then(r => r.arrayBuffer());
266
- const options = { ...DEFAULT_OPTIONS, fontSize: 8 };
267
- const { frames, fps } = await gifToAsciiFrames(buffer, options, canvas.width, canvas.height);
180
+ import { generateEmbedCode, generateAnimatedEmbedCode } from 'asciify-engine';
268
181
 
269
- const ctx = canvas.getContext('2d')!;
270
- let i = 0;
271
- let last = performance.now();
272
- const interval = 1000 / fps;
273
-
274
- let animId: number;
275
- const tick = (now: number) => {
276
- if (now - last >= interval) {
277
- renderFrameToCanvas(ctx, frames[i], options, canvas.width, canvas.height);
278
- i = (i + 1) % frames.length;
279
- last = now;
280
- }
281
- animId = requestAnimationFrame(tick);
282
- };
283
- animId = requestAnimationFrame(tick);
182
+ // Static image
183
+ const html = generateEmbedCode(frame, options);
284
184
 
285
- // Clean up → cancelAnimationFrame(animId);
185
+ // Animated
186
+ const html = generateAnimatedEmbedCode(frames, options, fps);
286
187
  ```
287
188
 
288
- ### Video (low-level)
289
-
290
- ```ts
291
- import { videoToAsciiFrames, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
292
-
293
- const video = document.createElement('video');
294
- video.crossOrigin = 'anonymous';
295
- video.src = '/my-video.mp4';
296
- // Guard against cached video where onloadeddata already fired
297
- if (video.readyState < 2) {
298
- await new Promise<void>(r => { video.onloadeddata = () => r(); });
299
- }
300
-
301
- const options = { ...DEFAULT_OPTIONS, fontSize: 8 };
302
- const { frames, fps } = await videoToAsciiFrames(video, options, canvas.width, canvas.height, 12, 10);
303
-
304
- const ctx = canvas.getContext('2d')!;
305
- let i = 0;
306
- let last = performance.now();
307
- const interval = 1000 / fps;
308
-
309
- let animId: number;
310
- const tick = (now: number) => {
311
- if (now - last >= interval) {
312
- renderFrameToCanvas(ctx, frames[i], options, canvas.width, canvas.height);
313
- i = (i + 1) % frames.length;
314
- last = now;
315
- }
316
- animId = requestAnimationFrame(tick);
317
- };
318
- animId = requestAnimationFrame(tick);
189
+ ---
319
190
 
320
- // Clean up → cancelAnimationFrame(animId);
321
- ```
191
+ ## API summary
322
192
 
323
- ### Vanilla JS (low-level image)
324
-
325
- ```html
326
- <canvas id="ascii" width="800" height="600"></canvas>
327
- <script type="module">
328
- import { imageToAsciiFrame, renderFrameToCanvas, DEFAULT_OPTIONS } from 'asciify-engine';
329
-
330
- const canvas = document.getElementById('ascii');
331
- const ctx = canvas.getContext('2d');
332
- const options = { ...DEFAULT_OPTIONS, fontSize: 10 };
333
-
334
- const img = new Image();
335
- img.crossOrigin = 'anonymous';
336
- img.src = 'https://picsum.photos/600/400';
337
- img.onload = () => {
338
- const { frame } = imageToAsciiFrame(img, options, canvas.width, canvas.height);
339
- renderFrameToCanvas(ctx, frame, options, canvas.width, canvas.height);
340
- };
341
- </script>
342
- ```
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 |
343
202
 
344
- ## API Reference
345
-
346
- ### Core Functions
347
-
348
- | Function | Returns | Description |
349
- |---|---|---|
350
- | `asciify(source, canvas, opts?)` | `Promise<void>` | Render a **single** ASCII frame from an image (or URL). For animated loops use `asciifyGif` / `asciifyVideo` |
351
- | `asciifyGif(source, canvas, opts?)` | `Promise<() => void>` | Fetch a GIF, convert, and start an animation loop — returns `stop()` |
352
- | `asciifyVideo(source, canvas, opts?)` | `Promise<() => void>` | Convert a video and start an animation loop — returns `stop()` |
353
- | `imageToAsciiFrame(source, options, w?, h?)` | `{ frame, cols, rows }` | Convert an image, video frame, or canvas to a raw ASCII frame |
354
- | `renderFrameToCanvas(ctx, frame, options, w, h, time?, hoverPos?)` | `void` | Render a raw ASCII frame to a 2D canvas context |
355
- | `gifToAsciiFrames(buffer, options, w, h, onProgress?)` | `{ frames, cols, rows, fps }` | Parse an animated GIF `ArrayBuffer` into ASCII frames |
356
- | `videoToAsciiFrames(video, options, w, h, fps?, maxDuration?, onProgress?)` | `{ frames, cols, rows, fps }` | Extract and convert video frames to ASCII |
357
- | `asciifyWebcam(canvas, opts?)` | `Promise<() => void>` | Start a live webcam → ASCII loop; returns `stop()` to cancel and release the camera |
358
- | `asciiBackground(target, options)` | `{ destroy: () => void }` | Mount a live animated ASCII background; call `destroy()` to stop and remove |
359
- | `generateEmbedCode(frame, options)` | `string` | Self-contained static HTML embed |
360
- | `generateAnimatedEmbedCode(frames, options, fps)` | `string` | Self-contained animated HTML embed |
361
-
362
- ### Simple API Options (`AsciifySimpleOptions`)
363
-
364
- Used by `asciify()`, `asciifyGif()`, and `asciifyVideo()`:
365
-
366
- | Option | Type | Default | Description |
367
- |---|---|---|---|
368
- | `fontSize` | `number` | `10` | Character cell size in px |
369
- | `artStyle` | `ArtStyle` | `'classic'` | Art style preset — sets charset, render mode, and color mode together |
370
- | `options` | `Partial<AsciiOptions>` | `{}` | Fine-grained overrides applied on top of the preset |
371
-
372
- ### Key Options (`AsciiOptions`)
373
-
374
- | Option | Type | Default | Description |
375
- |---|---|---|---|
376
- | `fontSize` | `number` | `10` | Character cell size in px |
377
- | `colorMode` | `'grayscale' \| 'fullcolor' \| 'matrix' \| 'accent'` | `'grayscale'` | Color output mode |
378
- | `renderMode` | `'ascii' \| 'dots'` | `'ascii'` | Render as characters or dot particles |
379
- | `charset` | `string` | `' .:-=+*#%@'` | Custom character density ramp (light → dark) |
380
- | `brightness` | `number` | `0` | Brightness adjustment (−1 → 0 → 1) |
381
- | `contrast` | `number` | `0` | Contrast boost (0 = unchanged, positive = more contrast) |
382
- | `invert` | `boolean` | `false` | Invert luminance mapping |
383
- | `animationStyle` | `AnimationStyle` | `'none'` | Per-character animation driven over time |
384
- | `hoverEffect` | `HoverEffect` | `'spotlight'` | Cursor interaction style |
385
- | `hoverStrength` | `number` | `0` | Effect intensity (0 = disabled) |
386
- | `hoverRadius` | `number` | `0.2` | Effect radius as a fraction of canvas size |
387
- | `artStyle` | `ArtStyle` | `'classic'` | Art style preset (see `ART_STYLE_PRESETS`) |
388
- | `ditherStrength` | `number` | `0` | Floyd-Steinberg dither intensity (0–1) |
389
- | `dotSizeRatio` | `number` | `0.8` | Dot size when `renderMode === 'dots'` (fraction of cell) |
390
-
391
- ### Art Styles (`artStyle`)
392
-
393
- | Value | Color mode | Description |
394
- |---|---|---|
395
- | `classic` | Grayscale | Standard density ramp — clean, universally readable |
396
- | `art` | Full color | 70-char dense ramp for maximum tonal detail |
397
- | `particles` | Full color | Dot circles (`renderMode: 'dots'`) — great for photos |
398
- | `letters` | Full color | Alphabet characters with pixel-accurate color |
399
- | `terminal` | Matrix | Classic charset with green phosphor / Matrix look |
400
- | `claudeCode` | Accent | Box-drawing chars with accent color — technical/hacker aesthetic |
401
- | `braille` | Full color | 256-char braille block — ultra-dense, printed feel |
402
- | `katakana` | Matrix | Half-width katakana — anime / cyberpunk aesthetic |
403
- | `box` | Grayscale | Filled block elements `▪◾◼■█` |
404
- | `lines` | Grayscale | Dash/em-dash ramp — minimalist typographic look |
405
- | `circles` | Accent | Concentric circle chars with accent highlight |
406
- | `musical` | Accent | Music notation ♩♪♫♬♭♮♯ — playful, low density |
407
- | `emoji` | Full color | Block emoji mosaic — best at larger `fontSize` |
408
-
409
- ### Background Options
410
-
411
- | Option | Type | Default | Description |
412
- |---|---|---|---|
413
- | `type` | `string` | `'wave'` | Background renderer |
414
- | `colorScheme` | `'auto' \| 'light' \| 'dark'` | `'dark'` | Theme; `'auto'` follows OS preference |
415
- | `fontSize` | `number` | `13` | Character size |
416
- | `speed` | `number` | `1` | Animation speed multiplier |
417
- | `density` | `number` | `0.55` | Fraction of cells active (0–1) |
418
- | `accentColor` | `string` | varies | Highlight / head colour |
203
+ ---
419
204
 
420
205
  ## License
421
206
 
422
207
  MIT © [asciify.org](https://asciify.org)
423
208
 
424
- ---
425
-
426
- <p align="left">
427
- <a href="https://www.buymeacoffee.com/asciify">☕ Buy me a coffee — if this saved you time, I'd appreciate it!</a>
428
- </p>
209
+ > ☕ [Buy me a coffee](https://www.buymeacoffee.com/asciify) — if this saved you time, I'd appreciate it!
package/dist/index.cjs CHANGED
@@ -112,6 +112,8 @@ var DEFAULT_OPTIONS = {
112
112
  animationSpeed: 1,
113
113
  dotSizeRatio: 0.8,
114
114
  ditherStrength: 0,
115
+ charAspect: 0.55,
116
+ normalize: false,
115
117
  hoverStrength: 0,
116
118
  hoverRadius: 0.2,
117
119
  hoverEffect: "spotlight",
@@ -606,7 +608,7 @@ function imageToAsciiFrame(source, options, targetWidth, targetHeight) {
606
608
  if (srcWidth === 0 || srcHeight === 0) {
607
609
  return { frame: [], cols: 0, rows: 0 };
608
610
  }
609
- const charAspect = 0.55;
611
+ const charAspect = options.charAspect;
610
612
  const cellW = options.fontSize * options.charSpacing;
611
613
  const cellH = options.fontSize / charAspect * options.charSpacing;
612
614
  const renderW = targetWidth || srcWidth;
@@ -620,6 +622,18 @@ function imageToAsciiFrame(source, options, targetWidth, targetHeight) {
620
622
  ctx.drawImage(source, 0, 0, cols, rows);
621
623
  const imageData = ctx.getImageData(0, 0, cols, rows);
622
624
  const pixels = imageData.data;
625
+ let normMin = 0;
626
+ let normRange = 255;
627
+ if (options.normalize) {
628
+ let lo = 255, hi = 0;
629
+ for (let k = 0; k < pixels.length; k += 4) {
630
+ const l = 0.299 * pixels[k] + 0.587 * pixels[k + 1] + 0.114 * pixels[k + 2];
631
+ if (l < lo) lo = l;
632
+ if (l > hi) hi = l;
633
+ }
634
+ normMin = lo;
635
+ normRange = hi > lo ? hi - lo : 255;
636
+ }
623
637
  const frame = [];
624
638
  for (let y = 0; y < rows; y++) {
625
639
  const row = [];
@@ -629,7 +643,8 @@ function imageToAsciiFrame(source, options, targetWidth, targetHeight) {
629
643
  const g = pixels[i + 1];
630
644
  const b = pixels[i + 2];
631
645
  const a = pixels[i + 3];
632
- const lum = 0.299 * r + 0.587 * g + 0.114 * b;
646
+ const rawLum = 0.299 * r + 0.587 * g + 0.114 * b;
647
+ const lum = options.normalize ? (rawLum - normMin) / normRange * 255 : rawLum;
633
648
  const adjustedLum = adjustLuminance(lum, options.brightness, options.contrast);
634
649
  const ditheredLum = applyDither(adjustedLum, x, y, options.ditherStrength);
635
650
  const char = options.customText ? customTextToChar(ditheredLum, options.customText, x, y, cols, options.invert) : luminanceToChar(ditheredLum, options.charset, options.invert);