asciify-engine 4.29.0 → 4.31.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/THIRD_PARTY.md +3 -0
  3. package/dist/field.cjs +1 -1
  4. package/dist/field.d.cts +1 -1
  5. package/dist/field.d.ts +1 -1
  6. package/dist/field.js +1 -1
  7. package/dist/{glyph-field-DRC-7uVf.d.ts → glyph-field-BCwGhohg.d.ts} +11 -0
  8. package/dist/{glyph-field-C2fQIlnq.d.cts → glyph-field-DKWHAMml.d.cts} +11 -0
  9. package/dist/kit/chunk-26HU7QJW.js +1 -0
  10. package/dist/kit/{chunk-4NV5APSJ.js → chunk-2NACMWVV.js} +1 -1
  11. package/dist/kit/chunk-2ZBPTNGK.js +1 -0
  12. package/dist/kit/{chunk-G4ZSMTFN.js → chunk-4NR7QENR.js} +1 -1
  13. package/dist/kit/{chunk-KY5KCIZE.js → chunk-62KEKWSM.js} +1 -1
  14. package/dist/kit/chunk-6646EUJY.js +1 -0
  15. package/dist/kit/{chunk-2FH6GGV6.js → chunk-6G2ZVIBS.js} +1 -1
  16. package/dist/kit/chunk-6YLMG37G.js +1 -0
  17. package/dist/kit/{chunk-FPCCX4FM.js → chunk-7DBDSJ5R.js} +1 -1
  18. package/dist/kit/chunk-7NS6YAD3.js +1 -0
  19. package/dist/kit/chunk-7OPCQDAL.js +1 -0
  20. package/dist/kit/{chunk-3OD5BMF5.js → chunk-AAV5QNHU.js} +1 -1
  21. package/dist/kit/chunk-AU33Y2SZ.js +1 -0
  22. package/dist/kit/{chunk-KCEYRIU4.js → chunk-BGPEFFKK.js} +1 -1
  23. package/dist/kit/{chunk-KDROJANO.js → chunk-BJJUDT26.js} +1 -1
  24. package/dist/kit/{chunk-2LBQ7QJT.js → chunk-BO4CPM26.js} +1 -1
  25. package/dist/kit/chunk-BQ53B2HP.js +1 -0
  26. package/dist/kit/chunk-BV6AOHFU.js +1 -0
  27. package/dist/kit/{chunk-QVICL57J.js → chunk-CH64MY3U.js} +1 -1
  28. package/dist/kit/{chunk-6G5MPND3.js → chunk-CIGEXHZ5.js} +1 -1
  29. package/dist/kit/{chunk-LVCGC3SO.js → chunk-CMGMCRAD.js} +1 -1
  30. package/dist/kit/{chunk-4U72YE65.js → chunk-CZL5WMU6.js} +1 -1
  31. package/dist/kit/chunk-DANNL7WR.js +1 -0
  32. package/dist/kit/{chunk-PB5E3D4E.js → chunk-DKRZSP5K.js} +1 -1
  33. package/dist/kit/{chunk-KFXMLIVF.js → chunk-E427W6P5.js} +1 -1
  34. package/dist/kit/{chunk-776H2BJ7.js → chunk-ECGALF3G.js} +1 -1
  35. package/dist/kit/chunk-EOTPXDMH.js +1 -0
  36. package/dist/kit/{chunk-K6JX64AS.js → chunk-FU4WW3PU.js} +1 -1
  37. package/dist/kit/chunk-GI5BNKBL.js +1 -0
  38. package/dist/kit/{chunk-Z2FRNFMB.js → chunk-HENGH3DE.js} +1 -1
  39. package/dist/kit/chunk-I377LEZA.js +1 -0
  40. package/dist/kit/chunk-IFZDWKIX.js +1 -0
  41. package/dist/kit/{chunk-3MHEDZLZ.js → chunk-J7TFGVZT.js} +1 -1
  42. package/dist/kit/{chunk-WDS7VM2E.js → chunk-J7Y2KBXK.js} +1 -1
  43. package/dist/kit/{chunk-KXI3T72F.js → chunk-JF3WEYIE.js} +1 -1
  44. package/dist/kit/{chunk-Z3KD6CLE.js → chunk-JSRXW3FL.js} +1 -1
  45. package/dist/kit/chunk-KO5NAI4S.js +1 -0
  46. package/dist/kit/chunk-LZGSWPH4.js +1 -0
  47. package/dist/kit/{chunk-ILXVBO7M.js → chunk-MT23DCGU.js} +1 -1
  48. package/dist/kit/{chunk-NF4WVWAC.js → chunk-MWUPCAET.js} +1 -1
  49. package/dist/kit/chunk-MZDZ6G75.js +1 -0
  50. package/dist/kit/{chunk-3YSK7V26.js → chunk-N7J4UAWY.js} +1 -1
  51. package/dist/kit/{chunk-YSN7EHSH.js → chunk-NP7UG7DS.js} +1 -1
  52. package/dist/kit/{chunk-ZCXNGJGB.js → chunk-NQ4PFWOG.js} +1 -1
  53. package/dist/kit/chunk-NSKX2OFA.js +1 -0
  54. package/dist/kit/{chunk-TRDA73HN.js → chunk-O25QXHEZ.js} +1 -1
  55. package/dist/kit/chunk-ODLKGAX2.js +1 -0
  56. package/dist/kit/chunk-P5M6AQQX.js +1 -0
  57. package/dist/kit/{chunk-W4U5CK6L.js → chunk-P5MVACML.js} +1 -1
  58. package/dist/kit/chunk-PZKQUZQK.js +1 -0
  59. package/dist/kit/{chunk-EKYEA7Z4.js → chunk-QRSVLQ2I.js} +1 -1
  60. package/dist/kit/{chunk-CGSWUO67.js → chunk-R3ALZFUQ.js} +1 -1
  61. package/dist/kit/{chunk-7DXSG526.js → chunk-RGV7VRYC.js} +1 -1
  62. package/dist/kit/chunk-RPSE33GI.js +1 -0
  63. package/dist/kit/chunk-RU2KQTTL.js +1 -0
  64. package/dist/kit/chunk-SBAUVRBY.js +1 -0
  65. package/dist/kit/chunk-SQNULNT3.js +1 -0
  66. package/dist/kit/{chunk-HYVTFKWX.js → chunk-TJHSBZBF.js} +1 -1
  67. package/dist/kit/{chunk-FWODCYYJ.js → chunk-UHLNVM63.js} +1 -1
  68. package/dist/kit/{chunk-UOYBFLGJ.js → chunk-UT6MJP5B.js} +1 -1
  69. package/dist/kit/{chunk-6WU247CB.js → chunk-UXDJSGED.js} +1 -1
  70. package/dist/kit/{chunk-GBNV3GXG.js → chunk-UZR3T5XA.js} +1 -1
  71. package/dist/kit/{chunk-FHN3PPSI.js → chunk-V3J6KJQ2.js} +1 -1
  72. package/dist/kit/{chunk-KLRF3S7O.js → chunk-V42Q4L2G.js} +1 -1
  73. package/dist/kit/{chunk-ZWZZMU5P.js → chunk-VQ4KIRBW.js} +1 -1
  74. package/dist/kit/chunk-VWFC5XOB.js +1 -0
  75. package/dist/kit/chunk-VXPUEFYR.js +1 -0
  76. package/dist/kit/{chunk-RXOPJJC4.js → chunk-W3GINZLK.js} +1 -1
  77. package/dist/kit/{chunk-F6F2C3RH.js → chunk-WOKKM6CG.js} +1 -1
  78. package/dist/kit/{chunk-OZFSNW5N.js → chunk-WPWCFCEZ.js} +1 -1
  79. package/dist/kit/{chunk-H5KHXZWR.js → chunk-WRBLIULG.js} +1 -1
  80. package/dist/kit/{chunk-SDSW7422.js → chunk-WSWIOD2C.js} +1 -1
  81. package/dist/kit/{chunk-NW4U2YRE.js → chunk-X6GAMLEH.js} +1 -1
  82. package/dist/kit/chunk-XB7XBYHA.js +1 -0
  83. package/dist/kit/{chunk-B3TY6UXJ.js → chunk-XDX6WLED.js} +1 -1
  84. package/dist/kit/chunk-XJBVU5QP.js +1 -0
  85. package/dist/kit/{chunk-42KMN727.js → chunk-XVA7UGUQ.js} +1 -1
  86. package/dist/kit/{chunk-DMRPBFLW.js → chunk-YFW377RN.js} +1 -1
  87. package/dist/kit/{chunk-CG6SGQMY.js → chunk-ZGCKUXYP.js} +1 -1
  88. package/dist/kit/{chunk-XUCYFWC3.js → chunk-ZTCSVEHT.js} +1 -1
  89. package/dist/kit.cjs +1 -1
  90. package/dist/kit.d.cts +235 -6
  91. package/dist/kit.d.ts +235 -6
  92. package/dist/kit.js +1 -1
  93. package/package.json +1 -1
  94. package/skills/asciify-engine/SKILL.md +4 -4
  95. package/skills/asciify-engine/references/applying-asciify.md +19 -2
  96. package/skills/asciify-engine/references/components.md +107 -0
  97. package/skills/asciify-engine/references/glyph-field.md +2 -0
  98. package/skills/asciify-engine/references/kit.md +165 -3
  99. package/skills/asciify-engine/references/scenario-prompts.md +17 -1
  100. package/dist/kit/chunk-2GAV4OAK.js +0 -1
  101. package/dist/kit/chunk-3LJ5C3U3.js +0 -1
  102. package/dist/kit/chunk-3VEMKDBW.js +0 -1
  103. package/dist/kit/chunk-7M2FFUZ3.js +0 -1
  104. package/dist/kit/chunk-AIGAJMNN.js +0 -1
  105. package/dist/kit/chunk-BFBG3VHI.js +0 -1
  106. package/dist/kit/chunk-E3FPFANJ.js +0 -1
  107. package/dist/kit/chunk-EEV6Y7NH.js +0 -1
  108. package/dist/kit/chunk-GF72XM4C.js +0 -1
  109. package/dist/kit/chunk-GKR3ROYR.js +0 -1
  110. package/dist/kit/chunk-I5R2347P.js +0 -1
  111. package/dist/kit/chunk-KEE55QSD.js +0 -1
  112. package/dist/kit/chunk-M5VVLYI2.js +0 -1
  113. package/dist/kit/chunk-ORSI57T2.js +0 -1
  114. package/dist/kit/chunk-PGFBIU7R.js +0 -1
  115. package/dist/kit/chunk-QSTDYL35.js +0 -1
  116. package/dist/kit/chunk-UVLKCAKH.js +0 -1
  117. package/dist/kit/chunk-VBWG7HQA.js +0 -1
  118. package/dist/kit/chunk-VV2KP7ZW.js +0 -1
  119. package/dist/kit/chunk-WI2AF2KE.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "asciify-engine",
3
- "version": "4.29.0",
3
+ "version": "4.31.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, artwork looks for wallpapers, posters and prints, art styles and export, hover and motion, layered typography and rendering performance.
4
4
  metadata:
