ansimax 1.2.3 → 1.2.5

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/CHANGELOG.md CHANGED
@@ -3,6 +3,201 @@
3
3
  All notable changes to **ansimax** are documented in this file.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [1.2.5] — Phase 3 closure: image-to-ASCII engine
7
+
8
+ Minor release closing the **ASCII engine roadmap (Phase 3)** with five
9
+ new capabilities. All additions are fully backwards-compatible — existing
10
+ code runs identically.
11
+
12
+ ### Added — `ascii.fromImage(pixels, opts)` — image-to-ASCII converter
13
+
14
+ Convert a `PixelGrid` (from `ansimax.images`) into ASCII art. Five
15
+ features in one call:
16
+
17
+ ```ts
18
+ import { ascii } from 'ansimax';
19
+
20
+ console.log(ascii.fromImage(pixels, {
21
+ width: 80,
22
+ ramp: 'detailed', // 'standard' | 'detailed' | 'blocks' | 'simple' | custom string
23
+ invert: false,
24
+ dither: 'floyd-steinberg', // 'none' | 'floyd-steinberg'
25
+ edgeDetect: 'sobel', // 'none' | 'sobel'
26
+ edgeThreshold: 40,
27
+ color: true, // preserve source colors
28
+ faceMode: false, // histogram stretch for portraits
29
+ }));
30
+ ```
31
+
32
+ **Aspect-ratio aware**: terminal cells are ~2× as tall as wide, so the
33
+ output height is auto-halved to maintain visual proportion (override
34
+ with `height`).
35
+
36
+ **Zero-dependency**: input is a `PixelGrid` (one Pixel per cell). Users
37
+ of `sharp`, `jimp`, or any decoder convert their output to `PixelGrid`
38
+ once, then call `ascii.fromImage()`.
39
+
40
+ ### Added — `ASCII_RAMPS` — pre-built character ramps
41
+
42
+ Four curated character ramps, ordered dark → light, exported as a
43
+ read-only object:
44
+
45
+ ```ts
46
+ ASCII_RAMPS.standard // ' .:-=+*#%@' — balanced 10-char (default)
47
+ ASCII_RAMPS.detailed // 70-char Paul Bourke — max detail
48
+ ASCII_RAMPS.blocks // ' ░▒▓█' — looks like a real photo
49
+ ASCII_RAMPS.simple // ' .+#' — minimal 4-char
50
+ ```
51
+
52
+ Or pass any custom string as the `ramp` option for full control.
53
+
54
+ ### Added — Sobel edge detection
55
+
56
+ Set `edgeDetect: 'sobel'` to render edges instead of luminance. Useful
57
+ for line-art effects or technical diagrams:
58
+
59
+ ```ts
60
+ console.log(ascii.fromImage(pixels, {
61
+ width: 100,
62
+ edgeDetect: 'sobel',
63
+ edgeThreshold: 50, // tune for noise/detail balance
64
+ ramp: 'blocks',
65
+ }));
66
+ ```
67
+
68
+ ### Added — Floyd-Steinberg dithering
69
+
70
+ Set `dither: 'floyd-steinberg'` for error-diffusion dithering. Produces
71
+ smoother tonal gradients in photos. Most useful with shorter ramps:
72
+
73
+ ```ts
74
+ ascii.fromImage(pixels, {
75
+ width: 80,
76
+ ramp: 'simple',
77
+ dither: 'floyd-steinberg',
78
+ });
79
+ ```
80
+
81
+ ### Added — Face mode
82
+
83
+ Set `faceMode: true` to apply histogram stretching ([10%, 90%] percentile
84
+ remap to [0, 255]). Boosts midtone contrast where facial features live,
85
+ producing better results on portraits:
86
+
87
+ ```ts
88
+ ascii.fromImage(portraitPixels, {
89
+ width: 60,
90
+ ramp: 'detailed',
91
+ faceMode: true,
92
+ });
93
+ ```
94
+
95
+ ### Added — Figlet (.flf) font support
96
+
97
+ Parse and render with community FIGfonts (250+ available at
98
+ [figlet.org](http://www.figlet.org/fontdb.cgi)):
99
+
100
+ ```ts
101
+ import { readFileSync } from 'node:fs';
102
+ import { parseFiglet, ascii } from 'ansimax';
103
+
104
+ const fontStr = readFileSync('./standard.flf', 'utf8');
105
+ const font = parseFiglet(fontStr);
106
+
107
+ console.log(ascii.figletText('Hello!', font, {
108
+ trim: true,
109
+ colorFn: (t) => t, // optional colorize
110
+ }));
111
+ ```
112
+
113
+ Returns a `FigletFont` object containing the parsed glyphs. Renders
114
+ ASCII 32-126; unknown chars fall back to space.
115
+
116
+ ### Added — Type exports
117
+
118
+ `AsciiRamp`, `FromImageOptions`, `FigletFont`, `FigletOptions` — all
119
+ exported from the main barrel.
120
+
121
+ ### Notes
122
+
123
+ - Phase 3 of the [roadmap](README.md#%EF%B8%8F-roadmap) is now **fully complete**.
124
+ - Image decoding (PNG/JPEG → PixelGrid) is intentionally **not** included;
125
+ users pair ansimax with `sharp`/`jimp`/`pngjs` to keep zero deps.
126
+ - 1914 + 30 new tests pass.
127
+ - No new runtime dependencies — still zero.
128
+
129
+ ---
130
+
131
+ ## [1.2.4] — Gradient utilities + inspectability
132
+
133
+ Patch release adding gradient inspection and manipulation utilities.
134
+ No breaking changes — `createGradient()` callers from 1.2.3 continue
135
+ to work, but now have access to metadata.
136
+
137
+ ### Added — `ReusableGradient` metadata
138
+
139
+ `createGradient()` now returns a `ReusableGradient` — still callable
140
+ like before, but with frozen metadata for inspection and debugging:
141
+
142
+ ```ts
143
+ const fire = createGradient(['#ff5555', '#ffb86c', '#f1fa8c'], {
144
+ easing: 'ease-in',
145
+ });
146
+
147
+ // All still callable
148
+ console.log(fire('hello'));
149
+
150
+ // New: read-only inspection
151
+ fire.stops; // → ['#ff5555', '#ffb86c', '#f1fa8c']
152
+ fire.resolvedStops; // → [{r:255,g:85,b:85}, {r:255,g:184,b:108}, ...]
153
+ fire.defaultOptions; // → { easing: 'ease-in' }
154
+ ```
155
+
156
+ All three properties are frozen — attempting to mutate them throws in
157
+ strict mode and silently fails in sloppy mode.
158
+
159
+ ### Added — `reverseGradient()` helper
160
+
161
+ Returns a new gradient with stops in reverse order. Works with both
162
+ plain arrays and `ReusableGradient` instances. Default options are
163
+ preserved when reversing a `ReusableGradient`:
164
+
165
+ ```ts
166
+ const fire = createGradient(['#ff5555', '#ffb86c', '#f1fa8c']);
167
+ const ice = reverseGradient(fire); // → '#f1fa8c' → '#ffb86c' → '#ff5555'
168
+
169
+ console.log(fire('warm side'));
170
+ console.log(ice('cool side'));
171
+
172
+ // Also works with plain arrays
173
+ reverseGradient(['#f00', '#0f0', '#00f']); // → ['#00f', '#0f0', '#f00']
174
+ ```
175
+
176
+ The original array / gradient is never mutated.
177
+
178
+ ### Added — `presets` alias (canonical name)
179
+
180
+ Previously `presets` was exported only as `colorPresets`. Many users
181
+ referenced `presets` based on docs and got `ReferenceError`. Now both
182
+ names point to the same object:
183
+
184
+ ```ts
185
+ import { presets, colorPresets } from 'ansimax';
186
+
187
+ presets === colorPresets; // → true
188
+ ```
189
+
190
+ `colorPresets` remains for backwards compatibility; new code can use
191
+ either name.
192
+
193
+ ### Notes
194
+
195
+ - Coverage holds steady at ~98%.
196
+ - No new runtime dependencies — still zero.
197
+ - All 1892 + 22 new tests pass.
198
+
199
+ ---
200
+
6
201
  ## [1.2.3] — Gradient factory + performance
7
202
 
8
203
  Patch release adding a new performance-oriented API and refinements. No
package/README.es.md CHANGED
@@ -7,7 +7,7 @@
7
7
  _Colores • Gradientes • Animaciones • ASCII Art • Pixel Art • Árboles • Componentes • Temas_
8
8
 
9
9
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE)
10
- [![npm](https://img.shields.io/badge/npm-v1.2.3-cb3837.svg?style=flat-square)](https://www.npmjs.com/package/ansimax)
10
+ [![npm](https://img.shields.io/badge/npm-v1.2.5-cb3837.svg?style=flat-square)](https://www.npmjs.com/package/ansimax)
11
11
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6.svg?style=flat-square)](tsconfig.json)
12
12
  [![Coverage](https://img.shields.io/badge/coverage-98%25-brightgreen.svg?style=flat-square)](#testing)
13
13
  [![Tests](https://img.shields.io/badge/tests-1700%2B%20passing-brightgreen.svg?style=flat-square)](#testing)
@@ -263,7 +263,7 @@ console.log(gradientRect({
263
263
  ### Gradientes reusables (v1.2.3)
264
264
 
265
265
  ```ts
266
- import { createGradient, ascii } from 'ansimax';
266
+ import { createGradient, reverseGradient, ascii } from 'ansimax';
267
267
 
268
268
  // Pre-resuelve los stops de hex una vez — significativamente más rápido para uso repetido
269
269
  const fire = createGradient(['#ff5555', '#ffb86c', '#f1fa8c']);
@@ -275,6 +275,14 @@ console.log(fire('Tercera línea'));
275
275
  // Úsalo como colorFn para banners — misma firma de ColorFn
276
276
  console.log(ascii.banner('FIRE', { colorFn: fire }));
277
277
 
278
+ // v1.2.4: inspecciona metadata
279
+ console.log('Stops:', fire.stops); // → ['#ff5555', '#ffb86c', '#f1fa8c']
280
+ console.log('Resolved:', fire.resolvedStops); // → [{r:255,g:85,b:85}, ...]
281
+
282
+ // v1.2.4: invierte un gradiente (preserva las opciones por defecto)
283
+ const ice = reverseGradient(fire);
284
+ console.log(ice('Lado frío'));
285
+
278
286
  // Las opciones por-llamada aún funcionan — perfecto para animación
279
287
  for (let p = 0; p < 1; p += 0.05) {
280
288
  process.stdout.write('\r' + fire('fluyendo', { phase: p }));
@@ -298,6 +306,60 @@ console.log(ascii.banner('HOLA', {
298
306
  console.log(ascii.box('¡Caja arcoiris!', { padding: 1, borderStyle: 'rounded' }));
299
307
  ```
300
308
 
309
+ ### Imagen → ASCII (v1.2.5)
310
+
311
+ ```ts
312
+ import { ascii } from 'ansimax';
313
+ import type { PixelGrid } from 'ansimax';
314
+
315
+ // Obtén un PixelGrid de cualquier librería de imágenes (sharp, jimp, pngjs, etc.)
316
+ // Cada Pixel es `{ r, g, b }` o null
317
+ const pixels: PixelGrid = await loadImagePixels('./foto.png');
318
+
319
+ // Monocromo
320
+ console.log(ascii.fromImage(pixels, { width: 80 }));
321
+
322
+ // Color + dithering Floyd-Steinberg + ramp detallado
323
+ console.log(ascii.fromImage(pixels, {
324
+ width: 100,
325
+ color: true,
326
+ dither: 'floyd-steinberg',
327
+ ramp: 'detailed',
328
+ }));
329
+
330
+ // Modo detección de bordes (line art)
331
+ console.log(ascii.fromImage(pixels, {
332
+ width: 80,
333
+ edgeDetect: 'sobel',
334
+ edgeThreshold: 50,
335
+ ramp: 'blocks',
336
+ }));
337
+
338
+ // Modo rostro para retratos (mejora contraste de tonos medios)
339
+ console.log(ascii.fromImage(retratoPixels, {
340
+ width: 60,
341
+ ramp: 'detailed',
342
+ faceMode: true,
343
+ }));
344
+ ```
345
+
346
+ ### Fuentes Figlet (v1.2.5)
347
+
348
+ ```ts
349
+ import { readFileSync } from 'node:fs';
350
+ import { parseFiglet, ascii } from 'ansimax';
351
+
352
+ // Descarga fuentes desde http://www.figlet.org/fontdb.cgi
353
+ const font = parseFiglet(readFileSync('./standard.flf', 'utf8'));
354
+
355
+ console.log(ascii.figletText('¡Hola!', font));
356
+
357
+ // Con color
358
+ console.log(ascii.figletText('STYLE', font, {
359
+ colorFn: (t) => gradient(t, ['#ff79c6', '#bd93f9', '#8be9fd']),
360
+ }));
361
+ ```
362
+
301
363
  ### Árboles
302
364
 
303
365
  <img src="media/trees.png" alt="Árboles" />
@@ -357,7 +419,7 @@ console.log(components.table([
357
419
  ['loaders', color.green('● listo'), '100%'],
358
420
  ], { borderStyle: 'rounded' }));
359
421
 
360
- console.log(components.badge('VERSION', 'v1.2.3'));
422
+ console.log(components.badge('VERSION', 'v1.2.5'));
361
423
  console.log(components.badge('BUILD', 'passing'));
362
424
  ```
363
425
 
@@ -571,7 +633,7 @@ El roadmap apunta intencionalmente — y busca superar — gaps que ni siquiera
571
633
  - [x] **Curvas de interpolación** — `linear` / `ease-in` / `ease-out` / `ease-in-out` / `cubic-bezier` / personalizado (v1.2.0)
572
634
  - [x] **Gradientes cónicos** — barrido radial con `style: 'conic'` (v1.2.0)
573
635
 
574
- ### 🟡 Fase 3 — Motor ASCII
636
+ ### Fase 3 — Motor ASCII
575
637
  - [x] Fuentes de bloque (`big`, `small`)
576
638
  - [x] Banner con gradiente + alineación + coloreado por carácter
577
639
  - [x] Dibujo de cajas (6 estilos de borde)
@@ -579,11 +641,12 @@ El roadmap apunta intencionalmente — y busca superar — gaps que ni siquiera
579
641
  - [x] Compositor de logos (gradiente + box wrapping)
580
642
  - [x] Registro de fuentes personalizadas (`registerFont`, `hasFont`, `listFonts`)
581
643
  - [x] API de stream (`ascii.stream()` con AbortSignal)
582
- - [ ] **Conversor Imagen → ASCII** (con detección de bordes, Sobel/Canny)
583
- - [ ] **Renderizado ASCII en color** (preservar colores de imagen)
584
- - [ ] **Dithering de imágenes** para mejor rango tonal (Floyd-Steinberg)
585
- - [ ] **ASCII optimizado para rostros** (modo de alto detalle para retratos)
586
- - [ ] **Soporte de fuentes figlet** (loader de archivos `.flf` 250+ fuentes de comunidad)
644
+ - [x] **Conversor Imagen → ASCII** — `ascii.fromImage()` con mapeo de luminancia (v1.2.5)
645
+ - [x] **Renderizado ASCII en color** preserva colores de imagen con `color: true` (v1.2.5)
646
+ - [x] **Dithering de imágenes** error diffusion Floyd-Steinberg (v1.2.5)
647
+ - [x] **ASCII optimizado para rostros** histogram stretching para retratos (v1.2.5)
648
+ - [x] **Soporte de fuentes figlet** parser + renderer `.flf` (`parseFiglet` + `ascii.figletText`) (v1.2.5)
649
+ - [x] **Detección de bordes** — operador Sobel integrado en `fromImage` (v1.2.5, bonus)
587
650
 
588
651
  ### ✅ Fase 4 — Primitivas TUI
589
652
  - [x] Tablas (filas irregulares, celdas multi-línea, conscientes de ANSI)
@@ -759,6 +822,56 @@ ansimax/
759
822
 
760
823
  ## 📝 Changelog
761
824
 
825
+ ### v1.2.5 — Fase 3 completa: motor de imagen-a-ASCII
826
+
827
+ Release minor que cierra el roadmap del motor ASCII con 5 features nuevas:
828
+
829
+ - 🖼️ **`ascii.fromImage(pixels, opts)`** — Conversor Imagen → ASCII (input: `PixelGrid`)
830
+ - 🎨 **Renderizado ASCII en color** — `color: true` preserva los colores fuente
831
+ - 📐 **Dithering Floyd-Steinberg** — `dither: 'floyd-steinberg'` para gradientes tonales más suaves
832
+ - 👤 **Modo optimizado para rostros** — `faceMode: true` mejora contraste de tonos medios en retratos
833
+ - 🔠 **Soporte Figlet (.flf)** — `parseFiglet()` + `ascii.figletText()` para 250+ fuentes de comunidad
834
+ - ⚡ **Bonus: detección de bordes Sobel** — `edgeDetect: 'sobel'` para efectos line-art
835
+
836
+ ```ts
837
+ import { ascii } from 'ansimax';
838
+
839
+ // Imagen a ASCII (input desde sharp/jimp/etc, sin dependencia de decoder)
840
+ console.log(ascii.fromImage(pixels, {
841
+ width: 80,
842
+ color: true,
843
+ dither: 'floyd-steinberg',
844
+ ramp: 'detailed',
845
+ }));
846
+
847
+ // Renderizado Figlet
848
+ const font = parseFiglet(readFileSync('./standard.flf', 'utf8'));
849
+ console.log(ascii.figletText('¡Hola!', font));
850
+ ```
851
+
852
+ Drop-in replacement para `1.2.4`.
853
+
854
+ ### v1.2.4 — Utilidades de gradiente + inspectabilidad
855
+
856
+ Release patch añadiendo metadata de inspección y un helper `reverseGradient()`:
857
+
858
+ - 🔍 **`ReusableGradient` expone `.stops`, `.resolvedStops`, `.defaultOptions`** — todos congelados, todos de solo lectura
859
+ - 🔄 **Helper `reverseGradient()`** — invierte el orden de stops de un gradiente (funciona con arrays o `ReusableGradient`)
860
+ - 🎯 **`presets` exportado con su nombre canónico** — junto al alias existente `colorPresets`
861
+
862
+ ```ts
863
+ import { createGradient, reverseGradient } from 'ansimax';
864
+
865
+ const fire = createGradient(['#ff5555', '#ffb86c', '#f1fa8c']);
866
+ const ice = reverseGradient(fire);
867
+
868
+ console.log(fire.stops); // ['#ff5555', '#ffb86c', '#f1fa8c'] — read-only
869
+ console.log(fire('cálido'));
870
+ console.log(ice('frío'));
871
+ ```
872
+
873
+ Drop-in replacement para `1.2.3`.
874
+
762
875
  ### v1.2.3 — Factory de gradientes + performance
763
876
 
764
877
  Release patch añadiendo una API orientada a performance:
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  _Colors • Gradients • Animations • ASCII Art • Pixel Art • Trees • Components • Themes_
8
8
 
9
9
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE)
10
- [![npm](https://img.shields.io/badge/npm-v1.2.3-cb3837.svg?style=flat-square)](https://www.npmjs.com/package/ansimax)
10
+ [![npm](https://img.shields.io/badge/npm-v1.2.5-cb3837.svg?style=flat-square)](https://www.npmjs.com/package/ansimax)
11
11
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6.svg?style=flat-square)](tsconfig.json)
12
12
  [![Coverage](https://img.shields.io/badge/coverage-98%25-brightgreen.svg?style=flat-square)](#testing)
13
13
  [![Tests](https://img.shields.io/badge/tests-1700%2B%20passing-brightgreen.svg?style=flat-square)](#testing)
@@ -263,7 +263,7 @@ console.log(gradientRect({
263
263
  ### Reusable Gradients (v1.2.3)
264
264
 
265
265
  ```ts
266
- import { createGradient, ascii } from 'ansimax';
266
+ import { createGradient, reverseGradient, ascii } from 'ansimax';
267
267
 
268
268
  // Pre-resolve hex stops once — significantly faster for repeated use
269
269
  const fire = createGradient(['#ff5555', '#ffb86c', '#f1fa8c']);
@@ -275,6 +275,14 @@ console.log(fire('Third line'));
275
275
  // Use as a colorFn for banners — same ColorFn signature
276
276
  console.log(ascii.banner('FIRE', { colorFn: fire }));
277
277
 
278
+ // v1.2.4: inspect metadata
279
+ console.log('Stops:', fire.stops); // → ['#ff5555', '#ffb86c', '#f1fa8c']
280
+ console.log('Resolved:', fire.resolvedStops); // → [{r:255,g:85,b:85}, ...]
281
+
282
+ // v1.2.4: reverse a gradient (preserves default options)
283
+ const ice = reverseGradient(fire);
284
+ console.log(ice('Cool side'));
285
+
278
286
  // Per-call options still work — perfect for animation
279
287
  for (let p = 0; p < 1; p += 0.05) {
280
288
  process.stdout.write('\r' + fire('flowing', { phase: p }));
@@ -298,6 +306,60 @@ console.log(ascii.banner('HELLO', {
298
306
  console.log(ascii.box('Rainbow box!', { padding: 1, borderStyle: 'rounded' }));
299
307
  ```
300
308
 
309
+ ### Image → ASCII (v1.2.5)
310
+
311
+ ```ts
312
+ import { ascii } from 'ansimax';
313
+ import type { PixelGrid } from 'ansimax';
314
+
315
+ // Get a PixelGrid from any image library (sharp, jimp, pngjs, etc.)
316
+ // Each Pixel is `{ r, g, b }` or null
317
+ const pixels: PixelGrid = await loadImagePixels('./photo.png');
318
+
319
+ // Monochrome
320
+ console.log(ascii.fromImage(pixels, { width: 80 }));
321
+
322
+ // Color + Floyd-Steinberg dithering + detailed ramp
323
+ console.log(ascii.fromImage(pixels, {
324
+ width: 100,
325
+ color: true,
326
+ dither: 'floyd-steinberg',
327
+ ramp: 'detailed',
328
+ }));
329
+
330
+ // Edge-detection mode (line art)
331
+ console.log(ascii.fromImage(pixels, {
332
+ width: 80,
333
+ edgeDetect: 'sobel',
334
+ edgeThreshold: 50,
335
+ ramp: 'blocks',
336
+ }));
337
+
338
+ // Face mode for portraits (boosts midtone contrast)
339
+ console.log(ascii.fromImage(portraitPixels, {
340
+ width: 60,
341
+ ramp: 'detailed',
342
+ faceMode: true,
343
+ }));
344
+ ```
345
+
346
+ ### Figlet Fonts (v1.2.5)
347
+
348
+ ```ts
349
+ import { readFileSync } from 'node:fs';
350
+ import { parseFiglet, ascii } from 'ansimax';
351
+
352
+ // Download fonts from http://www.figlet.org/fontdb.cgi
353
+ const font = parseFiglet(readFileSync('./standard.flf', 'utf8'));
354
+
355
+ console.log(ascii.figletText('Hello!', font));
356
+
357
+ // With color
358
+ console.log(ascii.figletText('STYLE', font, {
359
+ colorFn: (t) => gradient(t, ['#ff79c6', '#bd93f9', '#8be9fd']),
360
+ }));
361
+ ```
362
+
301
363
  ### Trees
302
364
 
303
365
  <img src="media/trees.png" alt="Trees" />
@@ -357,7 +419,7 @@ console.log(components.table([
357
419
  ['loaders', color.green('● ready'), '100%'],
358
420
  ], { borderStyle: 'rounded' }));
359
421
 
360
- console.log(components.badge('VERSION', 'v1.2.3'));
422
+ console.log(components.badge('VERSION', 'v1.2.5'));
361
423
  console.log(components.badge('BUILD', 'passing'));
362
424
  ```
363
425
 
@@ -571,7 +633,7 @@ The roadmap intentionally targets — and aims to surpass — gaps that even mat
571
633
  - [x] **Gradient interpolation curves** — `linear` / `ease-in` / `ease-out` / `ease-in-out` / `cubic-bezier` / custom (v1.2.0)
572
634
  - [x] **Conic gradients** — radial sweep with `style: 'conic'` (v1.2.0)
573
635
 
574
- ### 🟡 Phase 3 — ASCII engine
636
+ ### Phase 3 — ASCII engine
575
637
  - [x] Block fonts (`big`, `small`)
576
638
  - [x] Banner with gradient + alignment + per-char coloring
577
639
  - [x] Box drawing (6 border styles)
@@ -579,11 +641,12 @@ The roadmap intentionally targets — and aims to surpass — gaps that even mat
579
641
  - [x] Logo composer (gradient + box wrapping)
580
642
  - [x] Custom font registry (`registerFont`, `hasFont`, `listFonts`)
581
643
  - [x] Stream API (`ascii.stream()` with AbortSignal)
582
- - [ ] **Image → ASCII** converter (with edge detection, Sobel/Canny)
583
- - [ ] **Color ASCII** rendering (preserve image colors)
584
- - [ ] **Image dithering** for better tonal range (Floyd-Steinberg)
585
- - [ ] **Face-optimized ASCII** (high-detail mode for portraits)
586
- - [ ] **Figlet font support** (.flf file loader 250+ community fonts)
644
+ - [x] **Image → ASCII** converter — `ascii.fromImage()` with luminance mapping (v1.2.5)
645
+ - [x] **Color ASCII** rendering preserve image colors via `color: true` (v1.2.5)
646
+ - [x] **Image dithering** Floyd-Steinberg error diffusion (v1.2.5)
647
+ - [x] **Face-optimized ASCII** histogram stretching for portraits (v1.2.5)
648
+ - [x] **Figlet font support** — `.flf` parser + renderer (`parseFiglet` + `ascii.figletText`) (v1.2.5)
649
+ - [x] **Edge detection** — Sobel operator integrated in `fromImage` (v1.2.5, bonus)
587
650
 
588
651
  ### ✅ Phase 4 — Terminal UI primitives
589
652
  - [x] Tables (irregular rows, multi-line cells, ANSI-aware)
@@ -759,6 +822,56 @@ ansimax/
759
822
 
760
823
  ## 📝 Changelog
761
824
 
825
+ ### v1.2.5 — Phase 3 complete: image-to-ASCII engine
826
+
827
+ Minor release closing the ASCII engine roadmap with 5 new features:
828
+
829
+ - 🖼️ **`ascii.fromImage(pixels, opts)`** — Image → ASCII converter (input: `PixelGrid`)
830
+ - 🎨 **Color ASCII rendering** — `color: true` preserves source colors
831
+ - 📐 **Floyd-Steinberg dithering** — `dither: 'floyd-steinberg'` for smoother tonal gradients
832
+ - 👤 **Face-optimized mode** — `faceMode: true` boosts midtone contrast for portraits
833
+ - 🔠 **Figlet (.flf) support** — `parseFiglet()` + `ascii.figletText()` for 250+ community fonts
834
+ - ⚡ **Bonus: Sobel edge detection** — `edgeDetect: 'sobel'` for line-art effects
835
+
836
+ ```ts
837
+ import { ascii } from 'ansimax';
838
+
839
+ // Image to ASCII (input from sharp/jimp/etc, no decoder dependency)
840
+ console.log(ascii.fromImage(pixels, {
841
+ width: 80,
842
+ color: true,
843
+ dither: 'floyd-steinberg',
844
+ ramp: 'detailed',
845
+ }));
846
+
847
+ // Figlet rendering
848
+ const font = parseFiglet(readFileSync('./standard.flf', 'utf8'));
849
+ console.log(ascii.figletText('Hello!', font));
850
+ ```
851
+
852
+ Drop-in replacement for `1.2.4`.
853
+
854
+ ### v1.2.4 — Gradient utilities + inspectability
855
+
856
+ Patch release adding inspection metadata and a `reverseGradient()` helper:
857
+
858
+ - 🔍 **`ReusableGradient` exposes `.stops`, `.resolvedStops`, `.defaultOptions`** — all frozen, all read-only
859
+ - 🔄 **`reverseGradient()` helper** — flips a gradient's stop order (works with arrays or `ReusableGradient`)
860
+ - 🎯 **`presets` exported as canonical name** — alongside the existing `colorPresets` alias
861
+
862
+ ```ts
863
+ import { createGradient, reverseGradient } from 'ansimax';
864
+
865
+ const fire = createGradient(['#ff5555', '#ffb86c', '#f1fa8c']);
866
+ const ice = reverseGradient(fire);
867
+
868
+ console.log(fire.stops); // ['#ff5555', '#ffb86c', '#f1fa8c'] — read-only
869
+ console.log(fire('warm'));
870
+ console.log(ice('cool'));
871
+ ```
872
+
873
+ Drop-in replacement for `1.2.3`.
874
+
762
875
  ### v1.2.3 — Gradient factory + performance
763
876
 
764
877
  Patch release adding a performance-oriented API: