asciify-engine 4.15.0 → 4.17.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.15.0",
3
+ "version": "4.17.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.15.0"
5
+ tested-engine: "4.17.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.15.0**. 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.17.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
@@ -170,7 +170,7 @@ dark.addEventListener('change', event => ground.update({ poster: still(event.mat
170
170
 
171
171
  - **Canvases are decoration.** Kit fields are `aria-hidden` and ignore the pointer. Only the canvases that are the content take a `label`: `fieldBackground` `notFound`, `asciiVideo`, `artPanel`.
172
172
  - **Real text stays in the DOM.** `decode`, `wordmark` and `streamText` keep the actual words selectable and readable; `odometer` and `pill` counts add a hidden copy; `blockChart` adds a hidden table. Do not replace text with a field.
173
- - **Status is announced.** Pass a live region to `buttonLoading` (`announce`) and `thinking` (`label`), write `statusOrb` outcomes to one yourself, and give `pullRefresh` its `messages`. `toastStack` adds its own `role="status"` region.
173
+ - **Status is announced.** Pass a live region to `buttonLoading` (`announce`), give `thinking` its `label` (it adds a hidden live copy, so the label itself needs no role), write `statusOrb` outcomes to one yourself, and give `pullRefresh` its `messages`. `toastStack` adds its own `role="status"` region.
174
174
  - **Keyboard and focus.** The kit wires keys for `tabs`, `commandPalette`, `slideToConfirm`, `compare`, `accordion`, `swipeDeck` and `codeInput`; keep them (`manage` stays true). Keep a Refresh button beside pull to refresh. A field never covers a focus ring: `beam` draws inside the border, so a 2px outline at 3px offset stays clear.
175
175
 
176
176
  ```ts
@@ -243,12 +243,12 @@ reloadButton.onclick = async () => { s.reset(); renderPost(await fetchPost()); a
243
243
  Category: AI
244
244
 
245
245
  Prompt:
246
- “Show [our assistant]'s thinking state with Asciify: dot lines shimmer where the answer will appear under a Thinking… label, then the answer streams in word by word and the dots settle into a dotted rule beneath it.”
246
+ “Show [our assistant]'s thinking state with Asciify: a wave runs through the Thinking… label, each letter rolling up through glyphs and back like an odometer; when the answer starts the label rolls into Thought for 2s and the answer opens beneath it, word by word.”
247
247
 
248
248
  ```ts
249
249
  import { thinking } from 'asciify-engine/kit';
250
250
 
251
- const reply = thinking(answer, { label: statusLabel, lines: 3 }); // "Thinking…", then "Thought for 2s"
251
+ const reply = thinking(answer, { label: statusLabel }); // "Thinking…" with a rolling wave, then "Thought for 2s"
252
252
  await reply.stream(await ask(question)); // or an async iterable of chunks
253
253
  regenerate.onclick = async () => { reply.think(); await reply.stream(await ask(question)); };
254
254
  ```
@@ -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)) {
@@ -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. |
@@ -380,7 +380,7 @@ A toast is `{ title, body?, icon?, action?, duration? }`; `icon` is a node (such
380
380
 
381
381
  ### thinking
382
382
 
383
- An assistant reply's thinking state: line-shaped bands of drifting dots while the model thinks, then the answer fades in word by word as the dots give way, leaving a quiet dotted rule under it.
383
+ An assistant reply's thinking state. While the model thinks, a wave runs through the status label: each letter in turn rolls up through glyphs and lands back on itself, in the label's own font, like the odometer. When the answer starts, every letter rolls once more into `Thought for 2s`, and the answer fades in word by word.
384
384
 
385
385
  ```ts
386
386
  const reply = thinking(answer, { label: status }); // status reads "Thinking…", then "Thought for 2s"
@@ -388,21 +388,21 @@ await reply.stream(await ask(question)); // or an async iterable of c
388
388
  regenerate.onclick = () => reply.think();
389
389
  ```
390
390
 
391
- Options: `lines` (bands while thinking, default 3; the element is held at that many lines high), `label` (an element for the status), `labels` (`{ thinking, done(seconds) }`), `start` (default true; false waits for `think()`), `stagger` (ms between words, default 38), `rule` (default true), `onState(state, seconds)` (for a status you render yourself). Methods: `think()`, `stream(text | chunks)` (resolves once the last word has faded in; a partial word is held until it is whole); getter `state` (`thinking`, `streaming`, `done`). Sets `aria-busy` while thinking; with border-box sizing the held height adds the element's padding, so bottom padding makes room for the rule. Ink defaults to `--ascii-ink`, else a quiet graphite; the dots sit under the text.
391
+ Options: `label` (the status element; the field rolls its characters, and a visually hidden live copy announces each status), `labels` (`{ thinking, done(seconds) }`), `start` (default true; false waits for `think()`), `stagger` (ms between words, default 38), `roll` (ms one letter rolls, default 520), `wave` (ms between neighbouring letters, default 55), `glyphs` (default `#%*+=-:`), `onState(state, seconds)` (for a status you render yourself). `lines` and `rule` are ignored since 4.17. Methods: `think()`, `stream(text | chunks)` (resolves once the last word has faded in; a partial word is held until it is whole); getter `state` (`thinking`, `streaming`, `done`). Sets `aria-busy` while thinking. The answer element is empty while thinking: open it with the reply (for example a `grid-template-rows` transition from `0fr` to `1fr` keyed on the state) rather than holding an empty box. Ink defaults to the label's text colour.
392
392
 
393
393
  ### streamText
394
394
 
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