5
- tested-engine: "4.29.0"
5
+ tested-engine: "4.31.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.29.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.31.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 · Looks: https://asciify.org/looks · Backgrounds: https://asciify.org/backgrounds
@@ -35,7 +35,7 @@ Most requests are a feeling ("make the loading state feel alive", "a calm backgr
35
35
 
36
36
  | The user wants | Use |
37
37
  | --- | --- |
38
- | A UI component in characters: button loading, status, toasts, tabs, pills, odometer, chart, AI thinking and streaming, transitions, sign-in, pricing, 404 | `asciify-engine/kit` ([applying-asciify.md](references/applying-asciify.md), [components.md](references/components.md), [kit.md](references/kit.md)) |
38
+ | A UI component in characters: button loading, status, toasts, tabs, pills, odometer, chart, AI thinking and streaming, transitions, sign-in, pricing, 404; event-site pieces (scrambling headers and headlines, morphing hatched heroes, dithered glows, a dotted globe, two-tone photos, assembling wordmarks, a spray cursor); an ASCII ocean of swimming creatures; interactive backgrounds visitors stir, grow and ripple | `asciify-engine/kit` ([applying-asciify.md](references/applying-asciify.md), [components.md](references/components.md), [kit.md](references/kit.md)) |
39
39
  | A background behind a hero, pricing, docs or a section, light or dark | Kit `fieldBackground(el, { preset: 'hero', clear })` (asciify.org's hero) or `background(el, { preset, clear })` with one of the 12 curated presets below |
40
40
  | Their word or brand in characters | Kit `wordmark`, or `background()` with `wordField` |
41
41
  | Their video in characters on the page | Kit `asciiVideo` |
@@ -49,7 +49,7 @@ Most requests are a feeling ("make the loading state feel alive", "a calm backgr
49
49
 
50
50
  ## The kit in one paragraph
51
51
 
52
- Each kit function mounts an `aria-hidden` field over or inside your element, follows its size and border radius, inks it from your theme in light and dark, pauses off screen, honours reduced motion, falls back without WebGL2 to an inert handle that leaves your HTML working, and cleans up on `destroy()`. components.md has 31 components as short snippets with the prompt a person gives, in categories Components, AI, Text, Transitions, Media, Pages and Backgrounds. In React, create the handle in an effect and return its `destroy`.
52
+ Each kit function mounts an `aria-hidden` field over or inside your element, follows its size and border radius, inks it from your theme in light and dark, pauses off screen, honours reduced motion, falls back without WebGL2 to an inert handle that leaves your HTML working, and cleans up on `destroy()`. components.md has 37 components as short snippets with the prompt a person gives, in categories Components, AI, Text, Transitions, Media, Pages and Backgrounds. In React, create the handle in an effect and return its `destroy`.
53
53
 
54
54
  ```ts
55
55
  import { beam } from 'asciify-engine/kit';
@@ -16,8 +16,10 @@ Pick by what the field does for the person, not by how it looks.
16
16
  | Move between states or views | `tabs`, `pageDissolve`, `commandPalette`, `toastStack`, `disintegrate` |
17
17
  | Answer a gesture | `likeBurst`, `swipeDeck`, `compare`, `accordion`, `dropzone` |
18
18
  | Follow the pointer across the page | `cursorTrail` (one per page, off on touch and under reduced motion) |
19
- | Carry a brand moment or media | `decode`, `wordmark`, `asciiVideo`, `artPanel`, `pricingGlow`, `fieldBackground` `notFound` |
20
- | Set the page's one ground | `fieldBackground` `hero`, or `background()` with a preset (section 9) |
19
+ | Carry a brand moment or media | `decode` (and its `scramble`), `wordmark` (and its `assemble`), `morphField`, `ditherImage`, `asciiVideo`, `artPanel`, `pricingGlow`, `fieldBackground` `notFound` |
20
+ | Show where: places, reach, a network | `globe` with `points` |
21
+ | A playful, living hero ground | `ocean` (fish, sharks, mantas, jellyfish and schools that answer the cursor), or `background()` with `estuary`, `capillary` or `shockwave` for a ground visitors stir, grow and ripple |
22
+ | Set the page's one ground | `fieldBackground` `hero`, `ditherGradient`, or `background()` with a preset (section 9) |
21
23
 
22
24
  Do not use characters:
23
25
 
@@ -219,6 +221,21 @@ const bg = background(hero, { preset: drift, clear: heroCopy, poster: '/stills/d
219
221
  signUpButton.addEventListener('click', () => bg.field?.pulse({ element: signUpButton, strength: 1.2 })); // GPU presets expose the field
220
222
  ```
221
223
 
224
+ ## 9b. Event and launch pages
225
+
226
+ A conference, launch or campaign page can carry more character than a product page, but the same rules hold: one key visual a section, real HTML for every headline and link, and motion that answers the visitor.
227
+
228
+ | Section | Piece | Keep it |
229
+ | --- | --- | --- |
230
+ | Header | `decode(link, { scramble: true, trigger: 'manual', settle: 'text' })` on each nav link | Links rest as plain HTML and only scramble on hover; `cellSize` about 3 so letters stay crisp. |
231
+ | Hero | `morphField` of your own frames (`mark: 'engraving'` lines, white ink on the brand ground) and `decode(h1, { scramble: true, settle: 'text' })` | One morph per hero; a counter or caption through `onChange`; a poster per theme. |
232
+ | Ambient pointer | `cursorTrail({ style: 'spray', readout: true, zIndex: -1 })` | A background hover: behind cards, off on touch and under reduced motion. |
233
+ | Proof sections | `globe` for venues or reach, `ditherImage` for photos in the brand's two tones, `background(el, { preset: wordField, look: { glyphOrder: 'text', … } })` for repeated words | `globe` and `wordField` are section grounds; keep copy off their middle or `clear` it. |
234
+ | Call to action | `ditherGradient` on the brand-coloured panel | Light from an edge, the button over the quiet part. |
235
+ | Footer | `wordmark(mark, { assemble: 'scroll', look: { hover: 'none' } })` | The page trail draws over it; nothing else moves in the footer. |
236
+
237
+ Brand colour as the ground: give the section the brand background and draw in white or a light tint of it (`ink: '#fff'` or a token); on a white section draw in the brand token. Check contrast of the HTML over the busiest frame of the morph.
238
+
222
239
  ## 10. Checklist before you finish
223
240
 
224
241
  - [ ] Every field has a job from section 1; hiding it loses feedback, or it is the page's one ground.
@@ -478,6 +478,113 @@ const v = asciiVideo(player, { src: '/media/magnolia.mp4', poster: '/media/magno
478
478
  playButton.onclick = () => playButton.setAttribute('aria-label', v.toggle() ? 'Pause' : 'Play');
479
479
  ```
480
480
 
481
+ ## nav-scramble: Scrambling links
482
+ Category: Text
483
+
484
+ Prompt:
485
+ “Make [our header's links] scramble on hover with Asciify: the link's letters flip through other alphabets (kana, Hangul, accented Latin, Greek) and land back on the word, drawn in characters in the link's own font, then hand back to the plain HTML link.”
486
+
487
+ ```ts
488
+ import { decode } from 'asciify-engine/kit';
489
+
490
+ // manual: each link rests as plain HTML and only scrambles when the pointer enters it
491
+ const links = [...nav.querySelectorAll<HTMLElement>('a')].map(a => decode(a, { scramble: true, trigger: 'manual', settle: 'text', duration: 700, look: { cellSize: 3 } }));
492
+ ```
493
+
494
+ ## morph-hero: Morphing hero
495
+ Category: Pages
496
+
497
+ Prompt:
498
+ “Build [our event hero] with Asciify: on our brand-coloured ground, [our 3–5 images or drawn scenes] morph into one another as diagonal hatched lines whose weight follows the tone, a small counter shows which frame is up, and the headline flips through alphabets before landing on the real words. Keep the headline and buttons in HTML.”
499
+
500
+ ```ts
501
+ import { morphField, decode } from 'asciify-engine/kit';
502
+
503
+ // frames: image URLs, canvases, videos, or (pen, w, h, now) => void drawings in white on black
504
+ const hero = morphField(heroSection, { sources: frames, ink: '#ffffff', hold: 3200, transition: 1000, onChange: i => (counter.textContent = `${i + 1}/${frames.length}`) });
505
+ const title = decode(heroTitle, { scramble: true, settle: 'text' });
506
+ nextButton.onclick = () => hero.next();
507
+ ```
508
+
509
+ ## stir-grow-ripple: Interactive hero background
510
+ Category: Backgrounds
511
+
512
+ Prompt:
513
+ “Put an interactive ASCII background behind [our dark hero] with Asciify that visitors can stir, grow and ripple, with a small pill switcher: Estuary (soft blotches the cursor stirs), Capillary (thin veins that grow toward the cursor and burst from a click) and Shockwave (a click sends rings of characters outward). Faint by default so the headline reads, in our text token's ink, still under reduced motion.”
514
+
515
+ ```ts
516
+ import { background, capillary, estuary, shockwave } from 'asciify-engine/kit';
517
+
518
+ const modes = { estuary, capillary, shockwave };
519
+ const bg = background(hero, { preset: estuary, clear: headline });
520
+ pills.forEach(pill => (pill.onclick = () => bg.setPreset(modes[pill.dataset.mode])));
521
+ ```
522
+
523
+ ## ascii-ocean: Ocean
524
+ Category: Backgrounds
525
+
526
+ Prompt:
527
+ “Put an ASCII ocean behind [our landing hero] with Asciify: [a shark, a hammerhead and a school of small fish, plus a jellyfish or two] seen from above, swimming with real tail beats, the school keeping together, far ones smaller and fainter. The cursor scatters the fish and the sharks turn to investigate; a click sends a ripple. Inked from our text token, behind the headline, still under reduced motion.”
528
+
529
+ ```ts
530
+ import { ocean } from 'asciify-engine/kit';
531
+
532
+ ocean(hero, { creatures: [{ kind: 'shark' }, { kind: 'hammerhead' }, { kind: 'school', count: 32 }, { kind: 'jellyfish', count: 2 }], label: 'Sharks and fish swimming' });
533
+ ```
534
+
535
+ ## ascii-globe: Globe of dots
536
+ Category: Media
537
+
538
+ Prompt:
539
+ “Show [where we are: our venues, regions or network] on a globe of dots with Asciify: land, a faint ocean, latitude lines and the far side showing through, turning slowly, draggable, with a ring spreading from each place. Buttons turn it to a region.”
540
+
541
+ ```ts
542
+ import { globe } from 'asciify-engine/kit';
543
+
544
+ const earth = globe(globeBox, { ink: '--brand', points: [{ lat: 37.8, lon: -122.4 }, { lat: 51.5, lon: -0.1 }, { lat: 1.35, lon: 103.8 }], label: 'Our three venues' });
545
+ regionButtons.forEach(b => (b.onclick = () => earth.rotateTo(Number(b.dataset.lon))));
546
+ ```
547
+
548
+ ## dither-glow: Dithered glow
549
+ Category: Backgrounds
550
+
551
+ Prompt:
552
+ “Light [our call-to-action panel] with Asciify: a soft glow rising from the bottom edge, drawn as a fine two-tone dither in [a light tint] on [our brand colour], drifting gently and leaning toward the cursor. The button and copy sit over the quiet part.”
553
+
554
+ ```ts
555
+ import { ditherGradient } from 'asciify-engine/kit';
556
+
557
+ // the panel has the brand background; the ink is the dither's light tone
558
+ const glow = ditherGradient(ctaPanel, { ink: '--brand-tint', lights: [{ x: 0.5, y: 1.08, radius: 0.62 }] });
559
+ ```
560
+
561
+ ## dither-image: Two-tone photo
562
+ Category: Media
563
+
564
+ Prompt:
565
+ “Draw [our photo] in our two brand tones with Asciify, as [dither / pixels / dots / engraving], developing in from its lightest marks as it scrolls into view. Keep the image's alt text.”
566
+
567
+ ```ts
568
+ import { ditherImage } from 'asciify-engine/kit';
569
+
570
+ // draws the <img> inside the figure (hidden while drawn; its alt names the canvas)
571
+ const photo = ditherImage(figure, { tones: ['--brand-deep', '--brand'], mark: 'dither', cellSize: 5 });
572
+ markPicker.onchange = () => photo.update({ mark: markPicker.value });
573
+ ```
574
+
575
+ ## assemble-wordmark: Assembling wordmark
576
+ Category: Text
577
+
578
+ Prompt:
579
+ “Close [our footer] on our name, [name], set large in characters that gather out of scattered pieces as the visitor scrolls down to it, and scatter again as they scroll away. Keep the name in HTML.”
580
+
581
+ ```ts
582
+ import { wordmark } from 'asciify-engine/kit';
583
+
584
+ // draws the element's own text; scroll ties the assembly to the footer's way into view
585
+ const mark = wordmark(footerName, { assemble: 'scroll', font: 'Georgia, "Times New Roman", serif', fontWeight: 600 });
586
+ ```
587
+
481
588
  ## hero-background: Background
482
589
  Category: Backgrounds
483
590
 
@@ -37,6 +37,8 @@ Size the canvas with CSS (for example `position:absolute; inset:0; width:100%; h
37
37
  | `scene` | `'flow'` | Generated motion when there is no source: `'flow'` (domain-warped noise), `'drift'` (a few broad lobes, made for halftone, 4.12), `'swell'` (three crossing sine swells rolling diagonally, made for a fine dot grid), `'orb'` (lit sphere), the kit's background scenes `'caustics'`, `'contour'`, `'aurora'`, `'ripple'`, `'halo'`, `'interference'`, `'mesh'`, `'cascade'` and `'constellation'` (`FIELD_SCENES` lists them all), or `'none'` (blank, wake only). |
38
38
  | `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) |
39
39
  | `mark` (Studio styles) | | `FIELD_MARKS` also holds the Studio's tile and print styles, drawn per cell in the composite pass from the cell's tone and colour (one shader branch each, no extra pass or texture). Tiles, which fade with tone in `color: 'ink'` and stay solid in `'source'`: `'pixel'` (filled cell), `'mosaic'` (gapped tile), `'lego'` (gapped brick lit from above, with a ringed stud), `'voxel'` (isometric cube, faces at 1, 0.72 and 0.42), `'disco'` (gapped tile, lit upper facet, shaded lower), `'hex'` (flat-topped hexagon), `'led'` (round lamp, soft halo, glint). `'dither'` lights a 4x4 Bayer screen of about five dots a cell. `'cmyk'` separates the cell's hue into offset cyan, magenta and yellow dots, with the neutral share as a key dot in the ink's own lightness. Print marks, whose size carries the tone: `'stipple'` (four jittered dots a cell), `'engraving'` (parallel cuts at -35°, running on across cells), `'crosshatch'` (two more directions join in darker tones), `'woodblock'` (broad carved bands in five steps, edges wavering), `'risograph'` (main ink and a coral second ink, offset), `'pointillism'` (four small strokes, their shade varied). Every mark but `'dot'` leaves blank the cells the `charset` leaves blank (its first glyph), so keep a ramp that starts with a space. Cells near square suit them best. |
40
+ | `markAngle` | `-35` | Screen angle in degrees for the line and print marks (`engraving`, `crosshatch`, `woodblock`, `risograph`), turning clockwise as CSS `rotate` does: 0 horizontal, 90 vertical, -45 rising to the right. (4.30) |
41
+ | `glyphOrder` | `'tone'` | `'text'` reads the charset after its blank in order along each row (each row a few letters on), so a phrase stays legible as text-as-art; the tone decides which cells show their letter, every one from 0.6, thinning below it through a 4x4 screen. (4.30) |
40
42
  | `scale` | `1` | Feature size of generated scenes; higher is finer. Use about 1.8 on phones, where the field is tall. (4.12) |
41
43
  | `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) |
42
44
  | `charset` | `' .,:-=+*#%@'` | Ordered from empty to dense. Graphemes, so emoji, braille and block shading each take one cell. Up to 256. |
@@ -51,7 +51,20 @@ const d = decode(document.querySelector('h1')!, { duration: 800 });
51
51
  d.replay();
52
52
  ```
53
53
 
54
- Options: `text` (draw this instead, centred and as large as fits, in the element's own font weight, family and letter spacing; `\n` breaks lines), `duration` (ms, default 900), `trigger` (`'visible' | 'manual'`), `replayOnHover` (default true; entry is read from pointer moves too, so a scripted cursor replays it), `settle` (`'glyphs'` keeps the headline in characters, `'text'` hands back to the crisp HTML). Ink defaults to the element's text colour. Method: `replay()`.
54
+ Options: `text` (draw this instead, centred and as large as fits, in the element's own font weight, family and letter spacing; `\n` breaks lines), `duration` (ms, default 800, 1400 with `scramble`), `trigger` (`'visible' | 'manual' | 'scroll'`), `replayOnHover` (default true; entry is read from pointer moves too, so a scripted cursor replays it), `settle` (`'glyphs'` keeps the headline in characters, `'text'` hands back to the crisp HTML), `scramble` (4.30: `true` or your own alphabets, below). Ink defaults to the element's text colour. Method: `replay()`.
55
+
56
+ **Scramble (4.30).** `scramble: true` flips the headline through other alphabets before it lands: each flip fills every unresolved letter from one alphabet (kana, Hangul, accented Latin, Cyrillic, Greek, then digits and signs: `SCRAMBLE_SCRIPTS`), drawn in the element's own font at that letter's own place, and letters lock left to right over the last 60% of the run. Pass `scramble: ['ABCDEF…', '0123…']` for your own alphabets (one string each). The field draws every flip, so it is characters all the way, and the real text stays in the DOM.
57
+
58
+ **Scroll (4.30).** `trigger: 'scroll'` ties the decode to scroll position: it resolves as the element rises into view (0 at the viewport's bottom edge, done half a viewport, or its own height, later) and undoes as it leaves. Scroll is the clock, so nothing draws while the page is still. With `scramble`, the flips step with scroll too.
59
+
60
+ ```ts
61
+ // A headline that flips through alphabets and lands on the real words, then hands back to the HTML.
62
+ decode(h1, { scramble: true, settle: 'text' });
63
+ // Header links that scramble on hover: manual, so they rest as plain HTML.
64
+ for (const a of nav.querySelectorAll('a')) decode(a, { scramble: true, trigger: 'manual', settle: 'text', look: { cellSize: 3 } });
65
+ // A section title that resolves with scroll.
66
+ decode(sectionTitle, { trigger: 'scroll', settle: 'text' });
67
+ ```
55
68
 
56
69
  ## odometer
57
70
 
@@ -229,7 +242,12 @@ const mark = wordmark(box, { text: 'asciify' });
229
242
  input.oninput = () => mark.setText(input.value);
230
243
  ```
231
244
 
232
- Options: `text`, `font` (default `Georgia, "Times New Roman", serif`), `fontWeight` (default 600), `artHeight` (height of the 1800 px wide canvas the word is set on, default 520; a shorter one sets the word larger in a wide element), `label` (the canvas's accessible name when the word is not the element's own text; default the word). Methods: `setText(text)`; getter `text`.
245
+ Options: `text`, `font` (default `Georgia, "Times New Roman", serif`), `fontWeight` (default 600), `artHeight` (height of the 1800 px wide canvas the word is set on, default 520; a shorter one sets the word larger in a wide element), `label` (the canvas's accessible name when the word is not the element's own text; default the word), `assemble` (4.30: `'visible'` gathers the word out of scattered pieces once as it scrolls into view, over `duration` ms, default 1400; `'scroll'` ties the gathering to scroll position and scatters it again as it leaves; off under reduced motion; default off). Methods: `setText(text)`; getter `text`.
246
+
247
+ ```ts
248
+ // A footer wordmark that assembles as the page reaches it, with the page trail drawing over it.
249
+ wordmark(footerMark, { text: 'acme', assemble: 'scroll', look: { cellSize: 7, hover: 'none' } });
250
+ ```
233
251
 
234
252
  The site footer closes on `wordmark(box, { text: 'asciify', font: '"Fraunces", Georgia, serif', fontWeight: 600, label: 'Asciify', look: { cellSize } })`, with `cellSize` the box width / 190 held between 4 and 8 and the box capped at 1320px wide, and its hero is `fieldBackground(section, { preset: 'hero', ripple: false, palette: { light: '#7d7b75', dark: '#77756f' }, look: { scale: 1.25, cellSize: 12, brightness: 0.24, contrast: 1, charset: ' .:+*#', cellAspect: 0.62, glyphScale: 0.86, clear: { element: copy, padding: -24, feather: 0.6, shape: 'ellipse' } } })` (`scale` 1.6 under 760px).
235
253
 
@@ -242,7 +260,26 @@ const cursor = cursorTrail({ style: 'trail', ink: '--ink', charset: ' .:-=+*#%@'
242
260
  picker.onchange = () => (picker.value === 'off' ? cursor.destroy() : cursor.update({ style: picker.value }));
243
261
  ```
244
262
 
245
- Options: `style` (`'trail'` a fine tapered tail, `'wake'` a softer, wider band, `'water'` rings that spread from the path, `'sparkle'` glints scattered along it; default `trail`), `charset` (default ` .:-=+*#%@`), `cellSize` (CSS px, default 9), `life` (ms a point takes to fade, default 800, `water` 950), `zIndex` (default 1000; keep it under your dialogs and sticky header), `skip` (a selector to stay quiet over, on top of inputs, textareas, selects, `[contenteditable]`, dialogs and `[data-ascii-cursor-skip]`), plus the shared `ink` (default `--ascii-ink`, then the body's text colour), `look`, `reducedMotion` and `globalLook` (default `false`: the trail keeps its own ramp when `setGlobalLook` re-skins the page). Methods: `update`, `clear()`, `pause()`, `play()`, `destroy()`; getter `active` (something is fading). It is off on touch-only devices (`hover: none` and `pointer: coarse`) and for touch pointers, under reduced motion and while the tab is hidden. Mount it once per page (in React, in the app shell's effect); skip pages that have their own pointer work, such as an editor, and mark heroes with their own hover `data-ascii-cursor-skip`.
263
+ Options: `style` (`'trail'` a fine tapered tail, `'wake'` a softer, wider band, `'water'` rings that spread from the path, `'sparkle'` glints scattered along it, `'spray'` (4.30) patches of diagonal hatching sprayed round the path, where a drag draws a hatched selection box; default `trail`), `readout` (4.30: small monospaced coordinate tags such as `412, 168` beside the newest spray marks and at a drag box's corners, in the ink with `--ascii-readout-text`, default white, on it; decoration, `aria-hidden`; default false), `charset` (default ` .:-=+*#%@`), `cellSize` (CSS px, default 9), `life` (ms a point takes to fade, default 800, `water` 950), `zIndex` (default 1000; keep it under your dialogs and sticky header), `skip` (a selector to stay quiet over, on top of inputs, textareas, selects, `[contenteditable]`, dialogs and `[data-ascii-cursor-skip]`), plus the shared `ink` (default `--ascii-ink`, then the body's text colour), `look`, `reducedMotion` and `globalLook` (default `false`: the trail keeps its own ramp when `setGlobalLook` re-skins the page). Methods: `update`, `clear()`, `pause()`, `play()`, `destroy()`; getter `active` (something is fading). It is off on touch-only devices (`hover: none` and `pointer: coarse`) and for touch pointers, under reduced motion and while the tab is hidden. Mount it once per page (in React, in the app shell's effect, or a layout that persists across routes) and skip pages that have their own pointer work, such as an editor. `trail: { strength, radius, span, cell }` sizes the `trail` style to match a field's own trail (same brush, so a wordmark's hover and the page's read as one).
264
+
265
+ **One continuous trail across the page.** A field's own hover stops at its canvas edge, so a page trail that skips every canvas feels boxed in: it breaks at each hero and wordmark and restarts inside it. For one trail that flows through everything:
266
+
267
+ ```ts
268
+ // Heroes, backgrounds and wordmarks: no hover of their own; the page trail draws over them.
269
+ const hero = fieldBackground(heroEl, { preset: 'hero', look: { hover: 'none' } });
270
+ const mark = wordmark(footerMark, { text: 'acme', look: { cellSize: 7, hover: 'none' } });
271
+ // One layer for the page. Skip only interactive components that keep their own hover.
272
+ const cursor = cursorTrail({ style: 'trail', ink: '--ink', zIndex: 40, skip: '.demo canvas' });
273
+ ```
274
+
275
+ Match `cellSize` (and `trail.cell`) to the biggest field it crosses, such as the footer wordmark, so the glyphs don't change size at its edge. Don't put `data-ascii-cursor-skip` on those fields: that's what brings the box back.
276
+
277
+ **It's a background hover: keep it behind the content.** Give it `zIndex: -1` so cards, panels, dialogs and text cover it and it shows only on the open page and through transparent fields (heroes, wordmarks). For a negative z-index layer to show, only `html` may paint the page colour: `body`, the app root and full-page wrappers (main, footer, page shells) must have no background of their own, or they cover it. Cards keep their surface colour, which is what hides the trail inside them.
278
+
279
+ ```css
280
+ html { background: var(--bg); }
281
+ body, #root, .page, footer { background: transparent; }
282
+ ```
246
283
 
247
284
  ### asciiVideo
248
285
 
@@ -488,6 +525,116 @@ pull.revealRows(feed.querySelectorAll('.just-arrived')); // or reveal any rows y
488
525
 
489
526
  Options: `onRefresh` (may return a promise; the ring spins at least 600ms), `content` (the element that moves, default the first child), `threshold` (px, default 64), `messages` (`{ refreshing, done, failed }` for the live region), `reveal` (a selector for the rows `onRefresh` added: a frame after it resolves they are hidden behind the check, then resolve in from characters as the content springs back). Methods: `refresh()`, `revealRows(rows)` (each row appears as glyphs of its own text, with its photos drawn as tone rather than flat tiles, following a row that is still opening, then hands over to the HTML over about 760ms; a row is revealed once, and under reduced motion or without WebGL2 rows just show); getter `state` (`idle`, `pulling`, `refreshing`, `done`). Sets `aria-busy` while refreshing. It engages only when the element is scrolled to the top; give it `overflow: hidden` or `auto` and keep a Refresh button for keyboard users.
490
527
 
528
+ ## Event-site pieces (4.30)
529
+
530
+ The pieces an event or launch site is built from: a hero of images that morph as hatched lines, a dithered light, a globe of dots, photos in a brand's two tones, plus `decode`'s scramble and scroll, `wordmark`'s assembly and `cursorTrail`'s spray above. All draw through the shared GPU context, take `ink`, `look`, `poster`, `lazy`, `globalLook` and `reducedMotion`, pause off screen, return inert handles without WebGL2 (the poster, or your HTML, stays) and idle when nothing moves.
531
+
532
+ ```ts
533
+ import { morphField, ditherGradient, globe, ditherImage } from 'asciify-engine/kit';
534
+ ```
535
+
536
+ ### morphField
537
+
538
+ Frames that morph into one another as the field's marks: by default diagonal engraved lines (`mark: 'engraving'`, `markAngle: -45`) whose weight follows the tone, so one picture's stripes thin away as the next one's thicken. It holds each frame, then cross-fades; between morphs it idles unless the frame is a video or a drawing function.
539
+
540
+ ```ts
541
+ const hero = morphField(heroEl, {
542
+ sources: ['/frames/crowd.webp', '/frames/stage.webp', (pen, w, h, now) => drawCubes(pen, w, h, now)],
543
+ ink: '#ffffff', hold: 3200, transition: 1000,
544
+ onChange: i => (counter.textContent = `${i + 1}/3`),
545
+ });
546
+ nextButton.onclick = () => hero.next();
547
+ ```
548
+
549
+ Options: `sources` (required: image URLs or elements, canvases, videos, or `(pen, w, h, now) => void` functions that draw white-on-black each frame, such as a turning 3D scene), `hold` (ms, default 3200), `transition` (ms, default 1000), `autoplay` (default true; off under reduced motion), `fit` (`'cover' | 'contain'`), `layer` (`'under'` default, below the content; `'over'`), `onChange(index)` (as each morph starts: keep a caption or counter in step), `label` (read to assistive technology; otherwise decoration). Methods: `next()`, `go(index)`; getter `index`. Change the stripes with `look`: `{ markAngle: 90 }` vertical lines, `{ mark: 'crosshatch' }`, `{ mark: 'dither' }`, `{ mark: 'glyph' }` for characters, `{ cellSize }` for their pitch.
550
+
551
+ ### ditherGradient
552
+
553
+ A gradient of soft lights drawn as a two-tone dither: the field's ink on the element's own background. The lights drift and breathe gently and the first leans toward the pointer. With `motion: 0` it draws once and idles until the pointer moves over it.
554
+
555
+ ```ts
556
+ // A glow rising from the bottom of a brand-coloured hero.
557
+ ditherGradient(hero, { ink: '--brand-light', lights: [{ x: 0.5, y: 1.08, radius: 0.62 }] });
558
+ // Soft blobs, like a cosmic poster.
559
+ ditherGradient(panel, { ink: '--brand', lights: [{ x: 0.2, y: 0.3, radius: 0.35 }, { x: 0.75, y: 0.7, radius: 0.4, strength: 0.8 }], look: { mark: 'dot', cellSize: 8 } });
560
+ ```
561
+
562
+ Options: `lights` (`{ x, y, radius?, strength? }` in shares of the element, radius of its longer side; default one broad light from just below the bottom edge), `base` (ground tone, default 0.05), `motion` (0 still to 1, default 0.35), `follow` (how far the first light leans to the pointer, default 0.06; 0 off), `layer` (`'under'` default). Ink defaults to the element's text colour, so give the element its background and a text colour, or pass `ink`. Method: `setLights(lights)`. The default mark is `'dither'` (a 4x4 Bayer screen); `look: { mark: 'dot' | 'pixel' | 'stipple' | 'glyph' }` changes it.
563
+
564
+ ### globe
565
+
566
+ A globe of dots: land, a faint ocean, latitude and longitude lines and the far side's land showing through, turning slowly. Drag turns it with a little momentum; `points` mark places with a ring that keeps spreading. It carries a built-in world map (Natural Earth 1:50m land, public domain, 2.7 KB).
567
+
568
+ ```ts
569
+ const earth = globe(el, { ink: '--brand', points: [{ lat: 37.8, lon: -122.4 }, { lat: 51.5, lon: -0.1 }], label: 'Our network of 330 cities' });
570
+ regionSelect.onchange = () => earth.rotateTo(Number(regionSelect.value));
571
+ ```
572
+
573
+ Options: `speed` (degrees a second, default 6; 0 holds it, and with no points it then draws once and idles), `lon` (longitude facing the viewer, default -30), `tilt` (degrees, default 18), `land` (`(lat, lon) => number | boolean` for your own map, such as a coverage map), `graticule` (degrees between lines, default 15; 0 none), `points` (`{ lat, lon }[]`), `backside` (0–1, default 0.22), `size` (share of the shorter side, default 0.92), `drag` (default true; sets `touch-action: pan-y` so the page still scrolls), `label`. Methods: `rotateTo(lon, lat?)`; getter `lon`. Default look: `mark: 'dot'`, `cellSize: 5`; `look: { mark: 'glyph', charset: ' .:+#' }` writes it in characters.
574
+
575
+ ### ditherImage
576
+
577
+ An image drawn in a brand's two tones as dither, pixels, dots or engraving, developing in, lightest marks first, as it scrolls into view.
578
+
579
+ ```ts
580
+ ditherImage(figure, { tones: ['--navy', '--brand-blue'], mark: 'dither', cellSize: 5 }); // the <img> inside
581
+ ditherImage(tile, { src: '/aerial.webp', tones: ['transparent', '--brand'], mark: 'pixel', reveal: 'scroll' });
582
+ ```
583
+
584
+ Options: `src` (a URL or `<img>`; default the first `<img>` inside, hidden while drawn), `tones` (`[ground, ink]`, CSS colours or tokens; `'transparent'` keeps the element's own background; default `['transparent', '--ascii-ink-strong']`), `mark` (any field mark, default `'dither'`), `cellSize` (default 5), `reveal` (`'visible'` default, `'scroll'`, `'none'`), `duration` (ms, default 1100), `focus`, `label` (default the image's `alt`). Method: `replay()`.
585
+
586
+ ### Field options these use
587
+
588
+ Two `look` settings (and `mountGlyphField` options) came with these pieces:
589
+
590
+ - `markAngle` (degrees, default -35): the screen angle of the line and print marks (`engraving`, `crosshatch`, `woodblock`, `risograph`), turning clockwise as CSS `rotate` does: 0 horizontal lines, 90 vertical, -45 rising to the right.
591
+ - `glyphOrder: 'text'`: reads the charset after its blank in order along each row (each row a few letters on), so a phrase stays legible as text-as-art; the tone decides which cells show their letter (all from a tone of 0.6, thinning toward the quiet edges). For a background of repeated words that lights up under the pointer: `background(el, { preset: wordField, look: { glyphOrder: 'text', charset: ' build connect protect ', hover: 'lantern', highlight: '#ff5b1f' } })`.
592
+
593
+ ### Recipe: an event site's header, hero and footer
594
+
595
+ For "a Cloudflare Connect–style header, hero and footer", or any launch or event page with that energy, compose these pieces rather than a copy of anyone's design. Use your own brand tokens and images.
596
+
597
+ ```ts
598
+ import { morphField, decode, cursorTrail, ditherGradient, globe, wordmark } from 'asciify-engine/kit';
599
+
600
+ // Header: plain HTML links that scramble on hover.
601
+ for (const a of header.querySelectorAll('nav a')) decode(a, { scramble: true, trigger: 'manual', settle: 'text', look: { cellSize: 3 } });
602
+
603
+ // Hero on a brand-coloured ground: frames morphing as hatched lines, a headline that flips through alphabets and lands.
604
+ const hero = morphField(heroEl, { sources: frames, ink: '#ffffff', look: { cellSize: 6 }, onChange: i => (tag.textContent = `${i + 1}/${frames.length}`) });
605
+ decode(heroEl.querySelector('h1')!, { scramble: true, settle: 'text' });
606
+
607
+ // The pointer sprays hatching with coordinate tags over the open page (behind cards).
608
+ cursorTrail({ style: 'spray', readout: true, ink: '--ascii-ink', zIndex: -1 });
609
+
610
+ // A globe section, and a closing call to action on a dithered glow.
611
+ globe(globeEl, { ink: '--brand', points: venues, label: 'Where we meet' });
612
+ ditherGradient(ctaEl, { ink: '--brand-light' });
613
+
614
+ // Footer: the name gathers as the page reaches it.
615
+ wordmark(footerMark, { text: 'connect', assemble: 'scroll', look: { hover: 'none' } });
616
+ ```
617
+
618
+ Keep each piece to one job: the hero's morph is the key visual, the spray is ambient, the globe and glow are sections. Mount the cursor once per page, keep every headline and link real HTML, and give each piece a `poster` where it is above the fold.
619
+
620
+ ## Ocean
621
+
622
+ ### ocean
623
+
624
+ An ocean seen from above, drawn as characters: fish, sharks, hammerheads, manta rays, jellyfish and schools of small fish. Each creature is a procedural body (a spine with a width profile, fins and a tail) that swims with a travelling wave from snout to tail, steers, wanders and, in a school, keeps together. Depth layers make the far ones smaller, fainter and slower. The cursor scatters small fish and sharks turn to investigate it and circle; a press sends a ring through the field and scatters the fish near it. By default it sits behind the element's content, as a background.
625
+
626
+ ```ts
627
+ import { ocean } from 'asciify-engine/kit';
628
+
629
+ const sea = ocean(hero, {
630
+ creatures: [{ kind: 'shark' }, { kind: 'hammerhead', size: 0.9 }, { kind: 'school', count: 32 }, { kind: 'jellyfish', count: 2 }],
631
+ ink: '--ascii-ink', label: 'Sharks and a school of fish swimming',
632
+ });
633
+ addButton.onclick = () => sea.add('manta');
634
+ ```
635
+
636
+ Options: `creatures` (`{ kind, size?, count? }[]`, kinds `fish`, `shark`, `hammerhead`, `manta`, `jellyfish`, `school`; a school's `count` is its fish, default 28; size multiplies the kind's length, which follows the element's shorter side; default a shark, a hammerhead, a manta, four fish, two jellyfish and a school), `speed` (default 1; 0 holds and idles), `density` (multiplies every count), `depth` (0–1, default 0.7; 0 one plane), `palette` (an ink per kind, colours or tokens, drawn in source colour; kinds without one use `ink`), `background` (`'transparent'` default, or a colour or token for the water), `interactive` (default true), `ripples` (default true), `edges` (`'turn'` default, or `'wrap'`), `seed` (default 1; the same seed always gives the same ocean), `layer` (`'under'` default, `'over'`), `label`. Methods: `add(kind, { size?, count? })` returns how many were added, `remove(kind?)` removes the latest (a whole school at once), getter `count`. Default look: glyphs `' .:-=+*#%@'` at `cellSize: 7`; every mark works (`look: { mark: 'dither' }`, `'dot'`, `'pixel'`, `'engraving'`, …) and every charset (`look: { charset: ' ⠁⠃⠇⡇⣇⣧⣷⣿' }` for braille). Under reduced motion it draws a still composition (the creatures where a moment of swimming left them); it pauses off screen and in a hidden tab. Keep headlines over the quieter middle, or lower `density`.
637
+
491
638
  ## Backgrounds
492
639
 
493
640
  The curated backgrounds on asciify.org/backgrounds as one call each. They are scenes of the GPU glyph field, so they follow `setGlobalLook` (the header's characters and the 24 render styles) like every field component, keep a quiet zone for your copy, take `lazy`, `poster`, `globalLook` and the context budget, and hold one still frame under reduced motion. The canvas sits under the element's content (`isolation: isolate`, `z-index: -1`), follows the theme and pauses off screen and in hidden tabs.
@@ -523,6 +670,21 @@ Options: `preset` (required), `palette` (one ink or `{ light, dark }`, winning o
523
670
  | `cascade` | Streaks falling in three depths: far ones faint and slow, near ones bright and quick | 11, 1, lantern |
524
671
  | `wordField` | Your word (`text`) as a flowing field of letters | 13, 1, trail |
525
672
  | `fluid` | The original Asciify hero: flowing folds with a wake behind the cursor | 7, 1, wake |
673
+ | `estuary` | Soft blotches drifting on a slow current; the cursor stirs them into swirls that settle back, a click stirs harder | 9, 1, stir |
674
+ | `capillary` | Thin branching veins: a faint resting network, new veins sprouting toward the cursor and bursting from a click, fading as they age | 8, 1, grow |
675
+ | `shockwave` | A faint grain; a click sends rings of characters outward (a front and two echoes, slowing and fading), and the moving cursor leaves small gusts | 9, 1, rings |
676
+
677
+ ### Interactive simulations: estuary, capillary, shockwave
678
+
679
+ Three presets are simulations the visitor plays with rather than scenes: each runs on a coarse grid on the CPU and draws into the field's source, so every mark and charset works (`look: { mark: 'dither' }`, `charset: 'braille'`), the ink follows the theme, and the defaults are faint enough for a headline on top. They answer the cursor and clicks (`cursor: false` turns both off), hold one still frame under reduced motion, pause off screen, and stop drawing once nothing moves: `capillary` and `shockwave` rest until the pointer wakes them, `estuary` drifts at any `speed` above 0 and with `speed: 0` waits to be stirred. None keeps a quiet middle by default; pass `clear: headline` for one.
680
+
681
+ ```ts
682
+ import { background, capillary, estuary, shockwave } from 'asciify-engine/kit';
683
+
684
+ const modes = { estuary, capillary, shockwave };
685
+ const bg = background(hero, { preset: estuary, clear: headline });
686
+ pills.forEach(pill => (pill.onclick = () => bg.setPreset(modes[pill.dataset.mode as keyof typeof modes])));
687
+ ```
526
688
 
527
689
  Retired presets are still exported, marked `@deprecated`, so code that imports them keeps running unchanged; they are no longer shown on the site or in these docs. Move to the closest current one: `halftone` → `drift` with `mark: 'dot'` (`cellSize: 14`), `dna` and `voidWell` → `halo`, `circuit` and `stars` → `constellation`, `aurora`, `fire`, `silk` and `veil` → `mesh`, `grid`, `weave` and `moire` → `interference`, `morph` and `noise` → `mesh`, `pulse` → `ripple`, `rain` → `cascade`, `terrain`, `dune` and `etch` → `contour`, `facet` → `caustics`, `wave` → `swell`. The retired canvas presets draw on a 2D canvas and also take `accent` and `dither`.
528
690
 
@@ -84,4 +84,20 @@ The website uses the published renderer's native Trail. A chain-shaped tonal ove
84
84
 
85
85
  ## 19. Page-wide cursor trail
86
86
 
87
- “Add an Asciify cursor trail to [our site] with `cursorTrail` from `asciify-engine/kit`: one call in the app shell, style [trail/wake/water/sparkle], inked from [our text token], fine cells. Keep it off on touch and under reduced motion, quiet over inputs, dialogs and [our heroes or editors] (`data-ascii-cursor-skip` or `skip`), under our header and dialogs (`zIndex`), and destroy it on unmount. If we offer a picker, store the choice per visitor and call `update({ style })` or `destroy()` live.”
87
+ “Add an Asciify cursor trail to [our site] with `cursorTrail` from `asciify-engine/kit`: one call in the app shell, style [trail/wake/water/sparkle], inked from [our text token], fine cells. Keep it off on touch and under reduced motion, quiet over inputs, dialogs and [our editors] (`data-ascii-cursor-skip` or `skip`), under our header and dialogs (`zIndex`), and destroy it on unmount. Make it one continuous trail: turn off the own hover of our heroes, backgrounds and wordmarks (`look: { hover: 'none' }`) so the page trail flows over them instead of stopping at their edges, skip only interactive components with hovers of their own, and match its cell size to our biggest field (e.g. the footer wordmark). Treat it as a background: `zIndex: -1`, only `html` paints the page colour (body, root and full-page wrappers transparent), so cards, panels and text cover it and it never draws on top of them. If we offer a picker, store the choice per visitor and call `update({ style })` or `destroy()` live.”
88
+
89
+ ## 20. Event-site header, hero and footer
90
+
91
+ “Build a [Cloudflare Connect–style / our event's] header, hero and footer with `asciify-engine/kit`, using our own brand tokens, frames and copy, never another site's assets. Header: real nav links, each with `decode(link, { scramble: true, trigger: 'manual', settle: 'text' })`. Hero on the brand-coloured ground: `morphField` cycling our [3–5 images or a drawn scene] as hatched lines (`ink` white or a light brand tint, `onChange` updating a small counter), the headline `decode(h1, { scramble: true, settle: 'text' })`, and one page-wide `cursorTrail({ style: 'spray', readout: true, zIndex: -1 })`. Sections: `globe` with our venue `points` and a `label`; photos through `ditherImage` in our two brand tones; the closing call to action on `ditherGradient`. Footer: `wordmark` of our name with `assemble: 'scroll'` and `look: { hover: 'none' }`. Keep every headline, link and CTA in HTML, posters for the first viewport in both themes, reduced motion showing settled stills, nothing drawing off screen or when idle, and destroy every handle on unmount. Check light and dark, a 390px phone, keyboard focus and WebGL off.”
92
+
93
+ ## 21. Dither a photo in our brand colours
94
+
95
+ “Draw [image] with `ditherImage` in our two tones `[ground token, ink token]`, mark [dither/pixel/dot/engraving], developing in on scroll (`reveal: 'scroll'`). Keep the `<img>` in the markup with its `alt` as the fallback and accessible name. Compare cell sizes 4–8 on desktop and phone, and both themes.”
96
+
97
+ ## 22. An ASCII ocean behind the hero
98
+
99
+ “Put an ASCII ocean behind our landing hero with `ocean` from `asciify-engine/kit`: [a shark, a hammerhead, a manta, a few fish, two jellyfish and a school of 30 small fish], inked from [our text token] (or a `palette` per kind from our brand tokens), `layer: 'under'` so the headline and CTA stay real HTML above it. The cursor scatters the fish and the sharks turn to investigate; a click sends a ripple. Pick a `seed` we like, keep the busiest creatures off the headline (fewer, or a lower `density` on phones), give it a `label` or leave it decorative, and a `poster` for the first viewport in both themes. Reduced motion shows a still composition; it pauses off screen. Check light and dark, a 390px phone and WebGL off.”
100
+
101
+ ## 23. An interactive hero background with a mode switcher
102
+
103
+ “Build an interactive ASCII hero background visitors can stir, click and grow, with a mode switcher: `background(hero, { preset: estuary, clear: headline })` from `asciify-engine/kit`, and three pills (Estuary, Capillary, Shockwave) as a real radio group that call `bg.setPreset(estuary | capillary | shockwave)`. Keep it faint behind the headline in [our text token] (`palette: { light, dark }` if we need a tint), the headline and CTA real HTML, the pills keyboard-reachable and labelled, a `poster` for the first viewport in both themes, and one still frame under reduced motion. Check light and dark, a 390px phone, touch, and WebGL off.”
@@ -1 +0,0 @@
1
- import{parseColor as t,MONO as e,pixelSnap as n,fbm as o,hash2 as a,band as r,ditherIndex as i,toneAlpha as c}from"./chunk-ZCHGV2CV.js";import{mountCanvas as h}from"./chunk-3OD5BMF5.js";import{hexRgb as s}from"./chunk-GF72XM4C.js";function l(h,s,l,f,M={x:.5,y:.5},m={}){const{fontSize:g=13,charAspect:u=.62,lineHeightRatio:d=1.3,chars:p=" .:-=+*#%@",baseColor:x=null,accentColor:$,accentThreshold:b=.52,mouseInfluence:S=.4,mouseFalloff:C=2.8,speed:k=1,vortex:v=!0,sparkles:y=!1,breathe:z=!0,lightMode:j=!1}=m,w=Array.from(p),A=g*u,F=g*d,B=Math.ceil(s/A),G=Math.ceil(l/F),H=M.x,R=M.y,T=$?t($):null;h.clearRect(0,0,s,l),h.font=`${g}px ${e}`;const V=n(h);h.textBaseline="top";const q=f*k,D=z?.12*(.5*Math.sin(.22*q)+.5):0;for(let t=0;t<G;t++)for(let e=0;e<B;e++){const n=e/B,s=t/G,l=.045,f=.08*q,M=.06,g=.45*((.5*Math.sin(.08*e+.05*t+.6*q)+.5+(.5*Math.sin(.03*e-.07*t+.4*q)+.5)+(.5*Math.sin(.05*e+.03*t+.8*q)+.5))/3)+.35*(.5*o(e*l+f,t*l*1.4-.7*f)+.5)+.2*(.5*Math.sin((e+.65*t)*M+1.1*q)+.5)+D,u=n-H,d=s-R,p=Math.sqrt(u*u+d*d);let $=0;if(v&&p<.35){const t=Math.atan2(d,u),e=Math.sin(4*t+2.2*q-14*p),n=Math.max(0,1-p/.35);$=e*n*n*.22}const k=g*(1-S)+(Math.max(0,1-p*C)+.5*$)*S+.15*$,z=Math.min(1,Math.max(0,k));let I=z;if(y&&z>.72){const n=Math.floor(8*q),o=a(7*e+3*n,11*t+n);o>.88&&(I=Math.min(1,z+4*(o-.88)))}const O=r(I,.32,.9);if(O<=0)continue;const W=i(O,w.length,e,t,m.ditherStrength);if(" "===w[W])continue;const X=c(O);h.fillStyle=T&&I>b?`rgba(${T.r},${T.g},${T.b},${X})`:x?x.replace("{a}",String(X)):j?`rgba(111,109,103,${X})`:`rgba(143,141,134,${X})`,h.fillText(w[W],V(e*A),V(t*F))}}var f={id:"wave",name:"Wave",description:"An interference field with a soft vortex at the cursor and a slow breath.",cellSize:13,speed:.6,cursor:!0,mount:t=>h(t,(t,e,n,o,a,r)=>{const[i,c,h]=s(r.ink);l(t,e,n,o,a,{ditherStrength:r.ditherStrength,fontSize:r.fontSize,chars:r.chars,lightMode:r.lightMode,baseColor:`rgba(${i},${c},${h},{a})`,accentColor:r.accent})})};export{l as renderWaveBackground,f as wave};//# sourceMappingURL=chunk-2GAV4OAK.js.map
@@ -1 +0,0 @@
1
- import{MONO as t,GRAPHITE_LIGHT as o,GRAPHITE_DARK as e,pixelSnap as n,parseColor as r,hash2 as c,band as h,ditherIndex as i,toneAlpha as l}from"./chunk-ZCHGV2CV.js";import{mountCanvas as a}from"./chunk-3OD5BMF5.js";function s(a,s,f,M,g={}){const{fontSize:d=13,color:u,accentColor:S,speed:$=.4,layers:I=4,turbulence:P=.8,lightMode:b=!1}=g,p=.62*d,m=1.3*d,k=Math.ceil(s/p),x=Math.ceil(f/m);a.clearRect(0,0,s,f),a.font=`${d}px ${t}`,a.textBaseline="top";const z=b?o:e;let C=z.r,w=z.g,y=z.b;const j=n(a);if(u){const t=r(u);t&&(C=t.r,w=t.g,y=t.b)}let v=C,B=w,V=y;const A=S?r(S):null;A&&(v=A.r,B=A.g,V=A.b);const D=M*$,F=g.chars?Array.from(g.chars):void 0,G=["-","\\","|","/"];for(let t=0;t<x;t++){const o=t/x;for(let e=0;e<k;e++){const n=e/k;let r=0,s=0;for(let t=0;t<I;t++){const e=c(13*t,7*t+3),h=1.1+2.4*e,i=.9+2*c(29*t,11*t+1),l=e*Math.PI*6,a=(.2+.5*c(41*t,17*t))*(t%2==0?1:-1.3),f=Math.sin(n*h*Math.PI*2+D*a+l),M=Math.cos(o*i*Math.PI*2+D*a*.6+1.7*l),g=Math.sin(n*i*Math.PI*P+o*h*Math.PI*P+D*a*.4);r+=Math.atan2(M+.3*g,f),s+=.5*(f*M+1)}const f=r/I,M=h(s/I,.54,.74);if(M<=0)continue;const d=(f%Math.PI+Math.PI)%Math.PI,u=Math.round(d/(Math.PI/4))%G.length;if(!F&&g.ditherStrength&&0===i(M,2,e,t,g.ditherStrength))continue;const S=F?F[i(M,F.length,e,t,g.ditherStrength)]:G[u];if(" "===S)continue;const $=!!A&&M>.85,b=l(M);a.fillStyle=$?`rgba(${v},${B},${V},${b})`:`rgba(${C},${w},${y},${b})`,a.fillText(S,j(e*p),j(t*m))}}}var f={id:"silk",name:"Silk",description:"Long ribbons that follow a slow flow field.",cellSize:13,speed:1,cursor:!1,mount:t=>a(t,(t,o,e,n,r,c)=>s(t,o,e,n,{ditherStrength:c.ditherStrength,fontSize:c.fontSize,chars:c.chars,lightMode:c.lightMode,color:c.ink,accentColor:c.accent}))};export{s as renderSilkBackground,f as silk};//# sourceMappingURL=chunk-3LJ5C3U3.js.map
@@ -1 +0,0 @@
1
- import{mountPageField as e}from"./chunk-7DXSG526.js";var r=(e,r)=>!1===e.cursor?"none":"string"==typeof e.cursor?e.cursor:r;function t(r,t,n,l){let o=null;if(o=e({host:r.host,kit:r.kit,layer:"under",glyphs:l&&(()=>l(r.settings())),look:e=>{const n=r.settings();return{...t(n,e),...n.look}},ink:e=>r.ink(e)??(e?n.dark:n.light),reducedMotion:r.reducedMotion,onFail:()=>{o=null,r.onFail()}}),!o)return null;const i=o.canvas;return{get field(){return o?.field??null},get canvas(){return o?.canvas??i},refresh(){o?.refresh()},play(){o?.field.play()},pause(){o?.field.pause()},destroy(){o?.destroy(),o=null}}}var n=()=>"number"==typeof innerWidth&&innerWidth<760,l={light:"#86847e",dark:"#7c7a74"},o={light:"#8f8d86",dark:"#64625d"},i=e=>({font:'"Geist Mono", ui-monospace, monospace',fontWeight:400,cellSize:e.cellSize,cellAspect:.62,glyphScale:.86}),s=e=>({cellSize:e.cellSize,cellAspect:1,dotScale:.68}),a={x:.18,y:.2,width:.64,height:.6,feather:.3,shape:"ellipse"};function c(e){const{scene:n,charset:o,hover:c,middle:d=a,feather:h=.3,graphite:u=l}=e,p=e.mark??"glyph";return{id:e.id,name:e.name,description:e.description,cellSize:e.cellSize,speed:e.speed??1,cursor:e.cursor??!0,opacity:e.opacity,mount(l){const a=t(l,(t,l)=>{const a=((e,r)=>e.mark??(e.charset||e.look?.charset?"glyph":r))(t,p);return{scene:n,color:"ink",shade:0,background:"transparent",vignette:0,speed:t.speed,maxPixelRatio:2,levels:"none",blend:"on",..."dot"===a?{mark:"dot",...s(t)}:{mark:"glyph",charset:t.charset??o,...i(t)},hover:r(t,c),lantern:{radius:.14,strength:.45,trail:.8,follow:9},trail:{strength:.8,radius:.35},fluid:{radius:.09,refraction:.03,glow:.6},clear:t.clear?{element:t.clear,padding:0,feather:h,shape:"ellipse"}:d,...e.tune?.(t,l)}},u,"dot"===p?e=>({...i(e),charset:o}):void 0);if(!a||!e.extra)return a;const g=e.extra(l,a),f=a.destroy.bind(a);return Object.assign(a,{destroy(){g(),f()}})}}}export{o as FIELD_GRAPHITE,r as hoverOf,t as mountField,n as narrow,c as scenePreset};//# sourceMappingURL=chunk-3VEMKDBW.js.map
@@ -1 +0,0 @@
1
- import{parseColor as t,hash2 as o}from"./chunk-ZCHGV2CV.js";import{mountCanvas as e}from"./chunk-3OD5BMF5.js";function n(e,n,c,i,a={x:.5,y:.5},r={}){const{fontSize:l=13,chars:s=" ·:;=+*#%@",color:h,accentColor:f,speed:M=1,radius:$=.38,swirl:d=3,lightMode:g=!1}=r,m=f??(g?"#6b8700":"#d4ff00"),p=.62*l,u=1.4*l,x=Math.ceil(n/p),b=Math.ceil(c/u),S=n/c;e.clearRect(0,0,n,c),e.font=`${l}px monospace`,e.textBaseline="top";let y=255,z=255,C=255;if(g&&(y=55,z=55,C=55),h){const o=t(h);o&&(y=o.r,z=o.g,C=o.b)}let k=212,v=255,w=0;const V=t(m);V&&(k=V.r,v=V.g,w=V.b);const j=i*M;for(let t=0;t<b;t++){const n=t/b;for(let c=0;c<x;c++){const i=(c/x-a.x)*S,r=n-a.y,l=Math.sqrt(i*i+r*r)/$;if(l>1){const n=o(3*c,7*t)*Math.max(0,1-3*(l-1));if(n<.62)continue;const i=n*(g?.28:.04);e.fillStyle=`rgba(${y},${z},${C},${i})`,e.fillText(s[1],c*p,t*u);continue}const h=Math.max(0,1-Math.abs(l-(.15+.12*Math.sin(1.1*j)))/.07),f=Math.pow(1-l,2.2),M=Math.min(1,f+.6*h);if(M<.06)continue;const d=Math.floor(M*(s.length-1)),m=s[Math.min(s.length-1,d)],b=h>.35||l<.08,V=g?.85*M:.18*M;e.fillStyle=b?`rgba(${k},${v},${w},${g?.95:.38})`:`rgba(${y},${z},${C},${V})`,e.fillText(m,c*p,t*u)}}}var c={id:"void",name:"Void",description:"A gravity well that spirals characters around the cursor.",cellSize:13,speed:1,cursor:!0,mount:t=>e(t,(t,o,e,c,i,a)=>n(t,o,e,c,i,{fontSize:a.fontSize,chars:a.chars,lightMode:a.lightMode,color:a.ink,accentColor:a.accent}))};export{n as renderVoidBackground,c as voidWell};//# sourceMappingURL=chunk-7M2FFUZ3.js.map
@@ -1 +0,0 @@
1
- import{parseColor as o,hash2 as t}from"./chunk-ZCHGV2CV.js";import{mountCanvas as l}from"./chunk-3OD5BMF5.js";import{mixHex as e}from"./chunk-GF72XM4C.js";function r(l,e,r,a,i={}){const{fontSize:n=13,baseChars:f="ATCG",bridgeChars:h="-=≡",color:c="#00e5ff",color2:s="#ff4081",bridgeColor:M="#88ffcc",speed:b=1,helixCount:g,lightMode:p=!1}=i,d=.62*n,m=1.4*n,u=Math.ceil(e/d),$=Math.ceil(r/m),x=g??Math.max(1,Math.floor(e/80)),C=u/x,S=o(c)??{r:0,g:229,b:255},k=o(s)??{r:255,g:64,b:129},A=o(M)??{r:136,g:255,b:204};l.clearRect(0,0,e,r),l.font=`${n}px "JetBrains Mono", monospace`,l.textBaseline="top";const z=a*b,T=.35*C;for(let o=0;o<x;o++){const e=C*(o+.5);for(let r=0;r<$;r++){const a=r/$*Math.PI*6-1.8*z,i=e+Math.sin(a)*T,n=e+Math.sin(a+Math.PI)*T,c=Math.round(i),s=Math.round(n);if(c<0||c>=u)continue;const M=t(31*o+7*r,3),b=f[Math.floor(M*f.length)],g=.5*(Math.sin(a)+1),p=.5*(Math.sin(a+Math.PI)+1);if(l.globalAlpha=.35+.65*g,l.fillStyle=`rgb(${S.r},${S.g},${S.b})`,l.fillText(b,c*d,r*m),s>=0&&s<u){const e=t(53*o+11*r,7),a=f[Math.floor(e*f.length)];l.globalAlpha=.35+.65*p,l.fillStyle=`rgb(${k.r},${k.g},${k.b})`,l.fillText(a,s*d,r*m)}if(r%3===0){const e=Math.min(c,s),a=Math.max(c,s);if(a-e>1){const i=t(17*r+43*o,5),n=h[Math.floor(i*h.length)],f=.25*(g+p)+.2;l.globalAlpha=f,l.fillStyle=`rgb(${A.r},${A.g},${A.b})`;for(let o=e+1;o<a;o++)l.fillText(n,o*d,r*m)}}}}l.globalAlpha=1}var a={id:"dna",name:"DNA",description:"Double helices scroll past with base-pair letters.",cellSize:13,speed:1,cursor:!1,mount:o=>l(o,(o,t,l,a,i,n)=>r(o,t,l,a,{fontSize:n.fontSize,color:n.ink,color2:n.accent??e(n.ink,"#ffffff",.55),bridgeColor:e(n.ink,"#0c0d0b",.35)}))};export{a as dna,r as renderDnaBackground};//# sourceMappingURL=chunk-AIGAJMNN.js.map
@@ -1 +0,0 @@
1
- import{parseColor as t,hash2 as o}from"./chunk-ZCHGV2CV.js";import{mountCanvas as e}from"./chunk-3OD5BMF5.js";function n(e,n,c,l,i={}){const{fontSize:r=13,chars:a="0123456789ABCDEF@#$&*+=/<>",accentColor:s,color:f,speed:h=1,density:g=.55,tailLength:d=14,lightMode:$=!1}=i,p=s??($?"#6b8700":"#d4ff00"),M=.62*r,m=1.4*r,u=Math.ceil(n/M),b=Math.ceil(c/m);e.clearRect(0,0,n,c),e.font=`${r}px monospace`,e.textBaseline="top";let C=255,S=255,x=255;if($&&(C=55,S=55,x=55),f){const o=t(f);o&&(C=o.r,S=o.g,x=o.b)}let z=212,k=255,y=0;const B=t(p);B&&(z=B.r,k=B.g,y=B.b);const j=b+d;for(let t=0;t<u;t++){if(o(17*t,3)>g)continue;const n=(.5+1.5*o(31*t,7))*h,c=o(13*t,11)*j,i=Math.floor((l*n*7+c)%j),r=t*M;for(let n=0;n<=d;n++){const c=i-(d-n);if(c<0||c>=b)continue;const s=c*m,f=o(53*t+Math.floor(5*l+n),7*c),h=a[Math.floor(f*a.length)],g=n/d;if(n===d)e.fillStyle=`rgba(${z},${k},${y},${$?.7:.85})`;else{const t=$?.85*g:.15*g;e.fillStyle=`rgba(${C},${S},${x},${t})`}e.fillText(h,r,s)}}}var c={id:"rain",name:"Rain",description:"Columns of falling characters with bright heads.",cellSize:13,speed:1,cursor:!1,mount:t=>e(t,(t,o,e,c,l,i)=>n(t,o,e,c,{fontSize:i.fontSize,chars:i.chars,lightMode:i.lightMode,color:i.ink,accentColor:i.accent}))};export{c as rain,n as renderRainBackground};//# sourceMappingURL=chunk-BFBG3VHI.js.map
@@ -1 +0,0 @@
1
- import{parseColor as t,hash2 as o}from"./chunk-ZCHGV2CV.js";import{mountCanvas as e}from"./chunk-3OD5BMF5.js";import{mixHex as l}from"./chunk-GF72XM4C.js";var r=1,i=2,n=4,f=8,c={3:"─",12:"│",9:"┌",10:"┐",5:"└",6:"┘",11:"┬",7:"┴",13:"├",14:"┤",15:"┼",[r]:"╶",[i]:"╴",[n]:"╵",[f]:"╷"};function a(e,l,r,i,n={}){const{fontSize:f=13,pulseColor:a="#ffffff",color:s="#00ff88",density:h=.38,speed:g=1,lightMode:p=!1}=n,u=.62*f,b=1.4*f,m=Math.ceil(l/u),M=Math.ceil(r/b),d=t(s)??{r:0,g:255,b:136},C=t(a)??{r:255,g:255,b:255},$=(t,e)=>{if(o(17*t+1,7*e+2)>h)return 0;let l=0;return t+1<m&&o(17*t+1,7*e+2)>.15&&(l|=1),t-1>=0&&o(17*(t-1)+1,7*e+2)>.15&&(l|=2),e+1<M&&o(17*t+1,7*(e+1)+2)>.15&&(l|=8),e-1>=0&&o(17*t+1,7*(e-1)+2)>.15&&(l|=4),l};e.clearRect(0,0,l,r),e.font=`${f}px "JetBrains Mono", monospace`,e.textBaseline="top";const S=i*g;for(let t=0;t<M;t++)for(let l=0;l<m;l++){const r=$(l,t);if(0===r)continue;const i=c[r]??"·",n=(8*S+40*o(l,23*t))%m,f=(6*S+40*o(17*t,11*l))%M,a=(1&r||2&r)&&Math.abs(l-n)<1.5,s=(4&r||8&r)&&Math.abs(t-f)<1.5,h=a||s,g=.25+.35*o(3*l,5*t);h?(e.globalAlpha=.95,e.fillStyle=`rgb(${C.r},${C.g},${C.b})`):(e.globalAlpha=g,e.fillStyle=`rgb(${d.r},${d.g},${d.b})`),e.fillText(i,l*u,t*b)}e.globalAlpha=1}var s={id:"circuit",name:"Circuit",description:"Circuit traces with signals running along them.",cellSize:13,speed:1,cursor:!1,mount:t=>e(t,(t,o,e,r,i,n)=>a(t,o,e,r,{fontSize:n.fontSize,chars:n.chars,lightMode:n.lightMode,color:n.ink,pulseColor:n.accent??l(n.ink,"#ffffff",.7)}))};export{s as circuit,a as renderCircuitBackground};//# sourceMappingURL=chunk-E3FPFANJ.js.map
@@ -1 +0,0 @@
1
- import{mergeOptions as t,prefersReduced as i,hash as e,mountSurface as s}from"./chunk-GF72XM4C.js";import{isGlyphFieldSupported as h,watchPointer as a,watchVisibility as r,watchMotionPreference as n}from"../field.js";var o=class{constructor(t,i,e){this.previous=null,this.peak=0,this.width=t,this.height=i,this.span=e??Math.min(t,i);const s=t*i;this.pixels=new Uint8Array(4*s),this.signal=new Float32Array(s),this.offsetX=new Float32Array(s),this.offsetY=new Float32Array(s),this.driftX=new Float32Array(s),this.driftY=new Float32Array(s),this.u=new Float32Array(s),this.v=new Float32Array(s),this.ink=new Float32Array(s),this.a=new Float32Array(s),this.b=new Float32Array(s),this.c=new Float32Array(s),this.pressure=new Float32Array(s),this.pressureNext=new Float32Array(s),this.divergence=new Float32Array(s),this.curl=new Float32Array(s)}get active(){return this.peak>2e-4}clear(){for(const t of[this.u,this.v,this.ink,this.a,this.b,this.c,this.pressure,this.pressureNext,this.divergence,this.curl,this.signal,this.pixels,this.offsetX,this.offsetY,this.driftX,this.driftY])t.fill(0);this.previous=null,this.peak=0}leave(){this.previous=null}move(t,i,e,s=performance.now()){if(!Number.isFinite(t+i+e+s))return;t=Math.max(0,Math.min(1,t)),i=Math.max(0,Math.min(1,i)),e=Math.max(.1,Math.min(1,e));const h=this.previous;if(this.previous={x:t,y:i,time:s},!h)return;const a=(t-h.x)*(this.width-1),r=(i-h.y)*(this.height-1),n=Math.hypot(a,r);if(n<.001)return;const o=this.span,l=s>h.time?Math.max(1/240,(s-h.time)/1e3):1/60,c=Math.min(12,n/o/l),u=1-Math.exp(-c/2.2),f=o*(.022+.075*e)*(.65+.95*u),d=Math.min(128,Math.max(1,Math.ceil(n/(.4*f)))),p=n/f*(.32+.22*u)/d,m=a/n,M=r/n,x=-M,v=m,y=n*(2+18*u)/d;for(let e=0;e<d;e++){const s=(e+.5)/d,a=(h.x+(t-h.x)*s)*(this.width-1),r=(h.y+(i-h.y)*s)*(this.height-1),n=Math.max(1,Math.floor(a-3*f)),o=Math.min(this.width-2,Math.ceil(a+3*f)),l=Math.max(1,Math.floor(r-3*f)),c=Math.min(this.height-2,Math.ceil(r+3*f));for(let t=l;t<=c;t++)for(let i=n;i<=o;i++){const e=((i-a)*m+(t-r)*M)/f,s=((i-a)*x+(t-r)*v)/f,h=Math.exp(-e*e*.65-s*s*1.7),n=t*this.width+i;this.ink[n]=1-(1-this.ink[n])*Math.exp(-h*p);const o=1-s*s*.55,l=e*s*.7;this.u[n]=Math.max(-80,Math.min(80,this.u[n]+(m*o+x*l)*h*y)),this.v[n]=Math.max(-80,Math.min(80,this.v[n]+(M*o+v*l)*h*y))}}this.peak=1}read(t,i,e){const s=0|(i=Math.max(.5,Math.min(this.width-1.5,i))),h=0|(e=Math.max(.5,Math.min(this.height-1.5,e))),a=i-s,r=e-h,n=h*this.width+s;return(t[n]*(1-a)+t[n+1]*a)*(1-r)+(t[n+this.width]*(1-a)+t[n+this.width+1]*a)*r}sample(t,i){return this.read(this.signal,t*(this.width-1),i*(this.height-1))}displacement(t,i,e){e[0]=this.read(this.offsetX,t*(this.width-1),i*(this.height-1)),e[1]=this.read(this.offsetY,t*(this.width-1),i*(this.height-1))}step(t){if(!this.active||!Number.isFinite(t))return;let i=Math.max(0,Math.min(.05,t));const e=this.width,s=this.height;for(;i>1e-7;){const t=Math.min(1/60,i);i-=t;const h=Math.exp(-1.9*t),a=Math.exp(-3.2*t);for(let i=1;i<s-1;i++)for(let r=1;r<e-1;r++){const n=i*e+r,o=Math.max(.5,Math.min(e-1.5,r-this.u[n]*t)),l=Math.max(.5,Math.min(s-1.5,i-this.v[n]*t)),c=0|o,u=0|l,f=o-c,d=l-u,p=u*e+c,m=(1-f)*(1-d),M=f*(1-d),x=(1-f)*d,v=f*d;this.a[n]=(this.u[p]*m+this.u[p+1]*M+this.u[p+e]*x+this.u[p+e+1]*v)*h,this.b[n]=(this.v[p]*m+this.v[p+1]*M+this.v[p+e]*x+this.v[p+e+1]*v)*h,this.c[n]=(this.ink[p]*m+this.ink[p+1]*M+this.ink[p+e]*x+this.ink[p+e+1]*v)*a}[this.u,this.a]=[this.a,this.u],[this.v,this.b]=[this.b,this.v],[this.ink,this.c]=[this.c,this.ink];for(let t=1;t<s-1;t++)for(let i=1;i<e-1;i++){const s=t*e+i;this.curl[s]=.5*(this.v[s+1]-this.v[s-1]-this.u[s+e]+this.u[s-e])}for(let i=1;i<s-1;i++)for(let s=1;s<e-1;s++){const h=i*e+s,a=Math.abs(this.curl[h+1])-Math.abs(this.curl[h-1]),r=Math.abs(this.curl[h+e])-Math.abs(this.curl[h-e]),n=Math.hypot(a,r)+1e-4,o=this.curl[h]*t*10,l=Math.hypot(this.u[h],this.v[h]);this.u[h]+=r/n*o,this.v[h]-=a/n*o;const c=Math.min(1,l/(Math.hypot(this.u[h],this.v[h])+1e-6));this.u[h]*=c,this.v[h]*=c,this.divergence[h]=-.5*(this.u[h+1]-this.u[h-1]+this.v[h+e]-this.v[h-e])}this.pressure.fill(0);for(let t=0;t<4;t++){for(let t=1;t<s-1;t++)for(let i=1;i<e-1;i++){const s=t*e+i;this.pressureNext[s]=.25*(this.divergence[s]+this.pressure[s-1]+this.pressure[s+1]+this.pressure[s-e]+this.pressure[s+e])}[this.pressure,this.pressureNext]=[this.pressureNext,this.pressure]}let r=0;for(let i=1;i<s-1;i++)for(let s=1;s<e-1;s++){const h=i*e+s;this.u[h]=Math.max(-80,Math.min(80,this.u[h]-.5*(this.pressure[h+1]-this.pressure[h-1]))),this.v[h]=Math.max(-80,Math.min(80,this.v[h]-.5*(this.pressure[h+e]-this.pressure[h-e]))),this.driftX[h]+=(3.6*this.u[h]-58*this.offsetX[h]-12*this.driftX[h])*t,this.driftY[h]+=(3.6*this.v[h]-58*this.offsetY[h]-12*this.driftY[h])*t,this.offsetX[h]=Math.max(-2,Math.min(2,this.offsetX[h]+this.driftX[h]*t)),this.offsetY[h]=Math.max(-2,Math.min(2,this.offsetY[h]+this.driftY[h]*t)),this.signal[h]=1-Math.exp(.06*-Math.hypot(this.u[h],this.v[h])-.16*this.ink[h]),r=Math.max(r,this.signal[h])}this.peak=r}if(this.active)for(let t=0;t<this.ink.length;t++){const i=Math.round(65535*Math.min(1,this.signal[t]));this.pixels[4*t]=i>>>8,this.pixels[4*t+1]=255&i,this.pixels[4*t+2]=Math.round(128+63.5*this.offsetX[t]),this.pixels[4*t+3]=Math.round(128+63.5*this.offsetY[t])}else this.clear()}},l={style:"trail",charset:" .:-=+*#%@",cellSize:9,zIndex:1e3},c='input, textarea, select, [contenteditable], dialog, [role="dialog"], [role="alertdialog"], [data-ascii-cursor-skip]',u={trail:64,wake:48,water:20,sparkle:140},f={strength:.62,radius:.22,span:540,cell:15},d=t=>{const i=Math.round(255*Math.max(0,Math.min(1,t)));return`rgb(${i},${i},${i})`};function p(t,i,e,s,h,a,r){for(let n=1;n<i.length;n++){const o=i[n-1],l=i[n],c=i[n-2]??o,u=1-(s-l.t)/h;u<=0||(t.strokeStyle=d(r(u)),t.lineWidth=Math.max(1,a(u)*e),t.beginPath(),t.moveTo((c.x+o.x)/2*e,(c.y+o.y)/2*e),t.quadraticCurveTo(o.x*e,o.y*e,(o.x+l.x)/2*e,(o.y+l.y)/2*e),t.lineTo(l.x*e,l.y*e),t.stroke())}}function m(t,i,e,s,h,a,r){if(t.lineCap="round",t.lineJoin="round",t.globalCompositeOperation="lighten","trail"===e)p(t,i,r,s,h,t=>a*(.5+.95*t),t=>t**1.25);else if("wake"===e)p(t,i,r,s,h,t=>a*(2+3.2*t),t=>.34*t**1.6),p(t,i,r,s,h,t=>a*(.8+1.4*t),t=>.62*t**1.4);else if("water"===e)for(const e of i){const i=s-e.t,n=1-i/h;n<=0||(t.strokeStyle=d(.9*n**1.5),t.lineWidth=Math.max(1,a*(.45+.5*n)*r),t.beginPath(),t.arc(e.x*r,e.y*r,(.8*a+.085*i)*r,0,2*Math.PI),t.stroke())}else for(const e of i){const i=s-e.t,n=1-i/h;if(n<=0)continue;const o=.55+.45*Math.sin(.028*i+6.28*e.seed),l=a*(.55+.6*e.seed)*(.6+.4*n)*r;t.fillStyle=d(n**1.2*o),t.fillRect((e.x+e.vx*i)*r-l/2,(e.y+e.vy*i)*r-l/2,l,l)}t.globalCompositeOperation="source-over"}function M(d={}){const p={current:{...l,...d}},M=()=>p.current;let x=[],v=null,y=null,g=!1,w=!1,k=!1,b=0;const A="undefined"!=typeof document&&"undefined"!=typeof window&&h(),F=()=>!!(M().reducedMotion??i()),X=()=>({mark:"glyph",charset:M().charset,cellSize:M().cellSize,..."trail"===M().style?{font:'"Geist", Arial, sans-serif',fontWeight:500,cellAspect:.8,glyphScale:.8,shade:0,levels:"none"}:{cellAspect:.62,glyphScale:.92,shade:.6},contrast:1,...M().look}),Y={trail:null,cols:0,rows:0,at:0,moved:0,lit:!1,art:null},S=()=>({...f,...M().trail}),z=()=>{const{cell:t,span:i}=S(),e=window.innerWidth||1280,s=window.innerHeight||800,h=Math.ceil(e/t)+2,a=Math.ceil(s/t)+2;return Y.trail&&Y.cols===h&&Y.rows===a&&Y.trail.span===i/t||(Y.trail=new o(h,a,i/t),Y.cols=h,Y.rows=a),Y.trail},C=t=>{t.style.cssText=`position:fixed;left:0;top:0;width:100vw;height:100vh;pointer-events:none;z-index:${M().zIndex};contain:strict`},I=t=>{if("trail"===M().style)return(({pen:t,now:i,scale:e})=>{const s=z(),h=Y.at?Math.max(0,i-Y.at)/1e3:1/60;if(Y.at=Math.max(Y.at,i),s.step(h),!s.active)return Y.at=0,Y.lit=!1;const{cols:a,rows:r}=Y,{strength:n,cell:o}=S(),l=s.pixels,c=Y.art??(Y.art=document.createElement("canvas")),u=3*a,f=3*r;c.width===u&&c.height===f||(c.width=u,c.height=f);const d=c.getContext("2d");if(!d)return Y.at=0,Y.lit=!1;const p=d.createImageData(u,f),m=p.data,M=t=>(256*l[4*t]+l[4*t+1])/65535;let x=0;for(let t=0;t<f;t++){const i=Math.min(r-1.001,Math.max(0,(t+.5)/3-.5)),e=0|i,s=i-e;for(let i=0;i<u;i++){const h=Math.min(a-1.001,Math.max(0,(i+.5)/3-.5)),r=0|h,o=h-r,l=e*a+r,c=((M(l)*(1-o)+M(l+1)*o)*(1-s)+(M(l+a)*(1-o)+M(l+a+1)*o)*s)*n,f=Math.min(1,c/.38),d=f*f*(3-2*f);d>x&&(x=d);const p=Math.round(255*d),v=4*(t*u+i);m[v]=m[v+1]=m[v+2]=p,m[v+3]=255}}return x<.05&&i-Y.moved>150?(s.clear(),Y.at=0,Y.lit=!1):(d.putImageData(p,0,0),t.imageSmoothingEnabled=!0,t.drawImage(c,-o/2*e,-o/2*e,a*o*e,r*o*e),Y.lit=!0)})(t);const{pen:i,now:e,scale:s}=t,h=M().life??("water"===M().style?950:800);return x=x.filter(t=>e-t.t<h),!!x.length&&(m(i,x,M().style,e,h,M().cellSize,s),!0)},N=()=>{v||g||w||!A||(y=document.createElement("div"),y.setAttribute("aria-hidden","true"),y.dataset.asciiCursor="",C(y),document.body.appendChild(y),v=s({host:y,kit:{get current(){return{lazy:!1,globalLook:M().globalLook??!1}}},look:X,ink:()=>M().ink??"--ascii-ink",resolution:.5,paint:I,reducedMotion:()=>M().reducedMotion,onFail:()=>{g=!0,v=null,y?.remove(),y=null}}),v||(g=!0,y.remove(),y=null))},$=()=>{x=[],Y.trail?.clear(),Y.lit=!1,Y.at=0},T=A?[a({target:window,move:t=>{if(w||k||"touch"===t.pointerType||"function"==typeof matchMedia&&matchMedia("(hover: none)").matches&&matchMedia("(pointer: coarse)").matches||F()||(t=>{const i=t;return!!i?.closest&&!!i.closest(M().skip?`${c}, ${M().skip}`:c)})(t.target))return void Y.trail?.leave();const i=performance.now(),s=t.clientX,h=t.clientY,a=M().style;if("trail"===a){const{cell:t,radius:e}=S(),a=z();return Y.at||(Y.at=i),Y.moved=i,a.move(s/(t*(Y.cols-1)),h/(t*(Y.rows-1)),e,i),Y.lit=!0,N(),void v?.kick()}const r=x[x.length-1],n=r?Math.hypot(s-r.x,h-r.y):1/0;if(!("water"===a?n<2.6*M().cellSize&&i-b<90:n<2)){if(b=i,"sparkle"===a)for(let t=0;t<2;t++){const a=e(i+17*t,s+h),r=e(h+t,i)*Math.PI*2,n=1.4*M().cellSize*e(s,i+t);x.push({x:s+Math.cos(r)*n,y:h+Math.sin(r)*n,t:i,seed:a,vx:.012*Math.cos(r),vy:.012*Math.sin(r)+.01})}else x.push({x:s,y:h,t:i,seed:0,vx:0,vy:0});for(;x.length>u[a];)x.shift();N(),v?.kick()}},leave(){}}),r(()=>{document.hidden&&$()}),n(()=>{F()&&$(),v?.kick()})]:[];return{get supported(){return A&&!g&&!w},get field(){return w?null:v?.field??null},get active(){return x.length>0||Y.lit},clear(){$(),v?.kick()},update(i){if(w)return;const e=M().style;p.current={...l,...t(p.current,i)},M().style!==e&&$(),y&&C(y),v?.refresh()},play(){w||(k=!1,v?.play())},pause(){w||(k=!0,$(),v?.pause())},destroy(){if(!w){w=!0;for(const t of T)t();$(),v?.destroy(),v=null,y?.remove(),y=null}}}}export{M as cursorTrail,m as cursorTrailFrame};//# sourceMappingURL=chunk-EEV6Y7NH.js.map
@@ -1 +0,0 @@
1
- import{poster as e,liveField as t,disposeKit as n,setPosterSource as o}from"./chunk-FEAFQY7O.js";import{isGlyphFieldSupported as r,watchPointer as i,requestFieldFrame as a,watchVisibility as s,watchTheme as c,watchMotionPreference as l,cancelFieldFrame as u,ambientFactor as d}from"../field.js";var p,f,h=(e,t=0,n=1)=>Math.min(n,Math.max(t,e)),m=(e,t)=>{const n=43758.5453*Math.sin(127.1*e+311.7*t);return n-Math.floor(n)},g=(e,t=3)=>1-(1-h(e))**t,y=(e,t,n)=>{const o=h((n-e)/(t-e));return o*o*(3-2*o)},v=e=>{if(e<=0)return 0;if(e>=1)return 1;const t=(e,t,n)=>3*t*e*(1-e)**2+3*n*e*e*(1-e)+e**3;let n=0,o=1,r=e;for(let i=0;i<22;i++)r=(n+o)/2,t(r,.16,.3)<e?n=r:o=r;return t(r,1,1)},b=()=>"function"==typeof matchMedia&&matchMedia("(prefers-reduced-motion: reduce)").matches,x=e=>e??b(),w={scene:"none",background:"transparent",color:"ink",levels:"none",fit:"cover",focus:{x:.5,y:.5},vignette:0,drift:0,hover:"none",shade:0,contrast:1,brightness:0,gamma:1,maxPixelRatio:2,clear:!1};function M(e,t,n){let o=(e??"").trim();const r=/^var\(\s*(--[\w-]+)\s*(?:,\s*(.+))?\)$/.exec(o);if(r?o=k(t,r[1])||(r[2]??"").trim():o.startsWith("--")&&(o=k(t,o)),!o)return n?M(n,t):null;if(/^#[\da-f]{6}$/i.test(o))return o.toLowerCase();if(/^#[\da-f]{3}$/i.test(o))return`#${[...o.slice(1)].map(e=>e+e).join("")}`.toLowerCase();const i=/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(o);if(i)return C([+i[1],+i[2],+i[3]]);if(void 0===f)try{f="undefined"==typeof document?null:Object.assign(document.createElement("canvas"),{width:1,height:1}).getContext("2d",{willReadFrequently:!0})}catch{f=null}if(!f)return n?M(n,t):null;f.clearRect(0,0,1,1),f.fillStyle="#000",f.fillStyle=o,f.fillRect(0,0,1,1);const[a,s,c]=f.getImageData(0,0,1,1).data;return C([a,s,c])}var k=(e,t)=>"function"==typeof getComputedStyle?getComputedStyle(e).getPropertyValue(t).trim():"",C=e=>`#${e.map(e=>Math.round(h(e,0,255)).toString(16).padStart(2,"0")).join("")}`,R=e=>{const t=parseInt(e.slice(1,7),16);return[t>>16&255,t>>8&255,255&t]},S=(e,t,n)=>{const o=R(e),r=R(t);return C(o.map((e,t)=>e+(r[t]-e)*n))};function $(e){const t="function"==typeof getComputedStyle?getComputedStyle(e):null,n=/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?/i.exec(t?.backgroundColor??"");if(n){if((void 0===n[4]?1:n[4].endsWith("%")?parseFloat(n[4])/100:parseFloat(n[4]))>=.5)return.2126*+n[1]+.7152*+n[2]+.0722*+n[3]<128}const o=M(t?.color??"",e);if(!o)return"function"==typeof matchMedia&&matchMedia("(prefers-color-scheme: dark)").matches;const[r,i,a]=R(o);return.2126*r+.7152*i+.0722*a>140}function O(){const e=[];return{set(t,n,o){e.some(([e,o])=>e===t&&o===n)||e.push([t,n,t.style.getPropertyValue(n)]),t.style.setProperty(n,o)},restore(){for(const[t,n,o]of e.reverse())o?t.style.setProperty(n,o):t.style.removeProperty(n);e.length=0}}}function j(e="",t=!1){const n=document.createElement("span");return n.setAttribute("style","position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0"),t&&n.setAttribute("aria-live","polite"),n.textContent=e,n}function E(e,t){const n=e.getBoundingClientRect(),o=t.getBoundingClientRect(),r=n.width/(e.offsetWidth||n.width||1)||1;return{x:(o.left-n.left)/r,y:(o.top-n.top)/r,w:o.width/r,h:o.height/r}}function F(e,t){const n={...e,...t},o=e.look,r=t.look;return r&&(n.look=Object.fromEntries(Object.entries({...o,...r}).filter(([,e])=>void 0!==e))),n}var P=new WeakMap,W=e=>P.get(e)?.()??null;function A(n){const{host:o,layer:f="over",outset:h=0,kit:m}=n;if("undefined"==typeof document)return null;const g=`position:absolute;left:${-h}px;top:${-h}px;width:calc(100% + ${2*h}px);height:calc(100% + ${2*h}px);pointer-events:none;z-index:${"under"===f?-1:"auto"}`;if(!r())return m?.current.poster&&e(o,m.current.poster,g,m),null;const y=O(),v=document.createElement("canvas"),x=document.createElement("canvas"),k=x.getContext("2d");if(!k)return null;"static"===getComputedStyle(o).position&&y.set(o,"position","relative"),"under"===f&&y.set(o,"isolation","isolate"),v.setAttribute("aria-hidden","true"),v.dataset.asciiKit="";const C=v.style;C.position="absolute",C.left=C.top=-h+"px",C.width=C.height=`calc(100% + ${2*h}px)`,C.pointerEvents="none",C.zIndex="under"===f?"-1":"",o.appendChild(v);const R=()=>{const e=getComputedStyle(o);H.canvas.style.borderRadius=[e.borderTopLeftRadius,e.borderTopRightRadius,e.borderBottomRightRadius,e.borderBottomLeftRadius].map(e=>h&&/^[\d.]+px$/.test(e)?`${parseFloat(e)+h}px`:e).join(" ")};let S=$(o);const j=()=>M(n.ink(S),o)??M(getComputedStyle(o).color,o)??(S?"#d8d6cf":"#1d1d1b");let F=!1,W=!1,A=!0,I=!1,z=-1/0,T=-1/0;const B=i({target:o,move:e=>{T=e.timeStamp},leave(){}}),H=t({host:o,canvas:v,kit:m,place:g,options:()=>({...w,...n.look(),ink:j(),interactionTarget:o,label:""}),onMount:(e,t)=>{P.set(t,K),t.removeAttribute("role"),t.removeAttribute("aria-label"),e.ready.then(Y,()=>{}),Y()},onRelease:()=>{F&&u(Q),F=!1},onFail:()=>{ee(),n.onFail?.()}}),L=H.field;R();const D=()=>!!(n.reducedMotion()??b()),V=e=>E(H.canvas,e),q=e=>{const t=H.canvas,r=Math.max(1,t.clientWidth||o.clientWidth+2*h),i=Math.max(1,t.clientHeight||o.clientHeight+2*h),a=n.resolution??1,[s,c]="function"==typeof a?a(r,i):[Math.max(1,Math.round(r*a)),Math.max(1,Math.round(i*a))];x.width===s&&x.height===c||(x.width=s,x.height=c),k.setTransform(1,0,0,1,0,0),k.globalAlpha=1,k.globalCompositeOperation="source-over",k.filter="none",k.fillStyle="#000",k.fillRect(0,0,s,c);let l=!1;try{l=n.paint({pen:k,width:s,height:c,w:r,h:i,scale:s/r,now:e,still:D(),dark:S,box:V})}catch(e){"undefined"!=typeof console&&console.error(e)}return L.setSource(x),I||L.redraw(),l};function K(){const e=H.canvas,t=Math.max(1,e.clientWidth||o.clientWidth+2*h),r=Math.max(1,e.clientHeight||o.clientHeight+2*h),i=n.resolution??1,[a,s]="function"==typeof i?i(t,r):[Math.max(1,Math.round(t*i)),Math.max(1,Math.round(r*i))],c=document.createElement("canvas");c.width=a,c.height=s;const l=c.getContext("2d");if(!l)return null;l.fillStyle="#000",l.fillRect(0,0,a,s);try{n.paint({pen:l,width:a,height:s,w:t,h:r,scale:a/t,now:performance.now(),still:!0,dark:S,box:V})}catch{return null}return c}function Q(e){if(F=!1,I||W||!H.live)return;if(e-z<(p??(p=(()=>{const e="undefined"==typeof navigator?void 0:navigator;return 1e3/(e&&(e.hardwareConcurrency>0&&e.hardwareConcurrency<=2||e.deviceMemory&&e.deviceMemory<=2||e.connection?.saveData)?30:60)-2})()))*d()&&e-T>1e3)return F=!0,void a(Q);z=e;!q(e)||D()||!A||"undefined"!=typeof document&&document.hidden||(F=!0,a(Q))}function Y(){F||I||W||!H.live||(F=!0,a(Q))}P.set(v,K);const G="function"==typeof ResizeObserver?new ResizeObserver(()=>{R(),L.resize(),!H.live||I||W||q(performance.now()),Y()}):null;G?.observe(o);const J="function"==typeof IntersectionObserver?new IntersectionObserver(([e])=>{A=e.isIntersecting,A&&Y()},{rootMargin:"64px"}):null;J?.observe(o);const N=s(()=>{document.hidden||Y()});let U=!1;const X=()=>{U=!1,I||(S=$(o),L.update({ink:j()}),n.onTheme?.(),Y())},Z=c(()=>{U||I||(U=!0,a(X))}),_=l(Y);function ee(){I||(I=!0,F&&u(Q),U&&u(X),F=U=!1,G?.disconnect(),J?.disconnect(),N(),B(),Z(),_(),H.destroy(),y.restore(),x.width=x.height=1)}return H.start(),H.failed?(ee(),null):{field:L,get canvas(){return H.canvas},get live(){return H.live},kick:Y,refresh(){I||(S=$(o),L.update({...w,...n.look(),ink:j()}),Y())},still:D,dark:()=>S,box:V,play(){W&&(W=!1,L.play(),Y())},pause(){W=!0,F&&u(Q),F=!1,L.pause()},destroy:ee}}function I(e,t,r,i,a=()=>{}){let s=!1;const c={get supported(){return!!e()&&!s},get field(){return s?null:e()?.field??null},update(e){if(s)return;t.current=F(t.current,e);const n=e.poster;n&&o(t,n),r(t.current)},play(){s||e()?.play()},pause(){s||e()?.pause()},destroy(){s||(s=!0,e()?.destroy(),n(t),a())}};return Object.defineProperties(i,Object.getOwnPropertyDescriptors(c))}export{w as BASE_LOOK,E as boxIn,h as clamp,g as easeOut,I as handle,m as hash,R as hexRgb,v as house,$ as isDark,F as mergeOptions,S as mixHex,A as mountSurface,b as prefersReduced,x as reducedFrom,M as resolveColor,W as settledSource,y as smoothstep,j as srOnly,O as styleKeeper,C as toHex};//# sourceMappingURL=chunk-GF72XM4C.js.map
@@ -1 +0,0 @@
1
- import{parseColor as t,hash2 as e}from"./chunk-ZCHGV2CV.js";import{mountCanvas as o}from"./chunk-3OD5BMF5.js";function r(o,r,c,n,l={}){const{fontSize:i=14,chars:s=" ·∙•:-=+*#",color:a,accentColor:h,speed:f=.5,harmonics:m=3,lightMode:p=!1}=l,M=h??(p?"#6b8700":"#d4ff00"),g=.62*i,d=1.4*i,$=Math.ceil(r/g),u=Math.ceil(c/d);o.clearRect(0,0,r,c),o.font=`${i}px monospace`,o.textBaseline="top";let b=255,S=255,x=255;if(p&&(b=55,S=55,x=55),a){const e=t(a);e&&(b=e.r,S=e.g,x=e.b)}let z=212,C=255,k=0;const y=t(M);y&&(z=y.r,C=y.g,k=y.b);const j=n*f,v=Array.from({length:m},(t,e)=>1/(e+1)).reduce((t,e)=>t+e,0);for(let t=0;t<u;t++)for(let r=0;r<$;r++){let c=0;for(let o=0;o<m;o++){const n=.18+1.4*e(r*(o+3)+7,t*(o+5)+11),l=e(r*(o+7),t*(o+9)+3)*Math.PI*2,i=1/(o+1);c+=Math.sin(j*n+l)*i}const n=.5*(c/v+1);if(n<.28)continue;const l=(n-.28)/.72,i=s[Math.min(s.length-1,Math.floor(l*s.length))],a=n>.88,h=p?.82*l:.13*l;o.fillStyle=a?`rgba(${z},${C},${k},${p?.92:.28})`:`rgba(${b},${S},${x},${h})`,o.fillText(i,r*g,t*d)}}var c={id:"morph",name:"Morph",description:"Every cell shimmers at its own frequencies.",cellSize:14,speed:1,cursor:!1,mount:t=>o(t,(t,e,o,c,n,l)=>r(t,e,o,c,{fontSize:l.fontSize,chars:l.chars,lightMode:l.lightMode,color:l.ink,accentColor:l.accent}))};export{c as morph,r as renderMorphBackground};//# sourceMappingURL=chunk-GKR3ROYR.js.map
@@ -1 +0,0 @@
1
- import{blank as t,mountPageField as e,pageHandle as n}from"./chunk-7DXSG526.js";import{styleKeeper as r}from"./chunk-GF72XM4C.js";var o={font:'Georgia, "Times New Roman", serif',fontWeight:600};function i(e,n=o.font,r=o.fontWeight,i=520){const{canvas:c,pen:l}=t(1800,Math.max(1,Math.round(i))),s=Math.min(400,1700/Math.max(3,.56*[...e].length));return l.fillStyle="#fff",l.textAlign="center",l.textBaseline="middle",l.font=`${r} ${s}px ${n}`,l.fillText(e||" ",c.width/2,c.height/2+.05*s),c}function c(t,c={}){const l=(t.textContent??"").trim(),s={current:{...o,...c}};let a=s.current.text??l;const u=r(),f=()=>i(a,s.current.font,s.current.fontWeight,s.current.artHeight),d=!l;let h=null;h=e({host:t,layer:"over",look:()=>({mark:"glyph",scene:"none",charset:" .:-=+*#%@",font:'"Geist", Arial, sans-serif',fontWeight:500,cellSize:7,cellAspect:.8,glyphScale:.8,color:"ink",shade:0,contrast:1.3,levels:"auto",fit:"contain",focus:{x:.5,y:.5},hover:"trail",trail:{strength:.62,radius:.22},...s.current.look}),ink:()=>s.current.ink??"--ascii-ink-strong",source:f(),label:d?s.current.label??a:void 0,kit:s,reducedMotion:()=>s.current.reducedMotion,onFail:()=>{h=null,u.restore()}}),h&&l&&u.set(t,"color","transparent"),h&&document.fonts?.load?.(`${s.current.fontWeight} 100px ${s.current.font}`).then(()=>{h&&h.field.setSource(f())},()=>{});const x=e=>{if(e!==a)if(a=e,void 0!==s.current.text&&(s.current={...s.current,text:a}),h)if(h.field.setSource(f()),d)h.field.update({label:s.current.label??(a||" ")});else{for(const e of[...t.childNodes])e!==h.canvas&&e.remove();t.prepend(document.createTextNode(a))}else l&&(t.textContent=a)};return n(()=>h,s,t=>{void 0!==t.text&&t.text!==a?x(t.text):h&&h.field.setSource(f())},{setText:x,get text(){return a}},()=>u.restore())}export{i as wordArt,c as wordmark};//# sourceMappingURL=chunk-I5R2347P.js.map
@@ -1 +0,0 @@
1
- import{parseColor as t}from"./chunk-ZCHGV2CV.js";import{mountCanvas as e}from"./chunk-3OD5BMF5.js";function o(e,o,n,c,r={x:.5,y:.5},l={}){const{fontSize:s=14,chars:a=". · ○ ◎ ●",accentColor:i,color:h,rings:f=5,speed:M=1,sharpness:g=4,lightMode:p=!1}=l,m=i??(p?"#007a5e":"#00ffcc");e.clearRect(0,0,o,n),e.textBaseline="middle",e.textAlign="center";const u=o*r.x,d=n*r.y,x=1.6*Math.sqrt(u*u+d*d)+.2*Math.sqrt(o*o+n*n);let $=255,b=255,S=255;if(p&&($=55,b=55,S=55),h){const e=t(h);e&&($=e.r,b=e.g,S=e.b)}let z=0,C=255,k=204;const q=t(m);q&&(z=q.r,C=q.g,k=q.b);const y=a.replace(/ /g,"").split("");if(0===y.length)return;const j=Math.ceil(o/s),A=Math.ceil(n/s);for(let t=0;t<A;t++)for(let o=0;o<j;o++){const n=o*s+.5*s,r=t*s+.5*s,l=n-u,a=r-d,i=Math.sqrt(l*l+a*a)/x;let h=0;for(let t=0;t<f;t++){const e=(c*M*.38+t/f)%1,o=Math.abs(i-e),n=Math.max(0,1-o*x/(s*(12-g)));h+=Math.cos(n*Math.PI*.5)*n}if(h=Math.min(1,h),h<.02)continue;const m=h>.6;e.font=`${s}px monospace`;const q=Math.floor(h*(y.length-1)),j=y[Math.min(q,y.length-1)],A=p?.88*h:.22*h;e.fillStyle=m?`rgba(${z},${C},${k},${Math.min(p?.95:.4,.55*h)})`:`rgba(${$},${b},${S},${A})`,e.fillText(j,n,r)}e.textAlign="left"}var n={id:"pulse",name:"Pulse",description:"Rings of characters ripple out from the cursor.",cellSize:14,speed:1,cursor:!0,mount:t=>e(t,(t,e,n,c,r,l)=>o(t,e,n,c,r,{fontSize:l.fontSize,chars:l.chars,lightMode:l.lightMode,color:l.ink,accentColor:l.accent}))};export{n as pulse,o as renderPulseBackground};//# sourceMappingURL=chunk-KEE55QSD.js.map
@@ -1 +0,0 @@
1
- import{MONO as t,GRAPHITE_LIGHT as e,GRAPHITE_DARK as o,pixelSnap as r,parseColor as n,band as c,ditherIndex as i,toneAlpha as a,vnoise as l}from"./chunk-ZCHGV2CV.js";import{mountCanvas as s}from"./chunk-3OD5BMF5.js";function h(s,h,f,d,g={x:.5,y:.5},u={}){const{fontSize:M=13,chars:m=" .:-=+*#%@",accentColor:p,color:$,octaves:x=4,speed:S=1,scale:b=1,accentThreshold:y=.9,mouseWarp:z=.3,lightMode:C=!1}=u,k=Array.from(m),j=.62*M,v=1.3*M,w=Math.ceil(h/j),A=Math.ceil(f/v);s.clearRect(0,0,h,f),s.font=`${M}px ${t}`,s.textBaseline="top";const B=C?e:o;let T=B.r,V=B.g,q=B.b;const D=r(s);if($){const t=n($);t&&(T=t.r,V=t.g,q=t.b)}let F=T,G=V,H=q;const N=p?n(p):null;N&&(F=N.r,G=N.g,H=N.b);const O=.035*b,R=d*S,W=Math.min(6,Math.max(1,x)),Z=(t,e)=>{let o=0,r=.5,n=1,c=0;for(let i=0;i<W;i++)o+=l(t*n,e*n)*r,c+=r,r*=.5,n*=2.1;return o/c};for(let t=0;t<A;t++)for(let e=0;e<w;e++){const o=e*O+.06*R,r=t*O*1.3-.04*R,n=e/w-g.x,l=t/A-g.y,h=Math.sqrt(n*n+l*l),f=z>0?.12*Math.max(0,1-h/z):0,d=Z(o+f*Math.sin(1.3*R+8*l),r+f*Math.cos(.9*R+8*n)),M=c(.5*d+.5,.7,.83);if(M<=0)continue;const m=k[i(M,k.length,e,t,u.ditherStrength)];if(" "===m)continue;const p=!!N&&M>y,$=a(M);s.fillStyle=p?`rgba(${F},${G},${H},${$})`:`rgba(${T},${V},${q},${$})`,s.fillText(m,D(e*j),D(t*v))}}var f={id:"noise",name:"Noise",description:"A slow fractal noise field that warps around the cursor.",cellSize:13,speed:.5,cursor:!0,mount:t=>s(t,(t,e,o,r,n,c)=>h(t,e,o,r,n,{ditherStrength:c.ditherStrength,fontSize:c.fontSize,chars:c.chars,lightMode:c.lightMode,color:c.ink,accentColor:c.accent}))};export{f as noise,h as renderNoiseBackground};//# sourceMappingURL=chunk-M5VVLYI2.js.map
@@ -1 +0,0 @@
1
- import{parseColor as t,hash2 as e}from"./chunk-ZCHGV2CV.js";import{mountCanvas as o}from"./chunk-3OD5BMF5.js";function n(o,n,c,r,s={x:.5,y:.5},l={}){const{fontSize:a=14,chars:i=" . · * + ° ★",accentColor:h,color:f,speed:d=1,count:g=180,lightMode:M=!1}=l,m=h??(M?"#6b8700":"#d4ff00");o.clearRect(0,0,n,c),o.textBaseline="middle",o.textAlign="center";const p=n*(.2+.6*s.x),x=c*(.2+.6*s.y),$=.65*Math.sqrt(n*n+c*c);let u=255,S=255,b=255;if(M&&(u=55,S=55,b=55),f){const e=t(f);e&&(u=e.r,S=e.g,b=e.b)}let k=212,z=255,C=0;const y=t(m);y&&(k=y.r,z=y.g,C=y.b);const A=i.replace(/ /g,"").split("");if(0!==A.length){for(let t=0;t<g;t++){const s=e(17*t,3)*Math.PI*2,l=(r*(.15+.85*e(31*t,7))*d*.22+e(13*t,11))%1,i=p+Math.cos(s)*l*$,h=x+Math.sin(s)*l*$;if(i<-20||i>n+20||h<-20||h>c+20)continue;const f=Math.max(6,a*(.4+.9*l));o.font=`${f}px monospace`;const g=A[Math.min(A.length-1,Math.floor(l*A.length))],m=l>.72,y=M?.85*l:.2*l;o.fillStyle=m?`rgba(${k},${z},${C},${Math.min(M?.92:.32,2.2*y)})`:`rgba(${u},${S},${b},${y})`,o.fillText(g,i,h)}o.textAlign="left"}}var c={id:"stars",name:"Stars",description:"A sparse starfield that twinkles, drifts and leans toward the cursor.",cellSize:14,speed:1,cursor:!0,mount:t=>o(t,(t,e,o,c,r,s)=>n(t,e,o,c,r,{fontSize:s.fontSize,chars:s.chars,lightMode:s.lightMode,color:s.ink,accentColor:s.accent}))};export{n as renderStarsBackground,c as stars};//# sourceMappingURL=chunk-ORSI57T2.js.map