asciify-engine 4.14.2 → 4.16.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "asciify-engine",
3
- "version": "4.14.2",
3
+ "version": "4.16.0",
4
4
  "description": "Framework-agnostic ASCII art engine. Convert images, videos, and GIFs into ASCII art rendered on canvas, or run full-bleed GPU character fields with a fluid cursor wake.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -2,13 +2,13 @@
2
2
  name: asciify-engine
3
3
  description: Apply Asciify the way asciify.org does - ASCII components and backgrounds that belong in a real interface, inked from the theme, clear around text, inside the page's radii and motion. Use for UI components in characters (loading, status, toasts, tabs, AI states, charts, transitions, pages), animated ASCII backgrounds, and also photo, video and GIF conversion, GPU character fields, art styles and export, hover and motion, layered typography and rendering performance.
4
4
  metadata:
5
- tested-engine: "4.14.2"
5
+ tested-engine: "4.16.0"
6
6
  updated: "2026-10-17"
7
7
  ---
8
8
 
9
9
  # Asciify Engine
10
10
 
11
- Use the published `asciify-engine` npm package. Tested against **4.14.2**. Check the application's installed version and its public types before using newer options; a local checkout can contain unpublished changes.
11
+ Use the published `asciify-engine` npm package. Tested against **4.16.0**. Check the application's installed version and its public types before using newer options; a local checkout can contain unpublished changes.
12
12
 
13
13
  - Agent entry: https://asciify.org/skill (redirects to this Markdown document).
14
14
  - Components: https://asciify.org/components · Backgrounds: https://asciify.org/backgrounds
@@ -36,26 +36,25 @@ copyButton.addEventListener('click', () => bg.pulse(copyButton));
36
36
 
37
37
  ## 2. Ink: quiet by default, colour only for meaning
38
38
 
39
- Inks come from theme tokens, defined for both schemes. The kit re-reads them when `prefers-color-scheme` changes and when `class` or `data-theme` changes on `<html>` or `<body>`, so define the dark values under both selectors. These are the engine's own fallbacks and the site's graphite:
39
+ Inks come from the app's own design tokens, never from hex values you pick. Find the app's text, muted-text, background and status tokens first (CSS variables, a Tailwind theme, a tokens file) and derive the Asciify inks from them, so they follow every theme the app already has:
40
40
 
41
41
  ```css
42
42
  :root {
43
- --ascii-ink: #76746e; /* grounds, rings, quiet strokes */
44
- --ascii-ink-strong: #1d1d1b; /* a component that is the focus */
45
- --ascii-success: #17844a; --ascii-warning: #9a6700; --ascii-error: #c93636;
43
+ /* Map to the app's tokens; the names on the right are examples. */
44
+ --ascii-ink-strong: color-mix(in oklab, var(--foreground) 92%, var(--background)); /* a component that is the focus */
45
+ --ascii-ink: color-mix(in oklab, var(--foreground) 50%, var(--background)); /* grounds, rings, quiet strokes */
46
+ --ascii-success: var(--success); --ascii-warning: var(--warning); --ascii-error: var(--destructive);
46
47
  }
47
- @media (prefers-color-scheme: dark) {
48
- :root:not([data-theme="light"]) { --ascii-ink: #7a7872; --ascii-ink-strong: #d8d6cf; --ascii-success: #3ccf7e; --ascii-warning: #f5b83d; --ascii-error: #ff6b6b; }
49
- }
50
- :root[data-theme="dark"] { --ascii-ink: #7a7872; --ascii-ink-strong: #d8d6cf; --ascii-success: #3ccf7e; --ascii-warning: #f5b83d; --ascii-error: #ff6b6b; }
51
48
  ```
52
49
 
50
+ Because these derive from tokens the app already switches per theme, they need no dark-mode block of their own. The kit re-reads them when `prefers-color-scheme` changes and when `class` or `data-theme` changes on `<html>` or `<body>`. Left undefined, `--ascii-ink-strong` falls back to the component element's own text colour, and the status inks to the engine's greens, ambers and reds; define them anyway so nothing is guessed. Only when an app truly has no tokens, use the site's graphite: `#1d1d1b` and `#76746e` on light, `#d8d6cf` and `#7a7872` on dark.
51
+
53
52
  Rules:
54
53
 
55
54
  - **Default to graphite.** Leave `ink` unset: components pick `--ascii-ink` or `--ascii-ink-strong` and background presets use their graphite. On another brand, map these tokens to its muted text colour, never to its accent.
56
55
  - **Colour means something.** Green, amber and red are for outcomes only, and come from `--ascii-success`, `--ascii-warning` and `--ascii-error`. The site's phosphor marks live states only and is never a button or text.
57
56
  - **Palettes are deliberate.** `beam` `colorful`, `voiceOrb` `aurora` and `fieldBackground` `brand` are chosen palettes for one moment the user asked for, not a page default.
