asciify-engine 3.0.0 → 4.1.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.
@@ -0,0 +1,323 @@
1
+ type ColorMode = 'grayscale' | 'fullcolor' | 'matrix' | 'accent';
2
+ type RenderMode = 'ascii' | 'dots';
3
+ type AnimationStyle = 'none' | 'caustics' | 'current' | 'reform';
4
+ /** Core media draws ASCII/dots; use StudioStyle for the 15 render styles. */
5
+ type ArtStyle = 'classic';
6
+ type HoverEffect = 'spotlight' | 'magnify' | 'repel' | 'glow' | 'colorShift' | 'attract' | 'shatter' | 'trail' | 'glitchText';
7
+ type HoverShape = 'circle' | 'box';
8
+ type HoverPreset = 'none' | 'subtle' | 'flashlight' | 'magnifier' | 'forceField' | 'neon' | 'fire' | 'ice' | 'gravity' | 'shatter' | 'ghost' | 'glitchReveal';
9
+ type SourceCropUnit = 'percent' | 'pixel';
10
+ type SourceCropAnchor = 'center' | 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
11
+ interface SourceCrop {
12
+ /** Legacy crop origin on the source media. Percent values use 0–1. Default: centered when cropped */
13
+ x?: number;
14
+ /** Legacy crop origin on the source media. Percent values use 0–1. Default: centered when cropped */
15
+ y?: number;
16
+ /** Legacy crop width on the source media. Percent values use 0–1. Default: mirrors `height` to preserve proportions */
17
+ width?: number;
18
+ /** Legacy crop height on the source media. Percent values use 0–1. Default: mirrors `width` to preserve proportions */
19
+ height?: number;
20
+ /** CSS-like crop inset from the top edge. Percent values use 0–1. */
21
+ top?: number;
22
+ /** CSS-like crop inset from the right edge. Percent values use 0–1. */
23
+ right?: number;
24
+ /** CSS-like crop inset from the bottom edge. Percent values use 0–1. */
25
+ bottom?: number;
26
+ /** CSS-like crop inset from the left edge. Percent values use 0–1. */
27
+ left?: number;
28
+ /**
29
+ * Keep the source aspect ratio when deriving a crop from a single `width` or
30
+ * `height`. CSS-like side insets (`top`, `right`, `bottom`, `left`) already
31
+ * define an exact source view box, and `asciifyVideo` sizes from that crop
32
+ * aspect so the output does not stretch. Default: `true`.
33
+ */
34
+ preserveAspect?: boolean;
35
+ /** Bias auto-centered crops toward an edge or corner. Default: `'center'` */
36
+ anchor?: SourceCropAnchor;
37
+ /** Interpret values as normalized percentages or source pixels. Default: `'percent'` */
38
+ unit?: SourceCropUnit;
39
+ }
40
+ /**
41
+ * Named colour palette presets — pass as `colorMode` for themed rendering.
42
+ * Applied as a post-process colour remap over the standard grayscale output.
43
+ */
44
+ type PaletteTheme = 'dracula' | 'monokai' | 'nord' | 'catppuccin' | 'solarized' | 'gruvbox';
45
+ declare const PALETTE_THEMES: Record<PaletteTheme, {
46
+ name: string;
47
+ accent: string;
48
+ bg: string;
49
+ fg: string;
50
+ }>;
51
+ type SourceType = 'image' | 'video' | 'gif' | null;
52
+ interface AsciiOptions {
53
+ /** Character cell width in pixels. Smaller = more detail, more cells. Default: `7` */
54
+ fontSize: number;
55
+ /** Extra horizontal spacing between characters (pixels). Default: `1` */
56
+ charSpacing: number;
57
+ /**
58
+ * Brightness adjustment applied before luminance mapping.
59
+ * Range −1 (black) → 0 (unchanged) → 1 (white). Default: `0`
60
+ */
61
+ brightness: number;
62
+ /**
63
+ * Contrast boost applied before luminance mapping.
64
+ * `0` = unchanged, positive values increase contrast, negative decrease it.
65
+ * Default: `0`
66
+ */
67
+ contrast: number;
68
+ /**
69
+ * Character density ramp — ordered from lightest to darkest.
70
+ * Use `CHARACTER_SETS.standard.chars` for a curated ramp or supply your own string.
71
+ * Default: `' .:-=+*#%@'`
72
+ */
73
+ charset: string;
74
+ /**
75
+ * Colour output mode.
76
+ * - `'grayscale'` — white-on-black monochrome
77
+ * - `'fullcolor'` — samples pixel colours from the source
78
+ * - `'matrix'` — green phosphor terminal look
79
+ * - `'accent'` — single accent colour with intensity-driven brightness
80
+ * Default: `'grayscale'`
81
+ */
82
+ colorMode: ColorMode;
83
+ /**
84
+ * The accent colour used when `colorMode` is `'accent'` or `'matrix'`.
85
+ * Any CSS colour string. Default: `'#d4ff00'`
86
+ */
87
+ /**
88
+ * Hex color string used when `colorMode` is `'accent'`.
89
+ * Set to `'auto'` to let the engine pick contrasting ink automatically:
90
+ * dark ink (`#0d0d0d`) in light mode, light ink (`#faf9f7`) in dark mode.
91
+ */
92
+ accentColor: string;
93
+ /**
94
+ * Invert luminance mapping (light pixels → dense chars). Default: `false`
95
+ *
96
+ * Set to `'auto'` to let the engine detect the OS color scheme:
97
+ * - Light mode → inverts (bright pixel = dark char = visible on white background)
98
+ * - Dark mode → normal (bright pixel = bright char = visible on dark background)
99
+ */
100
+ invert: boolean | 'auto';
101
+ /**
102
+ * Render mode.
103
+ * - `'ascii'` — characters drawn as text
104
+ * - `'dots'` — each cell rendered as a filled circle (particle look)
105
+ * Default: `'ascii'`
106
+ */
107
+ renderMode: RenderMode;
108
+ /**
109
+ * Per-character animation driven over time.
110
+ * Applies wave, glitch, rain-drop, spiral, and other effects to the rendered text.
111
+ * Default: `'none'`
112
+ */
113
+ animationStyle: AnimationStyle;
114
+ /** Speed multiplier for `animationStyle` effects. Default: `1` */
115
+ animationSpeed: number;
116
+ /**
117
+ * Size of each dot relative to the cell when `renderMode === 'dots'`.
118
+ * `1` fills the whole cell, `0.5` draws half-size circles. Default: `0.8`
119
+ */
120
+ dotSizeRatio: number;
121
+ /**
122
+ * Ordered Bayer (4×4) dither strength applied to the luminance map.
123
+ * `0` = no dithering, `1` = full dithering. Default: `0`
124
+ */
125
+ ditherStrength: number;
126
+ /**
127
+ * Assumed aspect ratio (width ÷ height) of a single output character.
128
+ * Controls how many rows vs columns are generated — must match your rendering
129
+ * environment to preserve the source image's proportions.
130
+ * - `0.55` — browser monospace at `line-height: 1.09` (default)
131
+ * - `0.52` — browser monospace at `line-height: 1.15`
132
+ * - `0.5` — most terminal emulators
133
+ * Default: `0.55`
134
+ */
135
+ charAspect: number;
136
+ /**
137
+ * Auto-stretch the luminance range before charset mapping.
138
+ * When `true`, the darkest pixel in the frame maps to the first charset character
139
+ * and the brightest to the last, maximising perceived detail and contrast.
140
+ * Particularly useful for images with low inherent contrast or muted tones.
141
+ * Default: `false`
142
+ */
143
+ normalize: boolean;
144
+ /**
145
+ * Overall intensity of the hover / cursor interaction effect.
146
+ * `0` disables the effect. Default: `0`
147
+ */
148
+ hoverStrength: number;
149
+ /**
150
+ * Radius of the hover interaction zone as a fraction of the canvas size.
151
+ * `0.1` = 10% of canvas width. Default: `0.2`
152
+ */
153
+ hoverRadius: number;
154
+ /**
155
+ * Which cursor interaction style to apply when `hoverStrength > 0`.
156
+ * See `HoverPreset` or `HOVER_PRESETS` for ready-made configurations.
157
+ * Default: `'spotlight'`
158
+ */
159
+ hoverEffect: HoverEffect;
160
+ /**
161
+ * Tint colour used by hover effects such as spotlight, glow, and colorShift.
162
+ * Any CSS colour string. Default: `'#ffffff'`
163
+ */
164
+ hoverColor: string;
165
+ /**
166
+ * Shape of the hover interaction zone.
167
+ * - `'circle'` — radial falloff (default)
168
+ * - `'box'` — rectangular zone, text fills a block around the cursor
169
+ * Default: `'circle'`
170
+ */
171
+ hoverShape: HoverShape;
172
+ /**
173
+ * Text string revealed by the `'glitchText'` hover effect.
174
+ * Characters around the cursor scramble and resolve into this text.
175
+ * Supports `\n` for multi-line reveals.
176
+ *
177
+ * Pass an **array** of strings to cycle through different words based on
178
+ * the cursor's grid region — the engine picks one via a spatial hash so
179
+ * each area of the canvas shows a different word.
180
+ *
181
+ * Default: `'ASCIIFY'`
182
+ *
183
+ * @example
184
+ * // Single word:
185
+ * options: { hoverEffect: 'glitchText', hoverText: 'HELLO' }
186
+ *
187
+ * // Word pool — different text at different cursor positions:
188
+ * options: { hoverEffect: 'glitchText', hoverText: ['DEPLOY', 'SCALE', 'SHIP', 'BUILD'] }
189
+ */
190
+ hoverText: string | string[];
191
+ /**
192
+ * Core renderer discriminator. No legacy preset lookup is performed.
193
+ * Core rendering uses `classic`; select the current render styles through `/studio`.
194
+ */
195
+ artStyle: ArtStyle;
196
+ /** Explicit fine-dither treatment for the optional hover compositor. */
197
+ fineDither?: boolean;
198
+ /**
199
+ * Custom repeating text used when `artStyle` is `'letters'` or when you set
200
+ * a custom charset. Leave empty to use the selected `charset`. Default: `''`
201
+ */
202
+ customText: string;
203
+ /**
204
+ * Chroma-key colour to remove from the source (green screen / blue screen).
205
+ * Pixels whose RGB distance from this colour is within `chromaKeyTolerance`
206
+ * are keyed out: their character becomes a space and alpha is set to 0,
207
+ * letting the canvas background show through.
208
+ *
209
+ * - `true` — **smart green screen**: heuristic detection (`g > r*1.4 && g > b*1.4`)
210
+ * catches every shade of green screen (lime, broadcast, chroma green) with no
211
+ * config. Works on virtually all studio/streaming footage.
212
+ * - `'blue-screen'` — **smart blue screen**: same heuristic for blue.
213
+ * - `{ r, g, b }` or CSS colour string — custom key colour + `chromaKeyTolerance`
214
+ * - `null` / `false` — disabled (default)
215
+ *
216
+ * @example
217
+ * // Standard green screen — zero config:
218
+ * options: { chromaKey: true }
219
+ *
220
+ * // Blue screen — zero config:
221
+ * options: { chromaKey: 'blue-screen' }
222
+ *
223
+ * // Custom CSS colour:
224
+ * options: { chromaKey: '#00b140', chromaKeyTolerance: 70 }
225
+ */
226
+ chromaKey: {
227
+ r: number;
228
+ g: number;
229
+ b: number;
230
+ } | string | boolean | null;
231
+ /**
232
+ * Euclidean RGB tolerance radius for chroma-key detection.
233
+ * `0` = exact match only, `441` ≈ key out everything.
234
+ * Higher values remove more pixels. Default: `60`
235
+ */
236
+ chromaKeyTolerance: number;
237
+ /**
238
+ * Extra normalized padding kept around the detected chroma-key foreground when
239
+ * `asciifyVideo` auto-trims keyed video content inside `sourceCrop`.
240
+ * `0` hugs the visible foreground. `0.01` keeps 1% safety padding.
241
+ * Default: `0.002`
242
+ */
243
+ chromaKeyTrimPadding: number;
244
+ /**
245
+ * Minimum source-pixel luminance for chroma-key foreground auto-trim.
246
+ * Raise this when faint edge noise is counted as content and prevents a
247
+ * keyed subject from filling a preserved-ratio layout. Range `0–255`.
248
+ * Default: `0`
249
+ */
250
+ chromaKeyTrimLuminanceThreshold: number;
251
+ /**
252
+ * How keyed video foreground trimming is applied when `chromaKey` and
253
+ * `sourceCrop` are enabled.
254
+ *
255
+ * - `'range'` samples the video/scroll range once and keeps a stable union
256
+ * crop. Best default for consistent framing.
257
+ * - `'frame'` remeasures each rendered frame and expands the crop to the
258
+ * render aspect. Best for scroll-scrubbed hero footage where foreground
259
+ * position changes and pure contain leaves empty edges.
260
+ * - `'off'` disables automatic keyed foreground trimming.
261
+ *
262
+ * Default: `'range'`
263
+ */
264
+ chromaKeyTrimMode: 'range' | 'frame' | 'off';
265
+ /**
266
+ * Optional source crop applied before ASCII sampling.
267
+ *
268
+ * Use this when the media has empty edges or a subject that should be framed
269
+ * tighter without changing the destination canvas size.
270
+ *
271
+ * @example
272
+ * // Remove 15% from the top and 15% from the bottom, preserving proportions:
273
+ * options: { sourceCrop: { top: 0.15, bottom: 0.15 } }
274
+ *
275
+ * @example
276
+ * // Keep 70% of the source, preserve proportions, and center it:
277
+ * options: { sourceCrop: { height: 0.7 } }
278
+ *
279
+ * @example
280
+ * // Use an exact source window when manual framing should override proportions:
281
+ * options: { sourceCrop: { x: 0.05, y: 0.08, width: 0.9, height: 0.9, preserveAspect: false } }
282
+ */
283
+ sourceCrop?: SourceCrop | null;
284
+ /**
285
+ * Array of charset strings to cycle through over time.
286
+ * The engine picks `charsetFrames[Math.floor(time * charsetFps) % length]` on each
287
+ * render tick, re-mapping characters from stored cell luminance.
288
+ * Pairs beautifully with `animationStyle` and `asciiBackground`.
289
+ *
290
+ * Supply your own array of character ramps:
291
+ * @example
292
+ * options: { charsetFrames: [CHARACTER_SETS.standard.chars, CHARACTER_SETS.blocks.chars] }
293
+ */
294
+ charsetFrames?: string[];
295
+ /** Cycle rate for `charsetFrames` in frames-per-second. Default: `2` */
296
+ charsetFps?: number;
297
+ }
298
+ interface AsciiCell {
299
+ char: string;
300
+ r: number;
301
+ g: number;
302
+ b: number;
303
+ a: number;
304
+ /** Stored dithered luminance (0–255). Present when the frame was generated by the engine. Used for dynamic charset re-selection at render time via `charsetFrames`. */
305
+ lum?: number;
306
+ }
307
+ type AsciiFrame = AsciiCell[][];
308
+ interface AsciiResult {
309
+ frames: AsciiFrame[];
310
+ cols: number;
311
+ rows: number;
312
+ fps: number;
313
+ }
314
+ declare const DEFAULT_OPTIONS: AsciiOptions;
315
+ /**
316
+ * Hover presets — one-click configurations for the hover system.
317
+ */
318
+ declare const HOVER_PRESETS: Record<HoverPreset, {
319
+ label: string;
320
+ options: Partial<AsciiOptions>;
321
+ }>;
322
+
323
+ export { type AsciiFrame as A, type ColorMode as C, DEFAULT_OPTIONS as D, type HoverEffect as H, PALETTE_THEMES as P, type RenderMode as R, type SourceType as S, type AsciiOptions as a, type ArtStyle as b, type AnimationStyle as c, type AsciiCell as d, type AsciiResult as e, HOVER_PRESETS as f, type HoverPreset as g, type HoverShape as h, type PaletteTheme as i };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "asciify-engine",
3
- "version": "3.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Framework-agnostic ASCII art engine. Convert images, videos, and GIFs into ASCII art rendered on canvas.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -27,16 +27,6 @@
27
27
  "default": "./dist/core.cjs"
