asciify-engine 4.11.0 → 4.12.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.11.0",
3
+ "version": "4.12.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: Build and tune browser ASCII art with asciify-engine so it fits the user's request, preferences and the project's design. Use for photo, video and GIF conversion, interactive heroes, GPU character fields with the user's own words, ambient backgrounds, text banners, art styles and export, hover and motion choices, layered HTML typography, and rendering performance.
4
4
  metadata:
5
- tested-engine: "4.11.0"
6
- updated: "2026-10-09"
5
+ tested-engine: "4.12.0"
6
+ updated: "2026-10-14"
7
7
  ---
8
8
 
9
9
  # Asciify Engine
10
10
 
11
- Use the published `asciify-engine` npm package for browser canvas rendering. Tested against **4.11.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 for browser canvas rendering. Tested against **4.12.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
  - Playground: https://asciify.org/editor
@@ -31,12 +31,15 @@ Most requests are a feeling ("make the hero feel alive", "retro", "subtle behind
31
31
  4. **Decide living or still.** A still photo can be made to feel alive (field `drift`, studio `motion`), but some users want it still. Follow their words; keep art behind text, in grids and in data views still; reduced motion always gets a still. See [living or still](references/design-matching.md#living-or-still).
32
32
  5. **Say what you chose in one line** and offer one alternative. Ask only when the media is missing or motion would be new to a site that has none.
33
33
 
34
+ [references/components.md](references/components.md) has 21 ready-made components with a shared theme-aware helper: interface pieces (image card, avatar, empty state, loading state, copy feedback), backgrounds (hero, cursor field, brand field), text (CLI banner, wordmark, decode headline), transitions (photo from characters, page transition, scramble button), pages (404, coming soon, sign-in, pricing), AI states (thinking, image generation) and video in characters, each with the prompt a person gives and the reference code. Start there when the request matches one.
35
+
34
36
  [references/design-matching.md](references/design-matching.md) maps mood words to settings, explains light themes, contrast and fonts, and gives tested recipes for the common requests. Read it whenever the user describes a look rather than an option.
35
37
 
36
38
  | The user wants | Use |
37
39
  | --- | --- |
38
40
  | Their photo, video or GIF as ASCII on the page, optionally answering the cursor | `asciify` / `asciifyVideo` / `asciifyGif`, plus `mountHover` from `/hover` for the website hovers |
39
41
  | A full-bleed hero or brand field that stays smooth under the cursor, or their own word as the characters | `mountGlyphField` from `/field` ([glyph-field.md](references/glyph-field.md)); `hover: 'trail'` for the classic Asciify Trail on pure characters, `'fluid'` for bold and liquid, `'lantern'` for a subtle light |
42
+ | A calm background behind product content (SaaS hero, pricing, docs), light or dark | `mountGlyphField` with `mark: 'dot'`, `scene: 'drift'`, `clear` around the copy, and `pulse()` on events (4.12) |
40
43
  | Art styles, saved looks, backdrops, masks, effects, PNG/MP4/GIF export | `mountStudio` from `/studio` ([studio-workspace.md](references/studio-workspace.md)) |
41
44
  | Several media and text layers in one artwork | `mountStudioProject` from `/studio` ([layered-projects.md](references/layered-projects.md)) |
42
45
  | An ambient background with no media | A template from asciify.org/backgrounds, or a `/field` generated scene |
@@ -196,3 +199,7 @@ From 4.9, optional Studio adds Stipple, Engraving, Crosshatch, Woodblock, Risogr
196
199
  ### GPU glyph fields (4.11)
197
200
 
198
201
  `asciify-engine/field` adds `mountGlyphField`, `isGlyphFieldSupported` and `glyphFieldTier`. Older installs do not have this entry; upgrade before using it.
202
+
203
+ ## Scenario prompts
204
+
205
+ Use [scenario-prompts.md](references/scenario-prompts.md) for copyable briefs covering images, heroes, characters, all styles, dither, backgrounds, video/GIF, scroll, layers, text, export, frameworks, procedural trails, reference matching, troubleshooting, upgrades, saved looks and accessibility. Adapt the prompt to the user’s intent; do not add unrelated features. Use the published renderer’s native Trail; do not add a separate tonal ribbon overlay.
@@ -0,0 +1,460 @@
1
+ # Asciify components
2
+
3
+ Ready-made ASCII components for product interfaces. Each one has the prompt a person gives you and the reference code you should arrive at. Every component runs live at https://asciify.org/components, where an agent cursor demonstrates its interaction, so the person can see the result before asking for it.
4
+
5
+ Build them in the project's own framework, design tokens and lifecycle. Read [design matching](design-matching.md) first. The shared rules:
6
+
7
+ - Keep text and controls in HTML. The field is a decorative canvas behind or inside them, with `aria-hidden` on the canvas.
8
+ - Give the field a transparent background and an ink from the project's tokens, for each theme.
9
+ - Check `isGlyphFieldSupported()`, and keep the static design (or a poster image of the field) as the fallback.
10
+ - Honor reduced motion. Fields draw one still frame and skip pulses by default.
11
+ - Call `destroy()` on unmount. Mount fields that start off screen lazily, when they come near the viewport.
12
+ - GPU fields need asciify-engine 4.12 or later for `mark`, `clear` and `pulse()`.
13
+
14
+ ## The shared helper
15
+
16
+ Every component below uses this helper. It mounts a field that takes its ink from a CSS custom property, follows light and dark, and cleans up after itself. Put it in the project once.
17
+
18
+ ```ts
19
+ import { isGlyphFieldSupported, mountGlyphField, type GlyphField, type GlyphFieldOptions } from 'asciify-engine/field';
20
+
21
+ /** A field inked from a CSS variable that follows the theme. Returns null when the GPU field isn't available. */
22
+ export function asciiField(canvas: HTMLCanvasElement, options: GlyphFieldOptions = {}, inkVar = '--ascii-ink') {
23
+ if (!isGlyphFieldSupported()) return null;
24
+ const ink = () => getComputedStyle(canvas).getPropertyValue(inkVar).trim() || '#8f8d86';
25
+ const field: GlyphField = mountGlyphField(canvas, { color: 'ink', background: 'transparent', ...options, ink: ink() });
26
+ const scheme = matchMedia('(prefers-color-scheme: dark)');
27
+ const retint = () => field.update({ ink: ink() });
28
+ scheme.addEventListener('change', retint);
29
+ return { field, destroy() { scheme.removeEventListener('change', retint); field.destroy(); } };
30
+ }
31
+ ```
32
+
33
+ ```css
34
+ :root { --ascii-ink: #76746e; --ascii-ink-strong: #1d1d1b; }
35
+ @media (prefers-color-scheme: dark) { :root { --ascii-ink: #7a7872; --ascii-ink-strong: #d8d6cf; } }
36
+ ```
37
+
38
+ In React, wrap it once and reuse it:
39
+
40
+ ```tsx
41
+ import { useEffect, useRef } from 'react';
42
+ import type { GlyphField, GlyphFieldOptions } from 'asciify-engine/field';
43
+
44
+ export function AsciiField({ options, onField, className }: { options: GlyphFieldOptions; onField?: (field: GlyphField | null) => void; className?: string }) {
45
+ const canvas = useRef<HTMLCanvasElement>(null);
46
+ useEffect(() => {
47
+ const mounted = canvas.current ? asciiField(canvas.current, options) : null;
48
+ onField?.(mounted?.field ?? null);
49
+ return () => { onField?.(null); mounted?.destroy(); };
50
+ }, []); // mount once; call field.update() for later changes
51
+ return <canvas ref={canvas} className={className} aria-hidden="true" />;
52
+ }
53
+ ```
54
+
55
+ ## image-card: ASCII image card
56
+ Category: Components
57
+
58
+ Prompt:
59
+ “Make a card whose image is drawn in characters with Asciify. Use [image]. Characters trail behind the cursor, the title, details and a save button stay as HTML under the image, and the ink follows our light and dark theme.”
60
+
61
+ ```ts
62
+ const image = asciiField(canvas, {
63
+ source: '/images/statue.jpg', fit: 'cover', focus: { x: 0.5, y: 0.35 },
64
+ charset: ' .:-=+*#%@', cellSize: 6, contrast: 1.25,
65
+ hover: 'trail', label: 'A marble statue drawn in characters',
66
+ }, '--ascii-ink-strong');
67
+ ```
68
+
69
+ ## hero-background: Hero background
70
+ Category: Backgrounds
71
+
72
+ Prompt:
73
+ “Put a calm ASCII background behind our hero with Asciify. Characters drift slowly, clear around the headline and buttons so they stay readable, and follow our light and dark theme.”
74
+
75
+ ```ts
76
+ const hero = asciiField(canvas, {
77
+ mark: 'glyph', scene: 'drift', charset: ' .:+*#', cellSize: 12,
78
+ clear: { element: heroText, padding: 24, feather: 0.32, shape: 'ellipse' },
79
+ hover: 'trail',
80
+ });
81
+ ```
82
+
83
+ ## cli-banner: CLI banner
84
+ Category: Text
85
+
86
+ Prompt:
87
+ “Greet people in ASCII when our CLI starts, using asciifyText from Asciify with [word].”
88
+
89
+ ```ts
90
+ import { asciifyText } from 'asciify-engine';
91
+
92
+ console.log(asciifyText('Hey', { char: '#' }));
93
+ ```
94
+
95
+ ## cursor-field: Cursor field
96
+ Category: Backgrounds
97
+
98
+ Prompt:
99
+ “Add a halftone dot field to [section] with Asciify that drifts, clears around the content, and answers the cursor with [trail, fluid or lantern].”
100
+
101
+ ```ts
102
+ const dots = asciiField(canvas, {
103
+ mark: 'dot', scene: 'drift', dotScale: 0.72, cellSize: 8,
104
+ clear: { element: content, padding: 40, shape: 'rect' },
105
+ hover: 'fluid',
106
+ });
107
+ ```
108
+
109
+ ## wordmark: Brand wordmark
110
+ Category: Text
111
+
112
+ Prompt:
113
+ “Draw our name, [name], in moving characters with Asciify for [the footer or a hero]. The word stays crisp, characters trail under the cursor, and the real name stays in HTML for screen readers.”
114
+
115
+ ```ts
116
+ const word = document.createElement('canvas');
117
+ word.width = 1800; word.height = 480;
118
+ const ctx = word.getContext('2d')!;
119
+ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, word.width, word.height);
120
+ ctx.fillStyle = '#fff'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
121
+ ctx.font = '600 400px Georgia, serif';
122
+ ctx.fillText('asciify', word.width / 2, word.height / 2 + 20);
123
+
124
+ const wordmark = asciiField(canvas, {
125
+ source: word, fit: 'contain', charset: ' .:-=+*#%@', cellSize: 9,
126
+ hover: 'trail', label: 'asciify, drawn in characters',
127
+ }, '--ascii-ink-strong');
128
+ ```
129
+
130
+ ## video: Video in characters
131
+ Category: Media
132
+
133
+ Prompt:
134
+ “Play [video] in characters with Asciify as [a section background or a card]. Muted, looping, paused off screen, with a play and pause control, and the poster for reduced motion.”
135
+
136
+ ```ts
137
+ const video = asciiField(canvas, {
138
+ source: '/media/magnolia.mp4', fit: 'cover', charset: ' .:-=+*#%@', cellSize: 7,
139
+ contrast: 1.2, hover: 'trail', label: 'A magnolia opening, drawn in characters',
140
+ }, '--ascii-ink-strong');
141
+
142
+ let paused = false;
143
+ playButton.addEventListener('click', () => {
144
+ paused = !paused;
145
+ if (paused) video?.field.pause(); else video?.field.play();
146
+ });
147
+ ```
148
+
149
+ ## not-found: 404 page
150
+ Category: Pages
151
+
152
+ Prompt:
153
+ “Make our 404 page with Asciify: a calm field of characters that clears around the message, trails under the cursor, and pulses when someone presses Go home.”
154
+
155
+ ```ts
156
+ const lost = asciiField(canvas, {
157
+ mark: 'glyph', scene: 'drift', charset: ' .:+*#', cellSize: 12, hover: 'trail',
158
+ clear: { element: message, padding: 28, feather: 0.3, shape: 'ellipse' },
159
+ });
160
+
161
+ homeLink.addEventListener('click', () => lost?.field.pulse({ element: homeLink }));
162
+ ```
163
+
164
+ ## avatar: ASCII avatar
165
+ Category: Components
166
+
167
+ Prompt:
168
+ “Draw [person]'s avatar in characters with Asciify, inside our round avatar frame. Characters trail under the cursor, and the name, role and follow button stay as HTML.”
169
+
170
+ ```ts
171
+ const avatar = asciiField(canvas, {
172
+ source: '/images/portrait.jpg', fit: 'cover',
173
+ charset: ' .:-=+*#%@', cellSize: 5, contrast: 1.1,
174
+ hover: 'trail', label: 'Portrait of Elias Varga',
175
+ }, '--ascii-ink-strong');
176
+ ```
177
+
178
+ ## empty-state: Empty state
179
+ Category: Components
180
+
181
+ Prompt:
182
+ “Give our empty state a quiet ASCII field with Asciify. Keep the icon, message and primary action readable, and send a pulse through the field from the button when it is pressed.”
183
+
184
+ ```ts
185
+ const empty = asciiField(canvas, {
186
+ scene: 'flow', charset: ' .:-=+*#', cellSize: 11, hover: 'none',
187
+ clear: { element: emptyMessage, padding: 24, shape: 'rect' },
188
+ });
189
+
190
+ primaryButton.addEventListener('click', () => empty?.field.pulse({ element: primaryButton }));
191
+ ```
192
+
193
+ ## loading: Loading state
194
+ Category: Components
195
+
196
+ Prompt:
197
+ “Show [upload or sync] progress with Asciify: a small character field above the file name and progress bar that sends a soft pulse each second while it runs, and stops when it settles.”
198
+
199
+ ```ts
200
+ const loading = asciiField(canvas, { mark: 'glyph', scene: 'drift', charset: ' .:+*#', cellSize: 9, hover: 'none' });
201
+ const beat = setInterval(() => loading?.field.pulse({ strength: 0.8 }), 1000);
202
+ request.finally(() => clearInterval(beat));
203
+ ```
204
+
205
+ ## copy-pulse: Copy feedback
206
+ Category: Components
207
+
208
+ Prompt:
209
+ “When someone copies [the coupon code], answer with Asciify: a pulse travels through a halftone field from the copy button.”
210
+
211
+ ```ts
212
+ const ground = asciiField(canvas, { mark: 'dot', scene: 'drift', cellSize: 7, dotScale: 0.8, hover: 'none' });
213
+
214
+ copyButton.addEventListener('click', async () => {
215
+ await navigator.clipboard.writeText(code);
216
+ ground?.field.pulse({ element: copyButton });
217
+ });
218
+ ```
219
+
220
+ ## brand-field: Brand color field
221
+ Category: Backgrounds
222
+
223
+ Prompt:
224
+ “Make an ASCII field in our brand color with Asciify, drifting slower than the default, for [section].”
225
+
226
+ ```ts
227
+ const brand = asciiField(canvas, {
228
+ mark: 'glyph', scene: 'drift', charset: ' .:+*#', cellSize: 10,
229
+ speed: 0.5, hover: 'none',
230
+ }, '--brand');
231
+ ```
232
+
233
+ ## coming-soon: Coming soon page
234
+ Category: Pages
235
+
236
+ Prompt:
237
+ “Make a coming-soon page for [product] with Asciify: our mark and name, one headline, and an email field with a Join waitlist button. A drifting field of characters clears around the form, and pulses from the button when someone joins.”
238
+
239
+ ```ts
240
+ const waitlist = asciiField(canvas, {
241
+ mark: 'glyph', scene: 'drift', charset: ' .:+*#', cellSize: 10, hover: 'trail',
242
+ clear: { element: form, padding: 14, feather: 0.24, shape: 'ellipse' },
243
+ });
244
+
245
+ form.addEventListener('submit', event => {
246
+ event.preventDefault();
247
+ waitlist?.field.pulse({ element: joinButton });
248
+ form.dataset.state = 'joined';
249
+ });
250
+ ```
251
+
252
+ ## sign-in: Sign-in page
253
+ Category: Pages
254
+
255
+ Prompt:
256
+ “Make a split sign-in page with Asciify: [image] in characters on the left, trailing under the cursor, and an email field, Continue and a quiet Continue with GitHub on the right. Continue shows a loading state and pulses the art.”
257
+
258
+ ```ts
259
+ const art = asciiField(artCanvas, {
260
+ source: '/images/wing.jpg', fit: 'cover', focus: { x: 0.5, y: 0.4 },
261
+ charset: ' .:-=+*#%@', cellSize: 6, contrast: 1.3,
262
+ hover: 'trail', label: 'A carved wing drawn in characters',
263
+ }, '--ascii-ink-strong');
264
+
265
+ signInForm.addEventListener('submit', event => {
266
+ event.preventDefault();
267
+ continueButton.setAttribute('aria-busy', 'true');
268
+ art?.field.pulse({ element: continueButton });
269
+ });
270
+ ```
271
+
272
+ ## pricing: Pricing highlight
273
+ Category: Pages
274
+
275
+ Prompt:
276
+ “Make a two-plan pricing section with a monthly and yearly toggle, using Asciify. The recommended plan, [plan], gets a halftone field that lights up under the cursor and pulses from its button.”
277
+
278
+ ```ts
279
+ const featured = asciiField(canvas, {
280
+ mark: 'dot', scene: 'drift', cellSize: 7, dotScale: 0.86, speed: 0.7,
281
+ interactionTarget: featuredCard, hover: 'lantern',
282
+ lantern: { radius: 0.3, strength: 0.75 },
283
+ });
284
+
285
+ trialButton.addEventListener('click', () => featured?.field.pulse({ element: trialButton }));
286
+ ```
287
+
288
+ ## text-decode: Decode headline
289
+ Category: Text
290
+
291
+ Prompt:
292
+ “Make [our hero headline] decode into place through ASCII characters when it scrolls into view and on hover, left to right in about 900ms, without moving the layout. Screen readers get the real words, and reduced motion shows them at once.”
293
+
294
+ ```ts
295
+ const words = headline.textContent ?? '', ramp = '.:-=+*#%@abcdefghijklmnopqrstuvwxyz', cells = [...words].map(ch => Object.assign(document.createElement('span'), { textContent: ch }));
296
+ const layer = Object.assign(document.createElement('span'), { ariaHidden: 'true', style: 'position:absolute;inset:0;pointer-events:none;color:var(--ink-3)' });
297
+ headline.setAttribute('aria-label', words);
298
+ headline.replaceChildren(layer, ...cells); // the headline is position: relative
299
+
300
+ function decode(duration = 900, hold = 380) {
301
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches || layer.childElementCount) return;
302
+ const stagger = (duration - hold) / Math.max(1, cells.length - 1), start = performance.now();
303
+ const glyphs = cells.map(({ offsetLeft: x, offsetTop: y, offsetWidth: w }) => layer.appendChild(Object.assign(document.createElement('span'), { style: `position:absolute;left:${x}px;top:${y}px;width:${w}px;display:flex;justify-content:center;line-height:normal` })));
304
+ requestAnimationFrame(function frame(now) {
305
+ cells.forEach((cell, i) => {
306
+ const t = (now - start - i * stagger) / hold, busy = t >= 0 && t < 1 && words[i] !== ' ';
307
+ cell.style.color = busy ? 'transparent' : '';
308
+ if (!busy || Math.random() < 0.4) glyphs[i].textContent = busy ? ramp[Math.floor(Math.random() * (t < 0.56 ? 9 : ramp.length))] : '';
309
+ });
310
+ if (now - start < duration) requestAnimationFrame(frame); else layer.replaceChildren();
311
+ });
312
+ }
313
+ new IntersectionObserver(([entry]) => { if (entry.isIntersecting) decode(); }, { threshold: 0.5 }).observe(headline);
314
+ headline.addEventListener('pointerenter', () => decode());
315
+ ```
316
+
317
+ ## image-reveal: Photo from characters
318
+ Category: Transitions
319
+
320
+ Prompt:
321
+ “Show [photo] drawn in characters with Asciify. On hover or focus the real photo grows out of the characters in a soft circle from the cursor, and on leave it closes back with a pulse where the pointer left.”
322
+
323
+ ```ts
324
+ const art = asciiField(canvas, {
325
+ source: '/images/egret.jpg', fit: 'cover', charset: ' .:-=+*#%@', cellSize: 6,
326
+ contrast: 1.3, hover: 'trail', label: 'A great egret, drawn in characters',
327
+ }, '--ascii-ink-strong');
328
+
329
+ // The photo lies over the canvas, masked by radial-gradient(circle at var(--x) var(--y), #000 calc(var(--r) - 56px), transparent var(--r)),
330
+ // with --r registered as a <length> through @property so it can transition.
331
+ function reveal(open: boolean, x = frame.clientWidth / 2, y = frame.clientHeight / 2) {
332
+ const ms = matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : open ? 520 : 440;
333
+ if (open) photo.style.cssText += `--x:${x}px;--y:${y}px`;
334
+ else art?.field.pulse({ x, y });
335
+ photo.style.transition = `--r ${ms}ms cubic-bezier(.16, 1, .3, 1)`;
336
+ photo.style.setProperty('--r', open ? `${Math.hypot(frame.clientWidth, frame.clientHeight) + 56}px` : '0px');
337
+ }
338
+ frame.addEventListener('pointerenter', event => reveal(true, event.offsetX, event.offsetY));
339
+ frame.addEventListener('pointerleave', event => reveal(false, event.offsetX, event.offsetY));
340
+ frame.addEventListener('focus', () => reveal(true));
341
+ frame.addEventListener('blur', () => reveal(false));
342
+ ```
343
+
344
+ ## route-transition: Page transition
345
+ Category: Transitions
346
+
347
+ Prompt:
348
+ “When someone switches [tabs or pages] in [our app], sweep a dense Asciify character field out from the clicked tab, swap the page underneath, and sweep it away, all in about half a second. Swap instantly under reduced motion.”
349
+
350
+ ```ts
351
+ const veil = asciiField(canvas, {
352
+ mark: 'glyph', scene: 'flow', charset: ' .:-=+*#%@', cellSize: 10, scale: 2.4,
353
+ speed: 1.6, contrast: 1.4, brightness: 0.34, hover: 'none',
354
+ });
355
+ veil?.field.pause();
356
+
357
+ // cover holds the canvas on background: var(--surface), visibility: hidden at rest, masked by radial-gradient(circle at var(--x) var(--y),
358
+ // transparent var(--hole), #000 calc(var(--hole) + 36px), #000 var(--front), transparent calc(var(--front) + 36px)); --hole and --front are @property lengths.
359
+ async function navigate(tab: HTMLElement, swap: () => void) {
360
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches) return swap();
361
+ const box = cover.getBoundingClientRect(), t = tab.getBoundingClientRect(), ease = 'cubic-bezier(.16, 1, .3, 1)';
362
+ const x = t.left + t.width / 2 - box.left, y = t.top + t.height / 2 - box.top, reach = `${Math.hypot(box.width, box.height) + 36}px`;
363
+ cover.style.cssText += `--x:${x}px;--y:${y}px;visibility:visible`;
364
+ veil?.field.play();
365
+ veil?.field.pulse({ x, y });
366
+ await cover.animate({ '--hole': ['-72px', '-72px'], '--front': ['-36px', reach] }, { duration: 180, easing: ease, fill: 'forwards' }).finished;
367
+ swap();
368
+ await cover.animate({ '--hole': ['-72px', reach], '--front': [reach, reach] }, { duration: 320, easing: ease }).finished;
369
+ cover.style.visibility = 'hidden';
370
+ veil?.field.pause();
371
+ }
372
+ ```
373
+
374
+ ## scramble-button: Scramble button
375
+ Category: Transitions
376
+
377
+ Prompt:
378
+ “Make [our primary button]'s label scramble through ASCII characters on hover and focus without changing width, with a small Asciify dot halo behind it that follows the cursor and pulses on press.”
379
+
380
+ ```ts
381
+ const halo = asciiField(canvas, {
382
+ mark: 'dot', scene: 'drift', cellSize: 6, dotScale: 0.8, scale: 2.2, hover: 'lantern',
383
+ clear: { element: button, padding: 4, feather: 0.12, shape: 'rect' },
384
+ });
385
+ button.addEventListener('click', () => halo?.field.pulse({ element: button }));
386
+
387
+ const text = label.textContent ?? '', ramp = '.:-=+*#%@abcdefghijklmnopqrstuvwxyz';
388
+ const live = Object.assign(document.createElement('span'), { ariaHidden: 'true', textContent: text, style: 'position:absolute;inset:0;white-space:nowrap' });
389
+ button.setAttribute('aria-label', text);
390
+ label.replaceChildren(Object.assign(document.createElement('span'), { textContent: text, style: 'visibility:hidden;white-space:nowrap' }), live); // label is position: relative; the hidden copy holds the width
391
+ function scramble() {
392
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
393
+ const start = performance.now();
394
+ requestAnimationFrame(function frame(now) {
395
+ const settled = Math.floor((now - start - 120) / 28);
396
+ live.textContent = [...text].map((ch, i) => (i < settled || ch === ' ' ? ch : ramp[Math.floor(Math.random() * ramp.length)])).join('');
397
+ if (settled < text.length) requestAnimationFrame(frame);
398
+ });
399
+ }
400
+ button.addEventListener('pointerenter', scramble);
401
+ button.addEventListener('focus', () => { if (button.matches(':focus-visible')) scramble(); });
402
+ ```
403
+
404
+ ## thinking: Thinking state
405
+ Category: AI
406
+
407
+ Prompt:
408
+ “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 as the dots fade. Regenerate runs it again.”
409
+
410
+ ```ts
411
+ import { play } from 'cuelume';
412
+
413
+ const dots = asciiField(canvas, { mark: 'dot', scene: 'drift', cellSize: 5, dotScale: 0.86, scale: 3, speed: 2.4, hover: 'none' });
414
+
415
+ // The canvas fills the answer area under line-shaped mask bands; CSS fades it out, and each .word in 38ms apart, once data-phase leaves "thinking".
416
+ async function regenerate(ask: () => Promise<string>) {
417
+ play('loading', { emphasis: 'subtle' });
418
+ reply.dataset.phase = 'thinking';
419
+ dots?.field.play();
420
+ const beat = setInterval(() => dots?.field.pulse({ x: Math.random() * canvas.clientWidth, y: canvas.clientHeight / 2, strength: 0.7 }), 640);
421
+ const answer = await ask().finally(() => clearInterval(beat));
422
+ answerText.replaceChildren(...answer.split(' ').map((word, i) => {
423
+ const span = Object.assign(document.createElement('span'), { className: 'word', textContent: `${word} ` });
424
+ span.style.setProperty('--i', String(i));
425
+ return span;
426
+ }));
427
+ reply.dataset.phase = 'done';
428
+ setTimeout(() => dots?.field.pause(), 420);
429
+ play('ready', { emphasis: 'subtle' });
430
+ }
431
+ ```
432
+
433
+ ## image-generation: Image generation
434
+ Category: AI
435
+
436
+ Prompt:
437
+ “While [our image model] generates [prompt], fill the frame with drifting Asciify characters that pulse beside a progress label. When the image arrives, develop it in characters, then fade to the photo from blur to sharp.”
438
+
439
+ ```ts
440
+ const art = asciiField(canvas, {
441
+ mark: 'glyph', scene: 'flow', charset: ' .:-=+*#%@', cellSize: 7, scale: 2.6, speed: 1.4, contrast: 1.3, hover: 'trail',
442
+ }, '--ascii-ink-strong');
443
+ const ease = (ms: number, step: (k: number) => void) => new Promise<void>(done => {
444
+ const start = performance.now(), still = matchMedia('(prefers-reduced-motion: reduce)').matches;
445
+ requestAnimationFrame(function frame(now) { const t = still ? 1 : Math.min(1, (now - start) / ms); step(1 - (1 - t) ** 4); if (t < 1) requestAnimationFrame(frame); else done(); });
446
+ });
447
+
448
+ generate.addEventListener('click', async () => {
449
+ const field = art?.field;
450
+ if (!field) return;
451
+ const beat = setInterval(() => field.pulse({ strength: 0.8 }), 640);
452
+ const url = await createImage(promptInput.value).finally(() => clearInterval(beat));
453
+ await ease(200, k => field.update({ brightness: -k }));
454
+ await field.setSource(url);
455
+ field.pulse();
456
+ await ease(900, k => field.update({ brightness: k - 1 }));
457
+ photo.src = url;
458
+ imageFrame.dataset.done = ''; // CSS fades the photo in from opacity 0 and blur(14px), 760ms on cubic-bezier(.16, 1, .3, 1)
459
+ });
460
+ ```
@@ -37,6 +37,7 @@ Never introduce a new brand color, a black canvas on a light site, or a second a
37
37
  | --- | --- | --- |
38
38
  | A photo, video or GIF converted on their page, with optional hover | `asciify`, `asciifyVideo`, `asciifyGif` (+ `mountHover` from `/hover`) | Lightest path. Disable core hover and motion when `/hover` owns them. |
39
39
  | A full-bleed hero or brand field that must feel smooth under the cursor, their name or word as the characters, an Unseen-style look | `mountGlyphField` from `/field` | WebGL2 on a GPU, 120 fps class. Glyph ramps only. Keep a poster and a fallback. See [glyph-field.md](glyph-field.md). |
40
+ | A calm background for a product page, docs, pricing or SaaS hero, in light or dark | `mountGlyphField` with `mark: 'dot'`, `scene: 'drift'` and `clear` around the copy | The halftone. Muted ink, transparent background, `pulse()` on real events. See [glyph-field.md](glyph-field.md#halftone-for-interfaces-412). |
40
41
  | Art styles (halftone, dither, braille, print, mosaic, LED…), saved looks, backdrops, masks, effects, PNG/MP4/GIF export | `mountStudio` from `/studio` | Studio owns motion, hover and finish. Start from `studioLook(id)` when one of the 12 looks matches the mood. |
41
42
  | Several media and text layers in one artwork | `mountStudioProject` from `/studio` | One loop and budget for up to eight layers. See [layered-projects.md](layered-projects.md). |
42
43
  | An ambient animated background with no media (rain, stars, waves, aurora…) | A background template from asciify.org/backgrounds | Downloaded files, not npm exports. Or a `/field` generated scene (`flow`, `orb`) for a GPU field. |
@@ -54,6 +55,7 @@ Never introduce a new brand color, a black canvas on a light site, or a second a
54
55
  | Techy, data, futuristic | Braille, blocks, dots, LED, hex | Source color with saturation 1.1–1.3, or a cool brand ink | 5–9px | Silk, Contour, Lens; field with `orb` or `flow` |
55
56
  | Playful | Emoji or custom glyphs, mosaic, bricks, voxels | Source color | 9–14px | Elastic, Scatter, Ripple rings |
56
57
  | Brand, identity, "our name in the art" | The brand word after a quiet ramp: `' .,:-+' + WORD` | One ink (`color: 'ink'`, `shade: 1`) so density and brightness carry the image | 10–14px; `cellAspect` 0.75–0.85 for display fonts | Field `hover: 'fluid'` (Unseen-like) or `'lantern'` (quieter); video or generated scene underneath |
58
+ | Product UI, SaaS, docs | Halftone dots (`mark: 'dot'`) | Muted text token, never the accent; light and dark both | 12–16px dot spacing, `dotScale` ~0.6, `clear` around the copy | Lantern or none; `pulse()` on saves and messages |
57
59
  | Behind body text | Light ramp, no dense glyphs | Ink at low contrast against the background; opacity 0.15–0.35 | 10–14px | Ambient only, or none; never strong refraction behind reading text |
58
60
 
59
61
  Hover choice by intent:
@@ -32,7 +32,10 @@ Size the canvas with CSS (for example `position:absolute; inset:0; width:100%; h
32
32
  | Option | Default | Notes |
33
33
  | --- | --- | --- |
34
34
  | `source` | none | URL (`.mp4`, `.webm`, `.mov` load as muted, looping, inline video), `HTMLImageElement`, `HTMLVideoElement`, `HTMLCanvasElement` or `ImageBitmap`. Cross-origin media needs CORS. |
35
- | `scene` | `'flow'` | Generated motion when there is no source: `'flow'` (domain-warped noise), `'orb'` (lit sphere) or `'none'` (blank, wake only). |
35
+ | `scene` | `'flow'` | Generated motion when there is no source: `'flow'` (domain-warped noise), `'drift'` (a few broad lobes, made for halftone, 4.12), `'orb'` (lit sphere) or `'none'` (blank, wake only). |
36
+ | `mark`, `dotScale` | `'glyph'`, `0.9` | `'dot'` draws a halftone: one circle per cell whose area follows tone, antialiased at any pixel ratio. `dotScale` is the largest dot as a fraction of the cell; about 0.6 keeps a calm, open grid. (4.12) |
37
+ | `scale` | `1` | Feature size of generated scenes; higher is finer. Use about 1.8 on phones, where the field is tall. (4.12) |
38
+ | `clear` | `false` | Keeps the field quiet around content so text stays readable: an element, `{ element, padding, feather, shape }` (followed through layout changes), or a zone in field fractions. (4.12) |
36
39
  | `charset` | `' .,:-=+*#%@'` | Ordered from empty to dense. Graphemes, so emoji, braille and block shading each take one cell. Up to 256. |
37
40
  | `cellSize`, `cellAspect` | `12`, `1` | Cell height in CSS px; width ÷ height. Monospaced ramps look best near `0.6`. Cells are whole device pixels, and glyphs are sampled texel for texel. |
38
41
  | `font`, `fontWeight`, `glyphScale` | monospace, `500`, `0.8` | Any loaded CSS font. The atlas is redrawn when the font finishes loading. |
@@ -56,6 +59,26 @@ Size the canvas with CSS (for example `position:absolute; inset:0; width:100%; h
56
59
 
57
60
  `update(options)` merges and rebuilds only what changed (the atlas for type changes, the simulation for resolution changes). `setSource(source)` swaps media. `pointer(x, y)` moves the wake in CSS pixels relative to the canvas (`pointer(null)` lifts it), which is useful for an intro stroke. `redraw()` draws immediately, so you can copy the canvas with `drawImage` in the same task, for example to cross-fade between looks.
58
61
 
62
+ ## Halftone for interfaces (4.12)
63
+
64
+ Product pages, docs, pricing and SaaS heroes want a background that is calm, works in light and dark themes, and never fights the copy. Use dots, the drift scene, a clear zone around the content, and `pulse()` for interface events:
65
+
66
+ ```ts
67
+ const field = mountGlyphField(heroCanvas, {
68
+ mark: 'dot', scene: 'drift', cellSize: 14, dotScale: 0.64,
69
+ color: 'ink', ink: tokens.textMuted, background: 'transparent',
70
+ clear: { element: heroCopy, padding: 40, feather: 0.18 },
71
+ hover: 'lantern',
72
+ });
73
+ saveButton.addEventListener('click', () => field.pulse({ element: saveButton }));
74
+ ```
75
+
76
+ - Ink: the muted text token, not the accent. On light grounds a mid grey such as `#9a988f`; on dark about `#5f5e58`.
77
+ - Clear zone: always set one when text sits over the field. Measure contrast at the busiest frame anyway.
78
+ - Pulse: one ring per meaningful event (saved, sent, connected), not on every click. It is skipped under reduced motion.
79
+ - `pulse({ x, y })` takes CSS pixels relative to the canvas; `pulse({ element })` starts at the element's centre; no argument starts at the centre.
80
+
81
+
59
82
  ## Living or still
60
83
 
61
84
  A photo with `drift: 0`, no video and a pointer that has left draws once and stops. Add `drift` (0.15–0.35) to let it breathe, use a video, or use a generated `scene` for continuous motion. Choose by the user's words and the context; [design-matching.md](design-matching.md#living-or-still) has the rules. Reduced motion stops drift and generated scenes; Lantern follows instantly and stops flickering.
@@ -0,0 +1,83 @@
1
+ # Asciify scenario prompts
2
+
3
+ Copy a prompt, replace bracketed inputs, and keep your own visual direction. These are starting points for common supported workflows, not a claim to cover every possible project. Read [project integration](project-integration.md) first. Inspect the installed package's types; never guess exports or option IDs.
4
+
5
+ ## Shared acceptance criteria
6
+
7
+ Preserve the existing framework, design and chosen media. Use one renderer owner per composition. Keep navigation and readable copy in HTML. Include loading/error states, responsive crop, reduced motion, offscreen/hidden pause and unmount cleanup. Test live motion, not only a screenshot. Report tested package version and remaining limitations. Do not publish, deploy or upgrade unrelated dependencies unless asked.
8
+
9
+ ## 1. Convert a photo
10
+
11
+ “Use Asciify to render [image] in [style] in this component. Preserve [subject/crop/colors]. Choose /core for simple ASCII or /studio for art styles. Include a still fallback and PNG export using supported APIs. Compare the output with the source at mobile and desktop sizes.”
12
+
13
+ ## 2. Interactive website hero
14
+
15
+ “Add a live Asciify hero using [image] with [Trail/desired interaction]. Keep [headline/CTA] as accessible HTML and respect our design. Use one /studio player, or /field if a GPU glyph field meets the brief. Keep input and render loops bounded; honor reduced motion. Do not stack /hover over /studio.”
16
+
17
+ ## 3. Custom words and characters
18
+
19
+ “Render [image] with [brand word/custom Unicode/block characters]. Distinguish a density ramp from repeated text. Offer the engine character catalog plus custom input, keep a leading space for shading when appropriate, and check the actual font can display each glyph. Preserve the subject and colors.”
20
+
21
+ ## 4. All styles and looks
22
+
23
+ “Create a selector from STUDIO_STYLES and the installed Studio preset catalog. Expose every supported style without a handwritten subset. Keep render style, character ramp, color look, dither algorithm, motion and hover as separate controls. Explain which controls apply to the active style.”
24
+
25
+ ## 5. Dither and print
26
+
27
+ “Render [source] as [dither/engraving/woodblock/risograph/etc.]. Use the genuine Studio style. For dither, use the installed algorithm catalog, compare blue noise and ordered patterns, and avoid shimmer on animation. Preserve readable midtones; compare light and dark backgrounds.”
28
+
29
+ ## 6. Ambient background
30
+
31
+ “Build [pattern] behind [content] using a template from asciify.org/backgrounds. Templates are local downloads, not npm exports. For any Studio style, use the downloaded studio-background.js helper and its STUDIO.md instructions. Keep HTML readable, preserve the chosen look, pause offscreen and destroy on unmount.”
32
+
33
+ The website's Studio background helper renders a continuous procedural source through /studio; it supports the site's 24 styles and 12 looks. Its original template mode uses its own character/dither controls. Never imply every control applies to every renderer.
34
+
35
+ ## 7. Video or GIF
36
+
37
+ “Convert [direct permitted media file] into [style]. Start with a bounded processing resolution and 24–30 fps, then measure. Include a poster, muted inline playback, autoplay-failure UI, pause offscreen and cleanup. Do not use a YouTube watch URL as a canvas source.”
38
+
39
+ ## 8. Scroll-controlled footage
40
+
41
+ “Connect [video] progress to [scroll region] using the installed media APIs. Keep seeking bounded, avoid duplicate playback clocks, handle metadata loading, and show a still for reduced motion. Keep page scrolling native and keyboard access intact.”
42
+
43
+ ## 9. Layered composition
44
+
45
+ “Compose [sources and placements] with mountStudioProject when supported. Keep one project clock and shared budgets, save source keys separately from media, and keep UI text in HTML. Export with exportStudioProject independently from the live preview.”
46
+
47
+ ## 10. Text artwork
48
+
49
+ “Turn [text] into decorative Asciify art with [font/style]. Keep an accessible HTML equivalent, wait for fonts before rasterizing, handle resize, and preserve readable surrounding content. Use Studio text settings supported by the installed version.”
50
+
51
+ ## 11. Export an animation
52
+
53
+ “Export [composition] as [GIF/MP4] at [dimensions/duration]. Inspect the installed export API, preserve settings and source framing, and keep export progress/cancellation separate from live playback. If recording canvas instead, negotiate a supported MediaRecorder MIME type and stop every track.”
54
+
55
+ ## 12. Framework integration
56
+
57
+ “Integrate this Asciify composition into [React/Vue/Svelte/plain JS]. Own the renderer in one lifecycle scope, update settings in place, handle async setup that resolves after unmount, and remove observers/listeners/media resources on cleanup. Test repeated mount/unmount.”
58
+
59
+ ## 13. Improve a trail
60
+
61
+ “Improve the wake on [component] with restrained follow-through and smooth reversals. Keep the glyph grid stable and input responsive. If adding a procedural pointer chain, bound distance, curvature and work per frame; reset on exit/teleport, stop when settled and honor reduced motion. Test fast turns, slow movement, re-entry, 30/60/120 Hz and touch scrolling. Do not invent an engine proceduralTrail option.”
62
+
63
+ The website uses the published renderer's native Trail. A chain-shaped tonal overlay was evaluated and removed because it obscured the artwork. Do not add it as part of a standard integration; procedural-chain experiments should be opt-in and visually verified.
64
+
65
+ ## 14. Match a reference
66
+
67
+ “Match the visual character of [reference] using [our source]. Identify crop, density, palette, negative space and motion before selecting settings. Preserve our branding. Preview at real sizes and explain any limitation instead of claiming an exact match.”
68
+
69
+ ## 15. Diagnose performance or a blank canvas
70
+
71
+ “Diagnose [symptom] before changing the look. Check source loading/CORS, canvas dimensions, installed API, GPU fallback, visibility and lifecycle. Measure on [device]. Reduce processing size/cell count before source FPS; do not claim FPS from screenshots. Keep HTML and poster visible on failure.”
72
+
73
+ ## 16. Upgrade an existing project
74
+
75
+ “Update only Asciify and its installed skill as needed for [feature]. Inspect current version, public exports and migration guidance; preserve the user's settings and dependency manager. Remove obsolete catalog imports, verify preview/export parity and report the tested version. Do not deploy or publish a package automatically.”
76
+
77
+ ## 17. Save and restore a look
78
+
79
+ “Save the active Studio settings as JSON and restore them through the installed normalizer. Store media references separately, keep defaults compatible with the installed version, and test malformed/older settings without losing the user's current work.”
80
+
81
+ ## 18. Accessible interaction
82
+
83
+ “Audit this Asciify component for keyboard access, touch scrolling, contrast, reduced motion and error states. Keep decorative canvases out of the reading order, provide text alternatives for meaningful artwork, and keep every CTA usable when rendering fails.”