58
- - **Never a new colour.** No hex that is not a token, no black canvas on a light page, no white glow on a coloured image.
57
+ - **Never a new colour.** No hex that is not a token (map to the app's tokens as above), no black canvas on a light page, no white glow on a coloured image.
59
58
 
60
59
  ```ts
61
60
  import { background, halftone, statusOrb, beam } from 'asciify-engine/kit';
@@ -257,12 +257,12 @@ regenerate.onclick = async () => { reply.think(); await reply.stream(await ask(q
257
257
  Category: AI
258
258
 
259
259
  Prompt:
260
- “Stream [our assistant]’s reply with an Asciify write head: an aria-hidden field over the text draws the caret and each token’s unresolved characters as flickering glyphs that settle into letters. Show reasoning lines, Stop and Regenerate.”
260
+ “Stream [our assistant]’s reply with an Asciify write head: each character rolls up through glyphs in our own font and settles on the crisp letter, like an odometer, with the caret riding the front. Show reasoning lines, Stop and Regenerate.”
261
261
 
262
262
  ```ts
263
263
  import { streamText } from 'asciify-engine/kit';
264
264
 
265
- const out = streamText(answerText, { caret: true, settle: 60, stagger: 18, reasoningLines: 2 });
265
+ const out = streamText(answerText, { transition: 'roll', caret: true, stagger: 18, reasoningLines: 2 });
266
266
  async function run() {
267
267
  out.reset();
268
268
  for await (const part of llm.stream(question)) {
@@ -517,8 +517,11 @@ export function asciiField(canvas: HTMLCanvasElement, options: GlyphFieldOptions
517
517
  ```
518
518
 
519
519
  ```css
520
- :root { --ascii-ink: #76746e; --ascii-ink-strong: #1d1d1b; }
521
- @media (prefers-color-scheme: dark) { :root { --ascii-ink: #7a7872; --ascii-ink-strong: #d8d6cf; } }
520
+ /* From the app's own tokens (see applying-asciify.md); the names on the right are examples. */
521
+ :root {
522
+ --ascii-ink: color-mix(in oklab, var(--foreground) 50%, var(--background));
523
+ --ascii-ink-strong: color-mix(in oklab, var(--foreground) 92%, var(--background));
524
+ }
522
525
  ```
523
526
 
524
527
  In React, wrap it once and reuse it:
@@ -49,6 +49,7 @@ Size the canvas with CSS (for example `position:absolute; inset:0; width:100%; h
49
49
  | `background` | `'transparent'` | Hex or `'transparent'`; the canvas is premultiplied. |
50
50
  | `backdrop` | `0` | Opacity of a soft, blurred copy of the source under the glyphs. It is sampled from the mip chain, so it costs nothing extra. |
51
51
  | `levels` | `'auto'` | Stretches the source's 2nd–98th luminance percentiles across the charset. Needs pixel access; tainted cross-origin media falls back to `'none'`. |
52
+ | `blend` | `'auto'` | Shades between two characters, mixed across neighbouring cells in a fixed 4x4 pattern, as print halftones do. `'auto'` turns it on for photo and video sources drawn in nine or fewer characters; drawn interface shapes stay crisp. `'on'`, `'off'` force it. Locked to the grid, so it never shimmers; one comparison per cell. |
52
53
  | `brightness`, `contrast`, `gamma`, `invert` | `0`, `1`, `1`, `false` | Applied after levels. |
53
54
  | `fit`, `focus` | `'cover'`, centre | `focus` moves the cover crop (0–1 on each axis). |
54
55
  | `vignette` | `0` | Darkens toward the corners. |
@@ -98,7 +98,7 @@ await fetchPost();
98
98
  await s.resolve();
99
99
  ```
100
100
 
101
- Options: `blocks` (selector, default `[data-block]`; `reset()` fades loaded content out under the returning skeleton), `shimmer` (ms per sweep, default 1250), `resolve` (ms per block, default 240), `stagger` (ms between blocks unless `data-delay` is set, default 60), `develop` (ms an image develops, default 560). Methods: `resolve()` (a promise for the last block), `reset()`; getter `loading`. Sets `aria-busy` on the element while loading.
101
+ Options: `blocks` (selector, default `[data-block]`; `reset()` fades loaded content out under the returning skeleton), `shimmer` (ms per pass of a wave that loops without pausing until `resolve()`, default 1400), `resolve` (ms per block, default 420), `stagger` (ms between blocks unless `data-delay` is set, default 60), `develop` (ms an image develops, default 640). Block shapes come from the blocks' own CSS: an image's `border-radius`, and one bar per rendered line of text, as wide as the words and as tall as the font's x-height, so the skeleton follows the design's tokens without settings. Methods: `resolve()` (a promise for the last block), `reset()`; getter `loading`. Sets `aria-busy` on the element while loading.
102
102
 
103
103
  ## pill
104
104
 
@@ -175,7 +175,7 @@ const off = subscribeGlobalLook(look => menu.value = look?.charset ?? '');
175
175
  orb.update({ ink: '--brand', look: { cellSize: 6 } });
176
176
  ```
177
177
 
178
- `setGlobalLook({ charset })` applies to every glyph field, live or released, except dot marks, fields whose own `charset` holds letters (a word or a brand, such as `wordmark`), and fields created with `globalLook: false` (use it for a locked region). An empty `charset` keeps each field's own. `getGlobalLook()` reads the current look, so `subscribeGlobalLook` pairs with React's `useSyncExternalStore`.
178
+ `setGlobalLook({ charset })` balances the set's weight against classic ASCII: block and solid sets draw smaller glyphs so they keep detail, sets without light glyphs (binary) ease off, and faint sets (minimal, braille, code) draw larger, bolder glyphs; tones are untouched. It applies to every glyph field, live or released, except dot marks, fields whose own `charset` holds letters (a word or a brand, such as `wordmark`), and fields created with `globalLook: false` (use it for a locked region). An empty `charset` keeps each field's own. `getGlobalLook()` reads the current look, so `subscribeGlobalLook` pairs with React's `useSyncExternalStore`.
179
179
 
180
180
  Each change sweeps: a field that has drawn its first frame holds that frame over itself and wipes it away left to right in 200ms (after a 40ms beat, `cubic-bezier(.23,1,.32,1)`), led by a 2px scan line, as the new characters land under it. The line is `--ascii-sweep` (default amber `#f3ba32`, at 35% with a soft glow); the held frame and the line are an `aria-hidden` canvas and span marked `data-ascii-sweep`, placed exactly over the field (style them alongside the field, for example with the same mask) and removed when the sweep ends. Under reduced motion (the visitor's, or `reducedMotion: true`), and for a field still on its poster, the characters change at once.
181
181
 
@@ -395,14 +395,14 @@ Options: `lines` (bands while thinking, default 3; the element is held at that m
395
395
  Streamed text with a glyph write head: each token lands as flickering character cells that resolve into letters left to right, with a block caret after the last one. The real text is in the DOM all along, for selection and screen readers.
396
396
 
397
397
  ```ts
398
- const out = streamText(paragraph);
398
+ const out = streamText(paragraph, { transition: 'roll' });
399
399
  for await (const step of llm.reasoning(prompt)) out.reasoning(step); // optional: a short tail of reasoning above the reply
400
400
  for await (const token of llm.stream(prompt)) out.write(token);
401
401
  await out.done();
402
402
  stopButton.onclick = () => out.stop();
403
403
  ```
404
404
 
405
- Options: `caret` (default true: bright while writing, dim at rest), `settle` (ms the first character of a token flickers, default 60), `stagger` (ms more per character, default 18), `reasoningLines` (reasoning lines kept in view, default 2), `onState(state)`. Methods: `write(token)`, `reasoning(line)` (adds a line to a tail the kit puts just before the element, `.ascii-reasoning`: the newest lines stay in view, older ones scroll up under a fade, and it collapses once the reply is done or stopped), `done()` (resolves once the last characters land), `stop()` (lands what was written at once; stopped while only reasoning, it folds the tail away), `reset()` (clears the text and the reasoning); getters `state` (`idle`, `streaming`, `done`, `stopped`) and `text`. Sets `aria-busy` while streaming. The field covers the content box from a quarter line above to a quarter line below, in cells half a line tall and about a letter wide, so each glyph sits on its line's lowercase letters like a typed character; give the element a fixed `line-height`. New reasoning lines slide in together with the old ones. Ink defaults to the text colour.
405
+ Options: `transition` (set at creation: `'flicker'`, the default, or `'roll'`, where each character rolls up through glyphs in the text's own font and settles on the crisp letter like the odometer, the caret riding the front of the roll), `roll` (ms one character rolls, default 300), `glyphs` (what it rolls through, default `#%*+=-:`), `caret` (default true: bright while writing, dim at rest), `settle` (ms the first character of a token flickers, default 60), `stagger` (ms more per character, default 18; under `roll`, the gap between characters starting), `reasoningLines` (reasoning lines kept in view, default 2), `onState(state)`. Methods: `write(token)`, `reasoning(line)` (adds a line to a tail the kit puts just before the element, `.ascii-reasoning`: the newest lines stay in view, older ones scroll up under a fade, and it collapses once the reply is done or stopped), `done()` (resolves once the last characters land), `stop()` (lands what was written at once; stopped while only reasoning, it folds the tail away), `reset()` (clears the text and the reasoning); getters `state` (`idle`, `streaming`, `done`, `stopped`) and `text`. Sets `aria-busy` while streaming. The field covers the content box from a quarter line above to a quarter line below, in cells half a line tall and about a letter wide, so each glyph sits on its line's lowercase letters like a typed character; give the element a fixed `line-height`. New reasoning lines slide in together with the old ones. Ink defaults to the text colour.
406
406
 
407
407
  ### compare
408
408