28
28
  }
29
29
  },
30
- "./backgrounds": {
31
- "import": {
32
- "types": "./dist/backgrounds.d.ts",
33
- "default": "./dist/backgrounds.js"
34
- },
35
- "require": {
36
- "types": "./dist/backgrounds.d.cts",
37
- "default": "./dist/backgrounds.cjs"
38
- }
39
- },
40
30
  "./hover": {
41
31
  "import": {
42
32
  "types": "./dist/hover.d.ts",
@@ -108,7 +98,6 @@
108
98
  "terminal-art",
109
99
  "retro",
110
100
  "matrix-effect",
111
- "matrix-rain",
112
101
  "ascii-video",
113
102
  "ascii-webcam",
114
103
  "ascii-image",
@@ -143,9 +132,6 @@
143
132
  "ascii-hero",
144
133
  "landing-page-background",
145
134
  "website-background",
146
- "parallax-stars",
147
- "aurora-effect",
148
- "particle-effect",
149
135
  "ascii-particles"
150
136
  ],
151
137
  "author": "ayangabryl",
@@ -2,13 +2,13 @@
2
2
  name: asciify-engine
3
3
  description: Build and tune browser ASCII images, video, GIFs, text, and interactive heroes with asciify-engine. Use for media conversion, character and color choices, hover integration, layered HTML typography, scroll-synced video, and rendering performance in apps using this package.
4
4
  metadata:
5
- tested-engine: "3.0.0"
5
+ tested-engine: "4.1.0"
6
6
  updated: "2026-09-09"
7
7
  ---
8
8
 
9
9
  # Asciify Engine
10
10
 
11
- Use the published `asciify-engine` npm package for browser canvas rendering. Tested against **3.0.0**. Check the application's installed version and its public types before using newer options; a local checkout can contain unpublished changes.
11
+ Use the published `asciify-engine` npm package for browser canvas rendering. Tested against **4.1.0**. Check the application's installed version and its public types before using newer options; a local checkout can contain unpublished changes.
12
12
 
13
13
  - Agent entry: https://asciify.org/skill (redirects to this Markdown document).
14
14
  - Playground: https://asciify.org/editor
@@ -19,7 +19,7 @@ Use the published `asciify-engine` npm package for browser canvas rendering. Tes
19
19
  npm install asciify-engine@latest
20
20
  ```
21
21
 
22
- Version 1.2.0 exports `asciify-engine`, `asciify-engine/core` for media without procedural generators, and `asciify-engine/backgrounds` for optional backgrounds. Root imports remain supported. GIF decoding loads `gifuct-js` when a GIF API is called. Enhanced website hovers, living-image motion and fine dither are available from the optional `asciify-engine/hover` entry; root and `/core` do not load it.
22
+ Version 4.0 exports `asciify-engine` and `asciify-engine/core` for media rendering. Procedural background templates are downloaded from asciify.org/backgrounds and are not npm exports. Root imports remain supported. GIF decoding loads `gifuct-js` when a GIF API is called. Enhanced website hovers, living-image motion and fine dither are available from the optional `asciify-engine/hover` entry; root and `/core` do not load it.
23
23
 
24
24
  ## Install the agent skill
25
25
 
@@ -34,7 +34,7 @@ This uses the Vercel Skills CLI to install the repository skill and its linked r
34
34
  - Static image: `asciify(source, canvas, config)` returns `Promise<() => void>`. Call the cleanup on unmount, including for a still image.
35
35
  - Video: `asciifyVideo(source, canvas, config)` returns `Promise<() => void>`. Call that function on unmount. Use `fitTo` and `objectFit: 'cover'` for full-bleed heroes; `contain` for a whole-subject preview.
36
36
  - GIF: `asciifyGif(source, canvas, config)` returns a cleanup function asynchronously.
37
- - Procedural background: `asciiBackground(host, config)` returns an object with **`destroy()`**, not a stop function. Media conversion does not require adding a procedural background.
37
+ - Procedural background: download a template from https://asciify.org/backgrounds and import its `asciiBackground` helper locally. Call its `destroy()` on unmount. No background generators ship in npm 4.0.
38
38
  - Dense custom rendering: use `imageToAsciiTextFrame` and `renderTextFrameToCanvas`; keep the same options when converting and rendering. Use object frames when per-cell mutation is required.
39
39
 
40
40
  For a hero with HTML layered through the ASCII, read [references/layered-hero.md](references/layered-hero.md) and use the accompanying [HTML](examples/layered-hero.html), [CSS](examples/layered-hero.css), and [JavaScript](examples/layered-hero.js). These are ordinary Vite/bundler inputs, not another npm dependency. Supply your own media.
@@ -83,7 +83,7 @@ const letters = [...new Set('ASCIIFY'.toUpperCase().replace(/[^A-Z]/g, ''))].joi
83
83
  const charset = ` .:-=+${letters}#%@`;
84
84
  ```
85
85
 
86
- Preview it: letter glyph densities differ, so arbitrary words are not automatically an evenly ordered ramp. Keep the leading space and the light/dense symbols. Use `CHARSETS.standard`, `dense`, `blocks`, or `braille` when they better suit the subject.
86
+ Preview it: letter glyph densities differ, so arbitrary words are not automatically an evenly ordered ramp. Keep the leading space and the light/dense symbols. Use the curated `CHARACTER_SETS.standard.chars`, `detailed.chars`, `blocks.chars`, or `braille.chars` from `/core` when appropriate.
87
87
 
88
88
  ## Engine effects versus studio effects
89
89
 
@@ -136,6 +136,6 @@ Version 1.3.0 adds text-shaped charcoal cutouts (`textMask`), peripheral scanlin
136
136
 
137
137
  For optional wallpaper/backdrops, alternate renderers, palettes, masks, saved looks, text fonts, or deterministic MP4/GIF export, read [references/studio-workspace.md](references/studio-workspace.md). The new `/studio` API is separate from `/hover` and `AsciiOptions`; do not replace an existing hero pipeline just to add a backdrop. Use version 1.4.0 or newer for these APIs after verifying the installed package.
138
138
 
139
- ## Standard catalogs in 2.0
139
+ ## Current catalogs (4.1)
140
140
 
141
- `CHARSETS` and `ART_STYLE_PRESETS` no longer include `emoji`, `musical` or `starfield`. Do not recommend or generate those names. For character selection in a Studio integration, use `STUDIO_CHARACTER_SETS` from `asciify-engine/studio` and assign an entry's `chars` to `settings.charset`. These ramps apply to the `ascii` style; other styles define their own marks. Custom Unicode strings remain supported. Read the upgrade reference when migrating 1.x preset names. The site now opens its primary workspace at `/editor`; `/editor/classic` preserves earlier workflows. Keep the engine imports modular: new editor defaults do not require importing Studio into an existing hero.
141
+ Legacy `CHARSETS`, `ART_STYLE_PRESETS`, `CHARSET_SEQUENCES`, `LIVING_STYLE_PRESETS` and their key types are removed. Do not import them or generate old names such as Katakana, Binary, Waves or Smoke. Use the 15 `STUDIO_STYLES` for render-style selectors and `/studio` rendering. For ASCII-only character ramps use `CHARACTER_SETS` from `/core` or `STUDIO_CHARACTER_SETS` from `/studio`; custom Unicode and user-provided frame arrays remain supported. Core `artStyle` only accepts `classic`; use explicit options instead of preset shortcuts. Despite the requested 4.1.0 version number, migrating these removed APIs is a breaking change.
@@ -105,7 +105,7 @@ Do not promise a 120 FPS minimum on every mobile device or arbitrary character c
105
105
 
106
106
  ### Hero character catalog
107
107
 
108
- Hero → Customize → Character set exposes every preset from the published engine `CHARSETS` catalog, plus the Asciify brand set. Selecting a set clears a custom-letter override; Apply letters modifies only the selected set, and Reset set restores it. Selection works while paused or with reduced motion. The hero remains gray; these are character ramps, not the playground’s Halftone/Dither rendering modes. Prism at 95% with 35% edge softness is the default hero finish: red/cyan separation with short soft shoulders at the perimeter, a clear center, and no blur on HTML text or controls. The same Prism shader is used by the playground and optional engine hover surface.
108
+ Hero → Customize → Characters uses the same 15 render-style tiles as the editor, with six primary styles and nine under More render styles. Custom letters are available under ASCII. Alternate styles use the published Studio renderer and share the hero's video; they are not aliases for old alphabets. Default ASCII retains its GPU hover and Prism finish. Selecting styles works while paused or with reduced motion.
109
109
 
110
110
  The default Trail radius is now `0.45` in the hero, playground preset and optional engine mount. This widens the nominal fluid impulse by about 51% versus `0.2`, while preserving the same strength, bounded flow field. Pass `radius: 0.2` explicitly for the previous narrower reach. Other hover presets keep their existing sizes.
111
111
 
@@ -98,7 +98,7 @@ For GSAP, pass your installed `gsap` and `ScrollTrigger` instances through `scro
98
98
  ## Procedural backgrounds
99
99
 
100
100
  ```js
101
- import { asciiBackground } from 'asciify-engine';
101
+ import { asciiBackground } from './wave.js'; // Download from asciify.org/backgrounds
102
102
  const background = asciiBackground(host, {
103
103
  type: 'aurora', fontSize: 14, speed: 0.8, density: 0.55,
104
104
  });
@@ -106,4 +106,4 @@ const background = asciiBackground(host, {
106
106
  background.destroy();
107
107
  ```
108
108
 
109
- The published background types are wave, rain, stars, pulse, noise, grid, aurora, silk, void, morph, fire, dna, terrain, and circuit. The website's fluid scene is separate application code; it is not a `type: 'fluid'` option in this package version.
109
+ In 4.0, procedural backgrounds are copyable/downloadable templates at asciify.org/backgrounds, outside npm. Import the downloaded template locally. Fluid also remains application code. No procedural background type is a package option in 4.0.
@@ -114,3 +114,14 @@ For PNG/JPEG, pass `time: player.time` to `exportStudio` to save the selected fr
114
114
  ## Still-image motion (3.0)
115
115
 
116
116
  Import `STUDIO_MOTIONS` from `/studio` to populate controls; each item has `value`, `label`, and `description`. Use `motion: { type: 'caustics' | 'current' | 'reform' | 'none', speed: 1 }`. Off is the default. Caustics preserves the grid, Current introduces fluid displacement, and Reform uses a staggered dissolve with a readable hold. Loops last 12 / speed seconds and preserve source/accent/gray color choice. Speed is 0.1–3. Do not combine source video with ambient motion unless requested. Hover remains independently configurable. Retired saved motion names normalize to Off. See [upgrading.md](upgrading.md) before moving an existing integration to 3.0.
117
+
118
+ ## Sharp previews (3.0.1)
119
+
120
+ Studio defaults to a 6-unit cell size. Separate the sampling grid from canvas backing resolution: `instance.resize(width, height, pixelRatio)` updates the backing size and raster scale together without remounting media. Doubling dimensions and passing 2 keeps the same character count while drawing sharper glyphs. Use a bounded backing dimension and at most the device DPR; avoid stretching a small bitmap with CSS. `instance.pixelRatio` reports the scale. When exporting, pass `referenceWidth: canvas.width / instance.pixelRatio` to preserve logical density. Explicit saved cell sizes remain valid. Do not force a tile style's coarse cell size onto ASCII when changing render styles.
121
+
122
+
123
+ ## Fine grids and sharp previews (4.0)
124
+
125
+ Start with `cellSize: 6`; the editor remembers each style's size. A Retina preview uses `resize(width, height, pixelRatio)` on a `mountStudioMedia` handle to keep logical cells independent of physical raster resolution. Its `setBudget(maxCells)` changes the upper sampling budget; automatic adaptation may reduce it when frames are slow. At a 960×540 logical canvas, 32,768 cells permit Lego size 4, and 65,536 permit dither scale 3. Keep ASCII at a measured budget such as 12,000 cells. Smaller requested cells cannot bypass the active budget; show `studioGrid`'s actual minimum in controls. Fine grids cost more, so profile hover and motion on target devices. Studio caches Lego stud artwork.
126
+
127
+ For menus shared with a media hero, import the eight `CHARACTER_SETS` from `asciify-engine/core`; `STUDIO_CHARACTER_SETS` is the identical catalog through `/studio`. A hero need not load the Studio runtime.
@@ -30,7 +30,7 @@ After updating, read the installed SKILL.md and its linked references, confirm i
30
30
 
31
31
  ## 2.0 catalog migration
32
32
 
33
- 2.0.0 removes the `emoji`, `musical`, and `starfield` keys from `CHARSETS`, `ArtStyle`, and `ART_STYLE_PRESETS`. Before upgrading, search application settings and saved presets for those names. Use `classic` plus a custom `charset` to retain an application-owned alphabet, or use `circles`, `braille`, or `geometric` for structural alternatives. The existing cosmic sequence now uses sparse marks/circles/shadows, and angular uses geometry/shards/blocks. Other core options and renderers retain their behavior. Do not pass retired keys or tell users that custom Unicode has been removed.
33
+ 2.0.0 removes the `emoji`, `musical`, and `starfield` keys from `CHARSETS`, `ArtStyle`, and `ART_STYLE_PRESETS`. Before upgrading, search application settings and saved presets for those names. For current releases, use a custom `charset` or a current StudioStyle. The existing cosmic sequence now uses sparse marks/circles/shadows, and angular uses geometry/shards/blocks. Other core options and renderers retain their behavior. Do not pass retired keys or tell users that custom Unicode has been removed.
34
34
 
35
35
  The curated `STUDIO_CHARACTER_SETS` export is available from `/studio` in 2.0.0. Its values contain `label` and `chars`; assign `chars` to Studio settings only for `style: 'ascii'`. The primary editor at `/editor` uses Studio; earlier webcam and classic-editor workflows remain at `/editor/classic`.
36
36
 
@@ -43,3 +43,14 @@ The curated `STUDIO_CHARACTER_SETS` export is available from `/studio` in 2.0.0.
43
43
  These are actual new treatments, not aliases for Fire, Breathe, Rainbow, Hologram, or the old row-wave effects. Caustics moves light through a fixed grid; Current carries the image with bounded local flow; Reform disperses and reassembles cells with spatially staggered timing. Core Canvas uses the same field with simpler opacity modulation; `/hover` and `/studio` also modulate character density. Hover and optical glitch/blur finishes are separate and remain available. Optional procedural backgrounds are unchanged.
44
44
 
45
45
  Use one motion owner: `animationStyle` for core, `motion` for `/hover`, or `motion.type` for `/studio`. Keep Off as the default for video. Each motion loops every `12 / speed` seconds. Offer pause, respect reduced motion, and avoid claiming a guaranteed FPS on every device.
46
+
47
+ ## 4.0 background migration
48
+
49
+ Remove imports from `asciify-engine/backgrounds` and generator imports from the root package. Download the chosen `.js` template from https://asciify.org/backgrounds and change the import to `./wave.js` (or your selected file). The template exports `asciiBackground`; keep calling `destroy()` on unmount. It handles resizing, hidden/offscreen suspension, reduced motion, and cleanup. A selected template contains only that generator; backgrounds.js is the optional full collection. Application-owned source templates are separate from npm updates. Existing installations can stay on 3.x while migrating. Text and media rendering remain supported, including text backgrounds.
50
+
51
+ Use `CHARACTER_SETS` from root or `/core` for the same eight choices as the editor's `STUDIO_CHARACTER_SETS`. This data-only import does not load Studio. Keep custom characters available separately; do not import the removed `CHARSETS` object.
52
+
53
+
54
+ ## 4.1.0 — remove legacy catalogs
55
+
56
+ This requested minor version contains breaking API removals: `CHARSETS`, `ART_STYLE_PRESETS`, `CHARSET_SEQUENCES`, `LIVING_STYLE_PRESETS`, `CharsetKey`, `CharsetSequenceKey`, and `LivingStylePresetKey`. Search source, saved settings, generated snippets, and documentation before upgrading. Use `/studio`'s 15 `STUDIO_STYLES` for render styles; use `/core`'s curated `CHARACTER_SETS` for ASCII alphabets. Replace old shorthand `artStyle` names with actual Studio rendering or explicit core charset/renderMode/colorMode options. Core only accepts `artStyle: 'classic'`. Custom `charset` and `charsetFrames` remain supported. Fine-dither hover integration uses explicit `fineDither: true`, not the removed terminal preset.
@@ -1 +0,0 @@
1
- "use strict";function t(t){const e=t.match(/^#([0-9a-f]{3,8})$/i)?.[1];if(e){const t=e.length<=4?e.split("").map(t=>parseInt(t+t,16)):[parseInt(e.slice(0,2),16),parseInt(e.slice(2,4),16),parseInt(e.slice(4,6),16)];return{r:t[0],g:t[1],b:t[2]}}const o=t.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i);return o?{r:+o[1],g:+o[2],b:+o[3]}:null}function e(t){return t*t*t*(t*(6*t-15)+10)}function o(t,e,o){return t+(e-t)*o}function n(t,e){let o=127*t+311*e;return o^=o>>13,(o*(o*o*15731+789221)+1376312589&2147483647)/2147483647}function r(t,r){const l=Math.floor(t),a=Math.floor(r),i=r-a,c=e(t-l),s=e(i),h=n(l,a),f=n(l+1,a),g=n(l,a+1),M=n(l+1,a+1);return o(o(h,f,c),o(g,M,c),s)}function l(t,e){return(.5*r(t,e)+.25*r(2.1*t,2.1*e)+.125*r(4.3*t,4.3*e))/.875}function a(t,e,o,r,a={x:.5,y:.5},i={}){const{fontSize:c=13,charAspect:s=.62,lineHeightRatio:h=1.4,chars:f=" .:-=+*#%@",baseColor:g=null,accentColor:M,accentThreshold:d=.52,mouseInfluence:u=.55,mouseFalloff:p=2.8,speed:$=1,vortex:b=!0,sparkles:m=!0,breathe:x=!0,lightMode:y=!1}=i,S=M??(y?"#6b8700":"#d4ff00"),B=c*s,w=c*h,C=Math.ceil(e/B),v=Math.ceil(o/w),A=a.x,I=a.y;let k=212,T=255,R=0;{const t=S.replace("#","");6===t.length&&(k=parseInt(t.slice(0,2),16),T=parseInt(t.slice(2,4),16),R=parseInt(t.slice(4,6),16))}t.clearRect(0,0,e,o),t.font=`${c}px "JetBrains Mono", monospace`,t.textBaseline="top";const P=r*$,z=x?.12*(.5*Math.sin(.22*P)+.5):0;for(let e=0;e<v;e++)for(let o=0;o<C;o++){const r=o/C,a=e/v,i=.045,c=.08*P,s=.06,h=.45*((.5*Math.sin(.08*o+.05*e+.6*P)+.5+(.5*Math.sin(.03*o-.07*e+.4*P)+.5)+(.5*Math.sin(.05*o+.03*e+.8*P)+.5))/3)+.35*(.5*l(o*i+c,e*i*1.4-.7*c)+.5)+.2*(.5*Math.sin((o+.65*e)*s+1.1*P)+.5)+z,M=r-A,$=a-I,x=Math.sqrt(M*M+$*$);let S=0;if(b&&x<.35){const t=Math.atan2($,M),e=Math.sin(4*t+2.2*P-14*x),o=Math.max(0,1-x/.35);S=e*o*o*.22}const q=h*(1-u)+(Math.max(0,1-x*p)+.5*S)*u+.15*S,E=Math.min(1,Math.max(0,q));let F=E;if(m&&E>.72){const t=Math.floor(8*P),r=n(7*o+3*t,11*e+t);r>.88&&(F=Math.min(1,E+4*(r-.88)))}const L=Math.floor(F*(f.length-1));if(" "===f[L])continue;const _=.015+.07*F;if(F>d){const e=Math.min(y?.9:.28,_*(y?14:2.8));t.fillStyle=`rgba(${k},${T},${R},${e})`}else t.fillStyle=g?g.replace("{a}",String(_)):y?`rgba(55,55,55,${7*_})`:`rgba(255,255,255,${_})`;t.fillText(f[L],o*B,e*w)}}function i(e,o,r,l,a={}){const{fontSize:i=13,chars:c="0123456789ABCDEF@#$&*+=/<>",accentColor:s,color:h,speed:f=1,density:g=.55,tailLength:M=14,lightMode:d=!1}=a,u=s??(d?"#6b8700":"#d4ff00"),p=.62*i,$=1.4*i,b=Math.ceil(o/p),m=Math.ceil(r/$);e.clearRect(0,0,o,r),e.font=`${i}px monospace`,e.textBaseline="top";let x=255,y=255,S=255;if(d&&(x=55,y=55,S=55),h){const e=t(h);e&&(x=e.r,y=e.g,S=e.b)}let B=212,w=255,C=0;const v=t(u);v&&(B=v.r,w=v.g,C=v.b);const A=m+M;for(let t=0;t<b;t++){if(n(17*t,3)>g)continue;const o=(.5+1.5*n(31*t,7))*f,r=n(13*t,11)*A,a=Math.floor((l*o*7+r)%A),i=t*p;for(let o=0;o<=M;o++){const r=a-(M-o);if(r<0||r>=m)continue;const s=r*$,h=n(53*t+Math.floor(5*l+o),7*r),f=c[Math.floor(h*c.length)],g=o/M;if(o===M)e.fillStyle=`rgba(${B},${w},${C},${d?.7:.85})`;else{const t=d?.85*g:.15*g;e.fillStyle=`rgba(${x},${y},${S},${t})`}e.fillText(f,i,s)}}}function c(e,o,r,l,a={x:.5,y:.5},i={}){const{fontSize:c=14,chars:s=" . · * + ° ★",accentColor:h,color:f,speed:g=1,count:M=180,lightMode:d=!1}=i,u=h??(d?"#6b8700":"#d4ff00");e.clearRect(0,0,o,r),e.textBaseline="middle",e.textAlign="center";const p=o*(.2+.6*a.x),$=r*(.2+.6*a.y),b=.65*Math.sqrt(o*o+r*r);let m=255,x=255,y=255;if(d&&(m=55,x=55,y=55),f){const e=t(f);e&&(m=e.r,x=e.g,y=e.b)}let S=212,B=255,w=0;const C=t(u);C&&(S=C.r,B=C.g,w=C.b);const v=s.replace(/ /g,"").split("");if(0!==v.length){for(let t=0;t<M;t++){const a=n(17*t,3)*Math.PI*2,i=(l*(.15+.85*n(31*t,7))*g*.22+n(13*t,11))%1,s=p+Math.cos(a)*i*b,h=$+Math.sin(a)*i*b;if(s<-20||s>o+20||h<-20||h>r+20)continue;const f=Math.max(6,c*(.4+.9*i));e.font=`${f}px monospace`;const M=v[Math.min(v.length-1,Math.floor(i*v.length))],u=i>.72,C=d?.85*i:.2*i;e.fillStyle=u?`rgba(${S},${B},${w},${Math.min(d?.92:.32,2.2*C)})`:`rgba(${m},${x},${y},${C})`,e.fillText(M,s,h)}e.textAlign="left"}}function s(e,o,n,r,l={x:.5,y:.5},a={}){const{fontSize:i=14,chars:c=". · ○ ◎ ●",accentColor:s,color:h,rings:f=5,speed:g=1,sharpness:M=4,lightMode:d=!1}=a,u=s??(d?"#007a5e":"#00ffcc");e.clearRect(0,0,o,n),e.textBaseline="middle",e.textAlign="center";const p=o*l.x,$=n*l.y,b=1.6*Math.sqrt(p*p+$*$)+.2*Math.sqrt(o*o+n*n);let m=255,x=255,y=255;if(d&&(m=55,x=55,y=55),h){const e=t(h);e&&(m=e.r,x=e.g,y=e.b)}let S=0,B=255,w=204;const C=t(u);C&&(S=C.r,B=C.g,w=C.b);const v=c.replace(/ /g,"").split("");if(0===v.length)return;const A=Math.ceil(o/i),I=Math.ceil(n/i);for(let t=0;t<I;t++)for(let o=0;o<A;o++){const n=o*i+.5*i,l=t*i+.5*i,a=n-p,c=l-$,s=Math.sqrt(a*a+c*c)/b;let h=0;for(let t=0;t<f;t++){const e=(r*g*.38+t/f)%1,o=Math.abs(s-e),n=Math.max(0,1-o*b/(i*(12-M)));h+=Math.cos(n*Math.PI*.5)*n}if(h=Math.min(1,h),h<.02)continue;const u=h>.6;e.font=`${i}px monospace`;const C=Math.floor(h*(v.length-1)),A=v[Math.min(C,v.length-1)],I=d?.88*h:.22*h;e.fillStyle=u?`rgba(${S},${B},${w},${Math.min(d?.95:.4,.55*h)})`:`rgba(${m},${x},${y},${I})`,e.fillText(A,n,l)}e.textAlign="left"}function h(e,o,n,l,a={x:.5,y:.5},i={}){const{fontSize:c=14,chars:s=" .·:;=+*#%@░▒▓",accentColor:h,color:f,octaves:g=4,speed:M=1,scale:d=1,accentThreshold:u=.78,mouseWarp:p=.3,lightMode:$=!1}=i,b=h??($?"#6b8700":"#d4ff00"),m=.62*c,x=1.4*c,y=Math.ceil(o/m),S=Math.ceil(n/x);e.clearRect(0,0,o,n),e.font=`${c}px monospace`,e.textBaseline="top";let B=255,w=255,C=255;if($&&(B=55,w=55,C=55),f){const e=t(f);e&&(B=e.r,w=e.g,C=e.b)}let v=212,A=255,I=0;const k=t(b);k&&(v=k.r,A=k.g,I=k.b);const T=.035*d,R=l*M,P=Math.min(6,Math.max(1,g)),z=(t,e)=>{let o=0,n=.5,l=1,a=0;for(let i=0;i<P;i++)o+=r(t*l,e*l)*n,a+=n,n*=.5,l*=2.1;return o/a};for(let t=0;t<S;t++)for(let o=0;o<y;o++){const n=o*T+.06*R,r=t*T*1.3-.04*R,l=o/y-a.x,i=t/S-a.y,c=Math.sqrt(l*l+i*i),h=p>0?.12*Math.max(0,1-c/p):0,f=.5*z(n+h*Math.sin(1.3*R+8*i),r+h*Math.cos(.9*R+8*l))+.5;if(f<.12)continue;const g=s[Math.floor(f*(s.length-1))];if(" "===g)continue;const M=f>u,d=$?.82*f:.13*f;e.fillStyle=M?`rgba(${v},${A},${I},${$?.92:.28})`:`rgba(${B},${w},${C},${d})`,e.fillText(g,o*m,t*x)}}function f(e,o,r,l,a={x:.5,y:.5},i={}){const{fontSize:c=12,chars:s="·-=+|/",accentColor:h,color:f,bands:g=3,speed:M=1,bandWidth:d=.12,glitch:u=!0,lightMode:p=!1}=i,$=h??(p?"#6b8700":"#d4ff00"),b=.62*c,m=1.4*c,x=Math.ceil(o/b),y=Math.ceil(r/m);e.clearRect(0,0,o,r),e.font=`${c}px monospace`,e.textBaseline="top";let S=255,B=255,w=255;if(p&&(S=55,B=55,w=55),f){const e=t(f);e&&(S=e.r,B=e.g,w=e.b)}let C=212,v=255,A=0;const I=t($);I&&(C=I.r,v=I.g,A=I.b);const k=l*M;for(let t=0;t<y;t++)for(let o=0;o<x;o++){const r=t/y,l=((r*g-.5*k)%1+1)%1,i=Math.max(0,1-l/d),c=.35*(.5*n(3*o,7*t)+.5);let h=0;if(u){const e=o/x-a.x,l=r-a.y,i=Math.sqrt(e*e+l*l);if(i<.18){const e=n(11*o+Math.floor(12*k),5*t);h=Math.max(0,1-i/.18)*(e>.5?e-.3:0)}}const f=Math.min(1,c+.8*i+.6*h);if(f<.04)continue;const M=s[Math.floor(f*(s.length-1))],$=i>.55,I=p?.82*f:.12*f;e.fillStyle=$?`rgba(${C},${v},${A},${p?.92:.28})`:`rgba(${S},${B},${w},${I})`,e.fillText(M,o*b,t*m)}}function g(e,o,r,l,a={x:.5,y:.5},i={}){const{fontSize:c=14,chars:s=" ·∙•:;+=≡≣#@",color:h,accentColor:f,speed:g=1,layers:M=5,softness:d=1.2,mouseRipple:u=.2,lightMode:p=!1}=i,$=f??(p?"#6b8700":"#d4ff00"),b=.62*c,m=1.4*c,x=Math.ceil(o/b),y=Math.ceil(r/m);e.clearRect(0,0,o,r),e.font=`${c}px monospace`,e.textBaseline="top";let S=255,B=255,w=255;if(p&&(S=55,B=55,w=55),h){const e=t(h);e&&(S=e.r,B=e.g,w=e.b)}let C=212,v=255,A=0;const I=t($);I&&(C=I.r,v=I.g,A=I.b);const k=l*g,T=[];for(let t=0;t<M;t++){const e=n(17*t,31*t+7),o=n(23*t+5,11*t);T.push({fx:.8+2.2*e,fy:1.2+1.8*o,phase:e*Math.PI*4,dt:(.3+.5*n(7*t,13*t+3))*(t%2==0?1:-1),amp:.55+.45*n(29*t,3*t)})}for(let t=0;t<y;t++){const o=t/y;for(let n=0;n<x;n++){const r=n/x,l=r-a.x,i=o-a.y,c=Math.sqrt(l*l+i*i),h=u*Math.exp(-c*c/.06),f=r+l*h,g=o+i*h;let $=0,y=0;for(let t=0;t<M;t++){const{fx:e,fy:o,phase:n,dt:r,amp:l}=T[t];$+=Math.sin(f*e*Math.PI*2+k*r+n)*Math.cos(g*o*Math.PI*2+k*r*.7+1.3*n)*l,y+=l}const I=$/y,R=.5+.5*Math.tanh(I*d*2.2);if(R<.12)continue;const P=(R-.12)/.88,z=s[Math.min(s.length-1,Math.floor(P*s.length))],q=R>.82,E=p?.82*R:.14*R;e.fillStyle=q?`rgba(${C},${v},${A},${p?.92:.32})`:`rgba(${S},${B},${w},${E})`,e.fillText(z,n*b,t*m)}}}function M(e,o,r,l,a={}){const{fontSize:i=13,color:c,accentColor:s,speed:h=.4,layers:f=4,turbulence:g=.8,lightMode:M=!1}=a,d=s??(M?"#6b8700":"#d4ff00"),u=.62*i,p=1.4*i,$=Math.ceil(o/u),b=Math.ceil(r/p);e.clearRect(0,0,o,r),e.font=`${i}px monospace`,e.textBaseline="top";let m=255,x=255,y=255;if(M&&(m=55,x=55,y=55),c){const e=t(c);e&&(m=e.r,x=e.g,y=e.b)}let S=212,B=255,w=0;const C=t(d);C&&(S=C.r,B=C.g,w=C.b);const v=l*h,A=["─","─","╌","·","╌","─","─","╌","·"];for(let t=0;t<b;t++){const o=t/b;for(let r=0;r<$;r++){const l=r/$;let a=0,i=0;for(let t=0;t<f;t++){const e=n(13*t,7*t+3),r=1.1+2.4*e,c=.9+2*n(29*t,11*t+1),s=e*Math.PI*6,h=(.2+.5*n(41*t,17*t))*(t%2==0?1:-1.3),f=Math.sin(l*r*Math.PI*2+v*h+s),M=Math.cos(o*c*Math.PI*2+v*h*.6+1.7*s),d=Math.sin(l*c*Math.PI*g+o*r*Math.PI*g+v*h*.4);a+=Math.atan2(M+.3*d,f),i+=.5*(f*M+1)}const c=a/f,s=Math.min(1,i/f);if(s<.1)continue;const h=(c+Math.PI)/(2*Math.PI),d=A[Math.floor(h*A.length)%A.length],b=s>.8,C=M?.8*s:.13*s;e.fillStyle=b?`rgba(${S},${B},${w},${M?.9:.26})`:`rgba(${m},${x},${y},${C})`,e.fillText(d,r*u,t*p)}}}function d(e,o,r,l,a={x:.5,y:.5},i={}){const{fontSize:c=13,chars:s=" ·:;=+*#%@",color:h,accentColor:f,speed:g=1,radius:M=.38,swirl:d=3,lightMode:u=!1}=i,p=f??(u?"#6b8700":"#d4ff00"),$=.62*c,b=1.4*c,m=Math.ceil(o/$),x=Math.ceil(r/b),y=o/r;e.clearRect(0,0,o,r),e.font=`${c}px monospace`,e.textBaseline="top";let S=255,B=255,w=255;if(u&&(S=55,B=55,w=55),h){const e=t(h);e&&(S=e.r,B=e.g,w=e.b)}let C=212,v=255,A=0;const I=t(p);I&&(C=I.r,v=I.g,A=I.b);const k=l*g;for(let t=0;t<x;t++){const o=t/x;for(let r=0;r<m;r++){const l=(r/m-a.x)*y,i=o-a.y,c=Math.sqrt(l*l+i*i)/M;if(c>1){const o=n(3*r,7*t)*Math.max(0,1-3*(c-1));if(o<.62)continue;const l=o*(u?.28:.04);e.fillStyle=`rgba(${S},${B},${w},${l})`,e.fillText(s[1],r*$,t*b);continue}const h=Math.max(0,1-Math.abs(c-(.15+.12*Math.sin(1.1*k)))/.07),f=Math.pow(1-c,2.2),g=Math.min(1,f+.6*h);if(g<.06)continue;const d=Math.floor(g*(s.length-1)),p=s[Math.min(s.length-1,d)],x=h>.35||c<.08,I=u?.85*g:.18*g;e.fillStyle=x?`rgba(${C},${v},${A},${u?.95:.38})`:`rgba(${S},${B},${w},${I})`,e.fillText(p,r*$,t*b)}}}function u(e,o,r,l,a={}){const{fontSize:i=14,chars:c=" ·∙•:-=+*#",color:s,accentColor:h,speed:f=.5,harmonics:g=3,lightMode:M=!1}=a,d=h??(M?"#6b8700":"#d4ff00"),u=.62*i,p=1.4*i,$=Math.ceil(o/u),b=Math.ceil(r/p);e.clearRect(0,0,o,r),e.font=`${i}px monospace`,e.textBaseline="top";let m=255,x=255,y=255;if(M&&(m=55,x=55,y=55),s){const e=t(s);e&&(m=e.r,x=e.g,y=e.b)}let S=212,B=255,w=0;const C=t(d);C&&(S=C.r,B=C.g,w=C.b);const v=l*f,A=Array.from({length:g},(t,e)=>1/(e+1)).reduce((t,e)=>t+e,0);for(let t=0;t<b;t++)for(let o=0;o<$;o++){let r=0;for(let e=0;e<g;e++){const l=.18+1.4*n(o*(e+3)+7,t*(e+5)+11),a=n(o*(e+7),t*(e+9)+3)*Math.PI*2,i=1/(e+1);r+=Math.sin(v*l+a)*i}const l=.5*(r/A+1);if(l<.28)continue;const a=(l-.28)/.72,i=c[Math.min(c.length-1,Math.floor(a*c.length))],s=l>.88,h=M?.82*a:.13*a;e.fillStyle=s?`rgba(${S},${B},${w},${M?.92:.28})`:`rgba(${m},${x},${y},${h})`,e.fillText(i,o*u,t*p)}}function p(e,o,n,r,l={}){const{fontSize:a=13,chars:i=" .,:;i+xX#&@",color:c="#ff4500",hotColor:s="#ffe066",intensity:h=.85,wind:f=0,speed:g=1,lightMode:M=!1}=l,d=.62*a,u=1.4*a,p=Math.ceil(o/d),$=Math.ceil(n/u),b=p*$,m="__fire_heat__",x=e.canvas;let y=x[m];y&&y.length===b||(y=new Float32Array(b),x[m]=y);const S=.18*(.016*g),B=f*g*.8,w=$-1,C=r*g;for(let t=0;t<p;t++){const e=(.6*(.5*Math.sin(.31*t+4.1*C)+.5)+.4*(.5*Math.sin(.73*t-2.7*C)+.5))*h;y[w*p+t]=Math.min(1,e+.15*Math.random()*h),w>0&&(y[(w-1)*p+t]=Math.min(1,.85*e+.1*Math.random()*h))}const v=new Float32Array(b);for(let t=0;t<$-2;t++)for(let e=0;e<p;e++){const o=.4*y[(t+1)*p+e]+.25*y[(t+2)*p+Math.max(0,Math.min(p-1,e+Math.round(B)))]+.175*y[(t+1)*p+Math.max(0,e-1)]+.175*y[(t+1)*p+Math.min(p-1,e+1)];v[t*p+e]=Math.max(0,o-S-.02*Math.random()*g)}for(let t=0;t<p;t++)v[($-1)*p+t]=y[($-1)*p+t],$>1&&(v[($-2)*p+t]=y[($-2)*p+t]);x[m]=v;const A=t(c)??{r:255,g:69,b:0},I=t(s)??{r:255,g:224,b:102};e.clearRect(0,0,o,n),e.font=`${a}px "JetBrains Mono", monospace`,e.textBaseline="top";for(let t=0;t<$;t++)for(let o=0;o<p;o++){const n=v[t*p+o];if(n<.03)continue;const r=i[Math.min(i.length-1,Math.floor(n*i.length))];if(" "===r)continue;const l=Math.min(1,1.2*n),a=A.r+(I.r-A.r)*l|0,c=A.g+(I.g-A.g)*l|0,s=A.b+(I.b-A.b)*l|0,h=M?1-.3*n:Math.min(1,n+.15);e.globalAlpha=h,e.fillStyle=`rgb(${a},${c},${s})`,e.fillText(r,o*d,t*u)}e.globalAlpha=1}function $(e,o,r,l,a={}){const{fontSize:i=13,baseChars:c="ATCG",bridgeChars:s="-=≡",color:h="#00e5ff",color2:f="#ff4081",bridgeColor:g="#88ffcc",speed:M=1,helixCount:d,lightMode:u=!1}=a,p=.62*i,$=1.4*i,b=Math.ceil(o/p),m=Math.ceil(r/$),x=d??Math.max(1,Math.floor(o/80)),y=b/x,S=t(h)??{r:0,g:229,b:255},B=t(f)??{r:255,g:64,b:129},w=t(g)??{r:136,g:255,b:204};e.clearRect(0,0,o,r),e.font=`${i}px "JetBrains Mono", monospace`,e.textBaseline="top";const C=l*M,v=.35*y;for(let t=0;t<x;t++){const o=y*(t+.5);for(let r=0;r<m;r++){const l=r/m*Math.PI*6-1.8*C,a=o+Math.sin(l)*v,i=o+Math.sin(l+Math.PI)*v,h=Math.round(a),f=Math.round(i);if(h<0||h>=b)continue;const g=n(31*t+7*r,3),M=c[Math.floor(g*c.length)],d=.5*(Math.sin(l)+1),u=.5*(Math.sin(l+Math.PI)+1);if(e.globalAlpha=.35+.65*d,e.fillStyle=`rgb(${S.r},${S.g},${S.b})`,e.fillText(M,h*p,r*$),f>=0&&f<b){const o=n(53*t+11*r,7),l=c[Math.floor(o*c.length)];e.globalAlpha=.35+.65*u,e.fillStyle=`rgb(${B.r},${B.g},${B.b})`,e.fillText(l,f*p,r*$)}if(r%3===0){const o=Math.min(h,f),l=Math.max(h,f);if(l-o>1){const a=n(17*r+43*t,5),i=s[Math.floor(a*s.length)],c=.25*(d+u)+.2;e.globalAlpha=c,e.fillStyle=`rgb(${w.r},${w.g},${w.b})`;for(let t=o+1;t<l;t++)e.fillText(i,t*p,r*$)}}}}e.globalAlpha=1}function b(e,o,r,a,i={}){const{fontSize:c=13,chars:s=" .,:;+*#@",color:h="#4caf50",skyColor:f="#1a237e",peakColor:g="#e0e0e0",speed:M=1,roughness:d=.55,heightScale:u=.55,stars:p=!0,lightMode:$=!1}=i,b=.62*c,m=1.4*c,x=Math.ceil(o/b),y=Math.ceil(r/m),S=t(h)??{r:76,g:175,b:80},B=t(f)??{r:26,g:35,b:126},w=t(g)??{r:224,g:224,b:224};e.clearRect(0,0,o,r),e.font=`${c}px "JetBrains Mono", monospace`,e.textBaseline="top";const C=a*M*.4,v=new Array(x);for(let t=0;t<x;t++){const e=(.5*l((t/x+C)*d*3,.5)+.5)*u;v[t]=Math.floor(e*y)}for(let t=0;t<y;t++)for(let o=0;o<x;o++){const r=y-1-v[o],l=t===r;if(!(t>=r)){if(p){const r=n(7*o+Math.floor(.3*C),13*t);if(r>.97){const n=.3*Math.sin(2*a+100*r)+.7;e.globalAlpha=.5*n,e.fillStyle=`rgb(${B.r+60},${B.g+60},${B.b+80})`,e.fillText("·",o*b,t*m)}}continue}const i=(t-r)/Math.max(1,v[o]),c=s[Math.min(s.length-1,Math.floor(i*s.length))];if(" "===c&&!l)continue;const h=l?1:Math.max(0,1-4*i),f=S.r+(w.r-S.r)*h|0,g=S.g+(w.g-S.g)*h|0,M=S.b+(w.b-S.b)*h|0;e.globalAlpha=.5+.5*i,e.fillStyle=`rgb(${f},${g},${M})`,e.fillText(l?s[s.length-1]:c,o*b,t*m)}e.globalAlpha=1}var m=1,x=2,y=4,S=8,B={3:"─",12:"│",9:"┌",10:"┐",5:"└",6:"┘",11:"┬",7:"┴",13:"├",14:"┤",15:"┼",[m]:"╶",[x]:"╴",[y]:"╵",[S]:"╷"};function w(e,o,r,l,a={}){const{fontSize:i=13,pulseColor:c="#ffffff",color:s="#00ff88",density:h=.38,speed:f=1,lightMode:g=!1}=a,M=.62*i,d=1.4*i,u=Math.ceil(o/M),p=Math.ceil(r/d),$=t(s)??{r:0,g:255,b:136},b=t(c)??{r:255,g:255,b:255},m=(t,e)=>{if(n(17*t+1,7*e+2)>h)return 0;let o=0;return t+1<u&&n(17*t+1,7*e+2)>.15&&(o|=1),t-1>=0&&n(17*(t-1)+1,7*e+2)>.15&&(o|=2),e+1<p&&n(17*t+1,7*(e+1)+2)>.15&&(o|=8),e-1>=0&&n(17*t+1,7*(e-1)+2)>.15&&(o|=4),o};e.clearRect(0,0,o,r),e.font=`${i}px "JetBrains Mono", monospace`,e.textBaseline="top";const x=l*f;for(let t=0;t<p;t++)for(let o=0;o<u;o++){const r=m(o,t);if(0===r)continue;const l=B[r]??"·",a=(8*x+40*n(o,23*t))%u,i=(6*x+40*n(17*t,11*o))%p,c=(1&r||2&r)&&Math.abs(o-a)<1.5,s=(4&r||8&r)&&Math.abs(t-i)<1.5,h=c||s,f=.25+.35*n(3*o,5*t);h?(e.globalAlpha=.95,e.fillStyle=`rgb(${b.r},${b.g},${b.b})`):(e.globalAlpha=f,e.fillStyle=`rgb(${$.r},${$.g},${$.b})`),e.fillText(l,o*M,t*d)}e.globalAlpha=1}function C(t,e={}){const{type:o="wave",opacity:n=.2,className:r,zIndex:l=0,colorScheme:m="auto",color:x,...y}=e,S="string"==typeof t?document.querySelector(t):t;if(!S)return console.warn("[asciify] asciiBackground: target not found",t),{destroy:()=>{}};const B=S.style.position;"static"===getComputedStyle(S).position&&(S.style.position="relative");const C=document.createElement("canvas");C.style.cssText=["position:absolute","inset:0","width:100%","height:100%",`opacity:${n}`,"pointer-events:none",`z-index:${l}`].join(";"),r&&(C.className=r),S.prepend(C);const v=C.getContext("2d"),A=window.devicePixelRatio||1,I={x:.5,y:.5},k={x:.5,y:.5},T=()=>"light"===m||"dark"!==m&&(()=>{let t=S;for(;t&&t!==document.documentElement.parentElement;){const e=getComputedStyle(t).backgroundColor;if(e&&"transparent"!==e&&"rgba(0, 0, 0, 0)"!==e){const t=e.match(/rgba?\(\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*(\d+)/);if(t)return(.299*+t[1]+.587*+t[2]+.114*+t[3])/255>=.4}t=t.parentElement}const e=document.documentElement,o=(e.getAttribute("data-theme")||"").toLowerCase();return"dark"!==o&&("light"===o||!e.classList.contains("dark")&&window.matchMedia("(prefers-color-scheme: light)").matches)})(),R=x?function(t){const e=t.match(/^#([0-9a-f]{3,8})$/i)?.[1];if(e){const t=e.length<=4?e.split("").map(t=>parseInt(t+t,16)):[parseInt(e.slice(0,2),16),parseInt(e.slice(2,4),16),parseInt(e.slice(4,6),16)];return{r:t[0],g:t[1],b:t[2]}}const o=t.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i);return o?{r:+o[1],g:+o[2],b:+o[3]}:null}(x):null,P=()=>({...y,lightMode:void 0!==y.lightMode?y.lightMode:T(),baseColor:R?`rgba(${R.r},${R.g},${R.b},{a})`:y.baseColor}),z={current:P()},q=()=>{z.current="rain"===o||"stars"===o||"pulse"===o||"noise"===o||"grid"===o||"aurora"===o||"silk"===o||"void"===o||"morph"===o?{...y,lightMode:void 0!==y.lightMode?y.lightMode:T(),color:x??y.color}:"fire"===o||"dna"===o?{...y,color:x??y.color}:"terrain"===o||"circuit"===o?{...y,color:x??y.color,lightMode:void 0!==y.lightMode?y.lightMode:T()}:P()};q();const E=window.matchMedia("(prefers-color-scheme: light)"),F=()=>{q()};let L=null;"auto"===m&&(E.addEventListener("change",F),L=new MutationObserver(F),L.observe(document.documentElement,{attributes:!0,attributeFilter:["data-theme","class"]}));const _=()=>{const t=S.getBoundingClientRect();C.width=t.width*A,C.height=t.height*A,v.setTransform(A,0,0,A,0,0)};_();const J=t=>{const e=S.getBoundingClientRect();I.x=(t.clientX-e.left)/e.width,I.y=(t.clientY-e.top)/e.height},N=new ResizeObserver(_);N.observe(S),window.addEventListener("mousemove",J);let W=0,D=0;const G=()=>{k.x+=.07*(I.x-k.x),k.y+=.07*(I.y-k.y);const t=S.getBoundingClientRect();"rain"===o?i(v,t.width,t.height,W,z.current):"stars"===o?c(v,t.width,t.height,W,k,z.current):"pulse"===o?s(v,t.width,t.height,W,k,z.current):"noise"===o?h(v,t.width,t.height,W,k,z.current):"grid"===o?f(v,t.width,t.height,W,k,z.current):"aurora"===o?g(v,t.width,t.height,W,k,z.current):"silk"===o?M(v,t.width,t.height,W,z.current):"void"===o?d(v,t.width,t.height,W,k,z.current):"morph"===o?u(v,t.width,t.height,W,z.current):"fire"===o?p(v,t.width,t.height,W,z.current):"dna"===o?$(v,t.width,t.height,W,z.current):"terrain"===o?b(v,t.width,t.height,W,z.current):"circuit"===o?w(v,t.width,t.height,W,z.current):a(v,t.width,t.height,W,k,z.current),W+=.016,D=requestAnimationFrame(G)};return D=requestAnimationFrame(G),{destroy:()=>{cancelAnimationFrame(D),N.disconnect(),"auto"===m&&(E.removeEventListener("change",F),L?.disconnect()),window.removeEventListener("mousemove",J),C.remove(),S.style.position=B}}}var v=C;exports.BACKGROUND_TYPES=["wave","rain","stars","pulse","noise","grid","aurora","silk","void","morph","fire","dna","terrain","circuit"],exports.asciiBackground=C,exports.mountWaveBackground=v,exports.renderAuroraBackground=g,exports.renderCircuitBackground=w,exports.renderDnaBackground=$,exports.renderFireBackground=p,exports.renderGridBackground=f,exports.renderMorphBackground=u,exports.renderNoiseBackground=h,exports.renderPulseBackground=s,exports.renderRainBackground=i,exports.renderSilkBackground=M,exports.renderStarsBackground=c,exports.renderTerrainBackground=b,exports.renderVoidBackground=d,exports.renderWaveBackground=a;//# sourceMappingURL=backgrounds.cjs.map