@ansh-ui/core 0.2.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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/dist/behaviours.cjs +64 -0
  3. package/dist/behaviours.cjs.map +1 -0
  4. package/dist/behaviours.d.cts +3 -0
  5. package/dist/behaviours.d.ts +3 -0
  6. package/dist/behaviours.js +7 -0
  7. package/dist/behaviours.js.map +1 -0
  8. package/dist/chunk-23GSSIMK.cjs +439 -0
  9. package/dist/chunk-23GSSIMK.cjs.map +1 -0
  10. package/dist/chunk-56NNBIAO.js +2446 -0
  11. package/dist/chunk-56NNBIAO.js.map +1 -0
  12. package/dist/chunk-6PFYGJWD.cjs +136 -0
  13. package/dist/chunk-6PFYGJWD.cjs.map +1 -0
  14. package/dist/chunk-7MEIXWPH.js +313 -0
  15. package/dist/chunk-7MEIXWPH.js.map +1 -0
  16. package/dist/chunk-7MYTSAPR.js +83 -0
  17. package/dist/chunk-7MYTSAPR.js.map +1 -0
  18. package/dist/chunk-BOQQHICI.js +1036 -0
  19. package/dist/chunk-BOQQHICI.js.map +1 -0
  20. package/dist/chunk-C6XPQO2O.js +1170 -0
  21. package/dist/chunk-C6XPQO2O.js.map +1 -0
  22. package/dist/chunk-DXJ63QAR.cjs +723 -0
  23. package/dist/chunk-DXJ63QAR.cjs.map +1 -0
  24. package/dist/chunk-FAVIEION.js +705 -0
  25. package/dist/chunk-FAVIEION.js.map +1 -0
  26. package/dist/chunk-FNR35HTL.cjs +107 -0
  27. package/dist/chunk-FNR35HTL.cjs.map +1 -0
  28. package/dist/chunk-GHBRGMSO.cjs +91 -0
  29. package/dist/chunk-GHBRGMSO.cjs.map +1 -0
  30. package/dist/chunk-GJRIHN76.cjs +359 -0
  31. package/dist/chunk-GJRIHN76.cjs.map +1 -0
  32. package/dist/chunk-ILKUL7GD.cjs +115 -0
  33. package/dist/chunk-ILKUL7GD.cjs.map +1 -0
  34. package/dist/chunk-KTUBCQC3.js +409 -0
  35. package/dist/chunk-KTUBCQC3.js.map +1 -0
  36. package/dist/chunk-KU2C6GEA.js +90 -0
  37. package/dist/chunk-KU2C6GEA.js.map +1 -0
  38. package/dist/chunk-LGA3N3YJ.cjs +413 -0
  39. package/dist/chunk-LGA3N3YJ.cjs.map +1 -0
  40. package/dist/chunk-MMO7KO53.cjs +1060 -0
  41. package/dist/chunk-MMO7KO53.cjs.map +1 -0
  42. package/dist/chunk-NSXKCJJ7.js +109 -0
  43. package/dist/chunk-NSXKCJJ7.js.map +1 -0
  44. package/dist/chunk-QALZKHWH.js +387 -0
  45. package/dist/chunk-QALZKHWH.js.map +1 -0
  46. package/dist/chunk-R7UJ7MWP.cjs +2479 -0
  47. package/dist/chunk-R7UJ7MWP.cjs.map +1 -0
  48. package/dist/chunk-UJBXY6QC.cjs +1186 -0
  49. package/dist/chunk-UJBXY6QC.cjs.map +1 -0
  50. package/dist/chunk-YFQXRC5R.js +99 -0
  51. package/dist/chunk-YFQXRC5R.js.map +1 -0
  52. package/dist/color.cjs +257 -0
  53. package/dist/color.cjs.map +1 -0
  54. package/dist/color.d.cts +303 -0
  55. package/dist/color.d.ts +303 -0
  56. package/dist/color.js +8 -0
  57. package/dist/color.js.map +1 -0
  58. package/dist/easing.cjs +189 -0
  59. package/dist/easing.cjs.map +1 -0
  60. package/dist/easing.d.cts +134 -0
  61. package/dist/easing.d.ts +134 -0
  62. package/dist/easing.js +4 -0
  63. package/dist/easing.js.map +1 -0
  64. package/dist/effects.cjs +99 -0
  65. package/dist/effects.cjs.map +1 -0
  66. package/dist/effects.d.cts +5 -0
  67. package/dist/effects.d.ts +5 -0
  68. package/dist/effects.js +10 -0
  69. package/dist/effects.js.map +1 -0
  70. package/dist/index-CCdpefMp.d.ts +967 -0
  71. package/dist/index-CQEEPu-D.d.ts +435 -0
  72. package/dist/index-DHOfsEIn.d.cts +967 -0
  73. package/dist/index-Dl8GCsya.d.cts +435 -0
  74. package/dist/index.cjs +2206 -0
  75. package/dist/index.cjs.map +1 -0
  76. package/dist/index.d.cts +1122 -0
  77. package/dist/index.d.ts +1122 -0
  78. package/dist/index.js +1182 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/math.cjs +238 -0
  81. package/dist/math.cjs.map +1 -0
  82. package/dist/math.d.cts +120 -0
  83. package/dist/math.d.ts +120 -0
  84. package/dist/math.js +5 -0
  85. package/dist/math.js.map +1 -0
  86. package/dist/noise.cjs +78 -0
  87. package/dist/noise.cjs.map +1 -0
  88. package/dist/noise.d.cts +190 -0
  89. package/dist/noise.d.ts +190 -0
  90. package/dist/noise.js +5 -0
  91. package/dist/noise.js.map +1 -0
  92. package/dist/palette-6HkVltYU.d.cts +219 -0
  93. package/dist/palette-6HkVltYU.d.ts +219 -0
  94. package/dist/random--4mSc5Lp.d.cts +75 -0
  95. package/dist/random--4mSc5Lp.d.ts +75 -0
  96. package/dist/reduced-motion-D0LUgZle.d.cts +120 -0
  97. package/dist/reduced-motion-D0LUgZle.d.ts +120 -0
  98. package/dist/types-B1FLLf3-.d.cts +21 -0
  99. package/dist/types-B1FLLf3-.d.ts +21 -0
  100. package/package.json +78 -0
@@ -0,0 +1,967 @@
1
+ import { U as Unsubscribe, R as ReducedMotionStrategy, a as FrameState } from './reduced-motion-D0LUgZle.js';
2
+ import { R as Rng } from './random--4mSc5Lp.js';
3
+ import { Noise } from './noise.js';
4
+ import { R as Rgb, a as ColorInput, P as PaletteName } from './palette-6HkVltYU.js';
5
+
6
+ /**
7
+ * Unified pointer tracking.
8
+ *
9
+ * Three problems this solves that every project re-solves badly:
10
+ *
11
+ * 1. **Mouse, touch and pen are one input.** Pointer Events already unify them; this wraps
12
+ * that so effects never branch on input type. Touch gets a synthetic "leave" on `pointerup`
13
+ * because a finger genuinely does leave the surface, which mouse code forgets.
14
+ *
15
+ * 2. **Raw pointer coordinates are jittery.** A cursor moves in discrete jumps and a mesh
16
+ * gradient that follows it exactly looks nervous. We expose both the raw position and a
17
+ * critically-damped smoothed position; effects should use the smoothed one.
18
+ *
19
+ * 3. **Reading `getBoundingClientRect()` per move is a layout read in an event handler** —
20
+ * the classic thrash. The element rect is cached and refreshed in the ticker's `read` phase.
21
+ */
22
+ interface PointerState {
23
+ /** Position in CSS pixels relative to the element's top-left. */
24
+ x: number;
25
+ y: number;
26
+ /** Normalised to `[0, 1]` across the element. Clamped, so it stays sane outside the bounds. */
27
+ nx: number;
28
+ ny: number;
29
+ /** Critically-damped position, in element pixels. Prefer this for anything visual. */
30
+ smoothX: number;
31
+ smoothY: number;
32
+ /** Normalised smoothed position. */
33
+ smoothNx: number;
34
+ smoothNy: number;
35
+ /** Velocity in element pixels per second. */
36
+ velocityX: number;
37
+ velocityY: number;
38
+ /** Is the pointer currently over the element? */
39
+ inside: boolean;
40
+ /** Is a button or finger currently down? */
41
+ down: boolean;
42
+ /** Normalised pressure in `[0, 1]`. `0.5` for devices that don't report it. */
43
+ pressure: number;
44
+ /** `'mouse' | 'pen' | 'touch'`, or `'none'` before the first event. */
45
+ type: 'mouse' | 'pen' | 'touch' | 'none';
46
+ /**
47
+ * Has the pointer ever been over this element?
48
+ *
49
+ * Effects use this to avoid snapping from the element's centre to the cursor on first hover —
50
+ * before any interaction, the "pointer" should be treated as absent, not as at (0,0).
51
+ */
52
+ everInside: boolean;
53
+ }
54
+ interface PointerOptions {
55
+ /**
56
+ * Smoothing half-life factor passed to `damp`. Lower is snappier.
57
+ * `0.001` is near-instant, `0.2` is languid. Default `0.005`.
58
+ */
59
+ smoothing?: number;
60
+ /** Track movement even when the pointer is outside the element. Default `false`. */
61
+ trackOutside?: boolean;
62
+ /**
63
+ * Where the smoothed position rests before first interaction, normalised.
64
+ * Default `{ x: 0.5, y: 0.5 }` — the centre, which is the least surprising neutral pose.
65
+ */
66
+ origin?: {
67
+ x: number;
68
+ y: number;
69
+ };
70
+ }
71
+ interface PointerTracker {
72
+ readonly state: Readonly<PointerState>;
73
+ /** Recompute the cached element rect. Called automatically on resize and scroll. */
74
+ refresh(): void;
75
+ destroy(): void;
76
+ }
77
+ /**
78
+ * Attach pointer tracking to an element.
79
+ *
80
+ * Listeners are passive and the only per-event work is arithmetic — no layout reads, no style
81
+ * writes. Smoothing is integrated in the ticker's `compute` phase so it stays frame-rate
82
+ * independent.
83
+ */
84
+ declare function trackPointer(element: HTMLElement, options?: PointerOptions): PointerTracker;
85
+
86
+ /**
87
+ * A managed canvas drawing surface.
88
+ *
89
+ * Getting a canvas right is deceptively fiddly, and every project gets some of it wrong:
90
+ *
91
+ * - **Device pixel ratio.** A canvas sized only in CSS renders blurry on any Retina display.
92
+ * The backing store must be `size × dpr` with a matching context transform.
93
+ * - **Resolution scaling.** Full DPR is wasteful for soft, out-of-focus imagery. A blurred
94
+ * gradient at 0.5× resolution is visually identical and costs a quarter of the fill rate,
95
+ * so effects declare their own `resolution` and adaptive quality can lower it further.
96
+ * - **Context loss.** Browsers discard canvas backing stores under memory pressure —
97
+ * routinely on iOS Safari with several canvases on a page. Without a restore path the
98
+ * effect silently goes blank and never comes back.
99
+ * - **Resize storms.** A window drag fires dozens of resize events per second, and
100
+ * reallocating a multi-megabyte backing store on each one janks badly. Resizes are
101
+ * coalesced to one per frame.
102
+ */
103
+ interface Size {
104
+ /** CSS pixels. */
105
+ width: number;
106
+ height: number;
107
+ /** Backing-store pixels: `width × dpr × resolution`. */
108
+ pixelWidth: number;
109
+ pixelHeight: number;
110
+ /** The effective scale factor from CSS pixels to backing-store pixels. */
111
+ scale: number;
112
+ }
113
+ interface SurfaceOptions {
114
+ /**
115
+ * Resolution multiplier applied on top of the device pixel ratio, in `(0, 1]`.
116
+ * `1` is full DPR; `0.5` quarters the pixel count. Default `1`.
117
+ */
118
+ resolution?: number;
119
+ /**
120
+ * Cap on the device pixel ratio actually used. Default `2`.
121
+ *
122
+ * Phones report DPRs up to 4. Rendering a full-viewport canvas at 4× costs sixteen times
123
+ * the fill rate of 1× for a difference nobody can see on a 5-inch screen at arm's length.
124
+ */
125
+ maxDevicePixelRatio?: number;
126
+ /** `alpha: false` lets the compositor skip blending. Default `true` — most effects need it. */
127
+ alpha?: boolean;
128
+ /** Hint that the canvas will be read back with `getImageData`. Default `false`. */
129
+ willReadFrequently?: boolean;
130
+ /** Called after a resize, before the next frame is drawn. */
131
+ onResize?: (size: Size) => void;
132
+ /**
133
+ * Called after the context is lost and restored. All GPU-side state is gone at this point;
134
+ * the effect must rebuild any cached gradients or patterns.
135
+ */
136
+ onRestore?: () => void;
137
+ }
138
+ interface Surface {
139
+ readonly canvas: HTMLCanvasElement;
140
+ /** The 2D context, or `null` if the browser refused to provide one. */
141
+ readonly ctx: CanvasRenderingContext2D | null;
142
+ readonly size: Readonly<Size>;
143
+ /** `true` while the context is lost; effects should skip drawing. */
144
+ readonly isLost: boolean;
145
+ /**
146
+ * Reset the transform to the current scale and clear the canvas.
147
+ * Call at the top of each frame unless deliberately compositing over the previous one.
148
+ */
149
+ clear(): void;
150
+ /** Change the resolution multiplier at runtime. Used by adaptive quality. */
151
+ setResolution(resolution: number): void;
152
+ /** Force a re-measure and resize. */
153
+ resize(): void;
154
+ destroy(): void;
155
+ }
156
+ /**
157
+ * Create a managed surface over a canvas element.
158
+ *
159
+ * The canvas is sized from its parent element, because a canvas has no intrinsic layout size
160
+ * of its own — it must be told, and its parent is the thing CSS has actually sized.
161
+ */
162
+ declare function createSurface(canvas: HTMLCanvasElement, options?: SurfaceOptions): Surface;
163
+ /** Is `OffscreenCanvas` with a 2D context available? Gates the worker rendering path. */
164
+ declare function supportsOffscreenCanvas(): boolean;
165
+ /** Can this canvas hand its rendering to a worker? Requires `transferControlToOffscreen`. */
166
+ declare function supportsWorkerRendering(canvas: HTMLCanvasElement): boolean;
167
+ type Unsub = Unsubscribe;
168
+
169
+ /**
170
+ * Canvas drawing primitives shared across effects.
171
+ *
172
+ * These exist so that "draw a soft glow" is one call with one consistent look, rather than
173
+ * eight effects each inventing their own radial gradient with slightly different falloff.
174
+ * Consistency of light and falloff is a large part of why a component set looks designed
175
+ * rather than assembled.
176
+ */
177
+ /**
178
+ * A cache for `CanvasGradient` objects.
179
+ *
180
+ * Creating a gradient is not free, and effects that draw dozens of glows per frame would
181
+ * otherwise allocate dozens of gradient objects per frame. Keyed by geometry and colour, so
182
+ * a field of identically-coloured particles shares one object.
183
+ *
184
+ * Bounded and cleared wholesale rather than evicted per-entry: gradients are cheap to rebuild,
185
+ * the working set is small and stable, and LRU bookkeeping would cost more than it saves.
186
+ */
187
+ declare class GradientCache {
188
+ private readonly limit;
189
+ private readonly cache;
190
+ constructor(limit?: number);
191
+ radial(ctx: CanvasRenderingContext2D, radius: number, stops: readonly [number, string][], key: string): CanvasGradient;
192
+ clear(): void;
193
+ }
194
+ /**
195
+ * A soft radial glow.
196
+ *
197
+ * The falloff is deliberately not linear. A linear alpha ramp produces a visible hard edge at
198
+ * the boundary because the eye is sensitive to the discontinuity in the *derivative*. Two
199
+ * intermediate stops approximating a Gaussian shoulder remove it, which is the difference
200
+ * between "glow" and "circle with a gradient".
201
+ */
202
+ declare function drawGlow(ctx: CanvasRenderingContext2D, x: number, y: number, radius: number, color: Rgb, intensity?: number, cache?: GradientCache): void;
203
+ /** A filled circle. Cheaper than {@link drawGlow} when no falloff is needed. */
204
+ declare function drawDot(ctx: CanvasRenderingContext2D, x: number, y: number, radius: number, color: Rgb): void;
205
+ /**
206
+ * A closed Catmull-Rom spline through the given points, rendered as bézier segments.
207
+ *
208
+ * Effects that morph organic shapes (blobs, aurora ribbons, wave crests) need a curve that
209
+ * passes *through* its control points, which bézier does not and Catmull-Rom does. Converting
210
+ * to cubic béziers lets the canvas rasterise it natively rather than us approximating with
211
+ * line segments.
212
+ *
213
+ * @param tension `0.5` is the standard Catmull-Rom; lower is tighter, higher is looser.
214
+ */
215
+ declare function traceClosedSpline(ctx: CanvasRenderingContext2D, points: readonly {
216
+ x: number;
217
+ y: number;
218
+ }[], tension?: number): void;
219
+ /** An open Catmull-Rom spline. Used for wave crests and ribbon paths. */
220
+ declare function traceOpenSpline(ctx: CanvasRenderingContext2D, points: readonly {
221
+ x: number;
222
+ y: number;
223
+ }[], tension?: number): void;
224
+ /** A rounded rectangle path, using the native `roundRect` when available. */
225
+ declare function traceRoundRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number): void;
226
+ /**
227
+ * Canvas composite operations, narrowed to the ones that behave consistently across browsers
228
+ * and are actually useful for the effects here.
229
+ *
230
+ * `'lighter'` (additive) is the workhorse: overlapping glows accumulate brightness the way
231
+ * real light does, which is what makes particle fields look luminous instead of pasted.
232
+ */
233
+ type BlendMode = 'source-over' | 'lighter' | 'screen' | 'overlay' | 'multiply' | 'soft-light' | 'hard-light' | 'color-dodge' | 'hue' | 'saturation' | 'color' | 'luminosity';
234
+ declare function supportsCanvasFilter(ctx: CanvasRenderingContext2D): boolean;
235
+ /** Reset the memoised filter probe. Test-only. */
236
+ declare function resetFilterSupport(): void;
237
+
238
+ /**
239
+ * The Effect contract.
240
+ *
241
+ * This is the interface that makes "500+ components" a tractable claim rather than a marketing
242
+ * one. An Effect is a plain object with no framework knowledge, no DOM ownership beyond the
243
+ * canvas it is handed, and no lifecycle of its own. Everything shared — canvas sizing, pointer
244
+ * tracking, reduced-motion handling, off-screen pausing, adaptive quality, teardown — lives in
245
+ * {@link import('./mount').mountEffect}.
246
+ *
247
+ * The consequence: a new visual effect is one file with one `frame()` function, and a new
248
+ * *framework binding* is one `createEffectComponent` helper rather than N component rewrites.
249
+ */
250
+ /**
251
+ * Options every effect accepts.
252
+ *
253
+ * These are deliberately uniform across the whole library. A developer who has learned
254
+ * `speed`/`intensity`/`colors` on one background knows them on all of them, and a design
255
+ * system can set them globally without knowing which effect is underneath.
256
+ */
257
+ interface BaseEffectOptions {
258
+ /**
259
+ * Any number of colours, or the name of a built-in palette.
260
+ *
261
+ * There is no cap and no assumption about count — an effect that wants exactly six control
262
+ * colours expands or samples whatever it is given. Accepts every CSS colour syntax including
263
+ * `var(--token)`.
264
+ */
265
+ colors?: readonly ColorInput[] | PaletteName;
266
+ /** Time multiplier. `0` freezes, `1` is the designed speed, `2` is double. Default `1`. */
267
+ speed?: number;
268
+ /**
269
+ * Strength of the effect's defining characteristic — amplitude, contrast, displacement.
270
+ * Normalised so `1` is the designed look and `0` is the effect's null state. Default `1`.
271
+ */
272
+ intensity?: number;
273
+ /** Overall alpha applied to the whole effect, in `[0, 1]`. Default `1`. */
274
+ opacity?: number;
275
+ /** Blur in CSS pixels applied via the canvas filter, where supported. Default `0`. */
276
+ blur?: number;
277
+ /** Canvas composite operation for the effect's own internal layering. */
278
+ blend?: BlendMode;
279
+ /**
280
+ * Seed for all stochastic behaviour. The same seed always produces the same composition,
281
+ * which is what makes SSR agreement and screenshot testing possible. Accepts a string.
282
+ * Default `1`.
283
+ */
284
+ seed?: number | string;
285
+ /** React to pointer input. Default `true` for effects that support it. */
286
+ interactive?: boolean;
287
+ /**
288
+ * Render resolution multiplier in `(0, 1]`. Soft, blurred imagery is indistinguishable at
289
+ * `0.5` and costs a quarter of the fill rate. Each effect picks a sensible default.
290
+ */
291
+ resolution?: number;
292
+ /**
293
+ * - `'auto'` — adapt quality to measured frame cost. The default, and the right choice.
294
+ * - `'high'` — pin maximum quality. For controlled environments and screenshot capture.
295
+ * - `'low'` — pin reduced quality. For dashboards where the effect is decorative.
296
+ */
297
+ performance?: 'auto' | 'high' | 'low';
298
+ /** How to behave under `prefers-reduced-motion`. Each effect declares a sensible default. */
299
+ reducedMotion?: ReducedMotionStrategy;
300
+ /** Stop rendering while `true`, holding the last frame. Default `false`. */
301
+ paused?: boolean;
302
+ /**
303
+ * Skip rendering while the host element is outside the viewport. Default `true`.
304
+ *
305
+ * This is usually the single largest performance win on a long marketing page: five
306
+ * animated sections cost the same as one, because only the visible one runs.
307
+ */
308
+ pauseOffscreen?: boolean;
309
+ }
310
+ /** What an effect is given at setup. Everything it needs, nothing it must construct. */
311
+ interface EffectContext {
312
+ /** The 2D context, pre-scaled for device pixel ratio. Draw in CSS pixels. */
313
+ readonly ctx: CanvasRenderingContext2D;
314
+ readonly surface: Surface;
315
+ /** Current dimensions. Mutated in place on resize — read it, don't cache it. */
316
+ readonly size: Readonly<Size>;
317
+ /** Seeded RNG. Never call `Math.random()` in an effect. */
318
+ readonly rng: Rng;
319
+ /** Noise fields derived from the same seed. */
320
+ readonly noise: Noise;
321
+ /** Pointer state, or `null` when `interactive` is false. */
322
+ readonly pointer: Readonly<PointerState> | null;
323
+ /** The element the canvas lives in. Use for `var()` resolution against the local cascade. */
324
+ readonly host: HTMLElement;
325
+ /** Resolved palette, already parsed. Never re-parse colours in a frame loop. */
326
+ readonly colors: readonly Rgb[];
327
+ }
328
+ /** A live, mounted effect. */
329
+ interface EffectInstance {
330
+ /** Draw one frame. Called by the ticker in the `write` phase. */
331
+ frame(state: FrameState): void;
332
+ /** Called after the canvas has been resized. Rebuild any size-dependent caches. */
333
+ resize?(size: Readonly<Size>): void;
334
+ /**
335
+ * Apply changed options without remounting.
336
+ *
337
+ * Implement this for anything cheap (colours, speed, intensity). Omit it for options that
338
+ * change the effect's structure — {@link import('./mount').mountEffect} remounts instead,
339
+ * which is correct and much simpler than reconciling a particle system in place.
340
+ */
341
+ update?(options: Record<string, unknown>, context: EffectContext): void;
342
+ /** Release anything the effect allocated. The surface and observers are not yours to free. */
343
+ destroy?(): void;
344
+ }
345
+ /**
346
+ * A registered effect.
347
+ *
348
+ * `defaults` is `Required<Options>` on purpose: it is both the runtime fallback and the
349
+ * documentation source. The docs site renders the props table from it, and the CLI uses it to
350
+ * scaffold examples — so "every option has a documented default" is enforced by the type system
351
+ * rather than by review.
352
+ */
353
+ interface EffectDefinition<Options extends BaseEffectOptions = BaseEffectOptions> {
354
+ readonly name: string;
355
+ readonly defaults: Required<Options>;
356
+ /**
357
+ * Options that cannot be applied live and require a remount when they change.
358
+ * Keeps the "should I remount?" decision with the effect that knows the answer.
359
+ */
360
+ readonly structuralOptions?: readonly (keyof Options)[];
361
+ /** Capability required to instantiate. Used for Pro-tier gating. Default `'effect:core'`. */
362
+ readonly capability?: string;
363
+ setup(context: EffectContext, options: Required<Options>): EffectInstance;
364
+ }
365
+ /** Convenience alias for the frame callback signature effects receive. */
366
+ type EffectFrame = (state: FrameState) => void;
367
+
368
+ /**
369
+ * Mount an effect into a host element.
370
+ *
371
+ * Everything that is the same for every effect happens here, exactly once:
372
+ *
373
+ * - creating and sizing the canvas (DPR-correct, resize-coalesced, context-loss-recovering)
374
+ * - resolving colours, seeds and noise fields
375
+ * - pointer tracking, if the effect wants it
376
+ * - reduced-motion resolution and live reaction to the preference changing
377
+ * - off-screen pausing via a pooled IntersectionObserver
378
+ * - adaptive quality → render resolution
379
+ * - one ticker subscription, in the `write` phase
380
+ * - complete teardown
381
+ *
382
+ * An individual effect therefore contains only what makes it *that* effect. This is the
383
+ * single most important structural decision in the library: it is why adding an effect is
384
+ * ~100 lines and why the React and Vue components are ~40.
385
+ */
386
+ interface EffectHandle {
387
+ readonly canvas: HTMLCanvasElement;
388
+ /**
389
+ * Apply new options.
390
+ *
391
+ * Live-updates whatever the effect supports and remounts when a structural option changes.
392
+ * Framework bindings call this on every prop change, so it must be cheap in the common case.
393
+ */
394
+ update(options: Partial<BaseEffectOptions & Record<string, unknown>>): void;
395
+ pause(): void;
396
+ resume(): void;
397
+ /** Force one frame to be drawn even while paused. Used for the static reduced-motion render. */
398
+ renderOnce(): void;
399
+ destroy(): void;
400
+ readonly isRunning: boolean;
401
+ }
402
+ declare function mountEffect<Options extends BaseEffectOptions>(definition: EffectDefinition<Options>, host: HTMLElement, userOptions?: Partial<Options>): EffectHandle;
403
+ /**
404
+ * Register an effect so the CLI, docs site and registry can discover it.
405
+ * Returns the definition unchanged, so it can wrap a declaration inline.
406
+ */
407
+ declare function defineEffect<Options extends BaseEffectOptions>(definition: EffectDefinition<Options>): EffectDefinition<Options>;
408
+
409
+ /**
410
+ * A structure-of-arrays particle buffer.
411
+ *
412
+ * Every particle effect in the library sits on this. The layout is deliberate:
413
+ *
414
+ * **SoA, not AoS.** An array of `{x, y, vx, vy}` objects means the integrator walks a pointer
415
+ * chase through scattered heap allocations, and V8 must check the hidden class of each. Parallel
416
+ * `Float32Array`s mean the integration loop reads contiguous memory, stays monomorphic, and
417
+ * never allocates. At 2,000 particles the difference is roughly 4×; more importantly, the AoS
418
+ * version generates garbage every frame, and GC pauses are exactly the stutter users notice.
419
+ *
420
+ * **Swap-remove, not splice.** Killing a particle moves the last live one into its slot and
421
+ * decrements the count. O(1), no reallocation, no shifting. Particle order is not meaningful
422
+ * so nothing is lost — and the effects that *do* care about draw order (depth-sorted snow)
423
+ * sort an index array instead.
424
+ *
425
+ * **Fixed capacity.** Allocated once at the maximum the effect will ever need. Growing a
426
+ * typed array means allocating a new one and copying, which is precisely the kind of
427
+ * multi-megabyte operation that must never happen inside a frame.
428
+ */
429
+ declare class ParticleBuffer {
430
+ readonly capacity: number;
431
+ readonly x: Float32Array;
432
+ readonly y: Float32Array;
433
+ readonly vx: Float32Array;
434
+ readonly vy: Float32Array;
435
+ /** Remaining life in seconds. `Infinity` for particles that never expire. */
436
+ readonly life: Float32Array;
437
+ /** Life the particle started with, for computing normalised age. */
438
+ readonly maxLife: Float32Array;
439
+ readonly size: Float32Array;
440
+ /** Index into the effect's resolved palette. */
441
+ readonly colorIndex: Uint8Array;
442
+ /** Free-form per-particle scalar — phase, spin, depth, whatever the effect needs. */
443
+ readonly seed: Float32Array;
444
+ /** Second free-form scalar. */
445
+ readonly extra: Float32Array;
446
+ private liveCount;
447
+ constructor(capacity: number);
448
+ get count(): number;
449
+ /**
450
+ * Claim a slot.
451
+ * @returns The particle index, or `-1` when the buffer is full. Callers must check.
452
+ */
453
+ spawn(): number;
454
+ /**
455
+ * Kill particle `index` by swapping the last live particle into its slot.
456
+ *
457
+ * The caller's loop must **not** advance its cursor after calling this, since a new particle
458
+ * now occupies the current index. Every consumer in this package follows that contract with
459
+ * a `while` loop rather than a `for`.
460
+ */
461
+ kill(index: number): void;
462
+ /** Normalised age in `[0, 1]`. `0` for immortal particles. */
463
+ age(index: number): number;
464
+ /**
465
+ * Reduce the live count, keeping the first `target` particles.
466
+ *
467
+ * Used by adaptive quality: dropping from 1,200 to 800 particles must not resample or
468
+ * reshuffle, because a visible reshuffle draws far more attention than the density change.
469
+ */
470
+ truncate(target: number): void;
471
+ clear(): void;
472
+ }
473
+ /**
474
+ * Advance positions by velocity — symplectic Euler.
475
+ *
476
+ * Deliberately not Runge-Kutta. RK4 is more *accurate*, but for particles driven by noise
477
+ * fields there is no ground truth to be accurate to, and symplectic Euler has the property
478
+ * that actually matters here: it conserves energy over long runs, so a field of particles
479
+ * doesn't slowly gain or lose speed over minutes of animation.
480
+ */
481
+ declare function integrate(buffer: ParticleBuffer, dt: number, drag?: number): void;
482
+ /** Reduce life and swap-remove anything that has expired. */
483
+ declare function ageAndCull(buffer: ParticleBuffer, dt: number): void;
484
+ /** Wrap particles that leave the bounds back to the opposite edge, with a margin. */
485
+ declare function wrapBounds(buffer: ParticleBuffer, width: number, height: number, margin?: number): void;
486
+ /**
487
+ * A uniform grid for neighbour queries.
488
+ *
489
+ * The constellation effect needs "every pair of particles within distance R", which is O(n²)
490
+ * naively — 2,000 particles is 4 million distance checks per frame, which is not viable. A
491
+ * spatial hash with cell size R reduces it to checking each particle against the nine cells
492
+ * around it, which is effectively O(n) for uniformly distributed particles.
493
+ *
494
+ * Cells are stored as a flat `Int32Array` bucket list rather than an array of arrays, so
495
+ * rebuilding it each frame allocates nothing.
496
+ */
497
+ declare class SpatialGrid {
498
+ private capacity;
499
+ private cols;
500
+ private rows;
501
+ private cellSize;
502
+ /** Start index of each cell's run inside `items`. Length `cols * rows + 1`. */
503
+ private cellStart;
504
+ /** Particle indices, grouped by cell. */
505
+ private items;
506
+ /** Scratch counters used while building. */
507
+ private counts;
508
+ constructor(capacity: number);
509
+ /**
510
+ * Bucket every live particle. Must be called before {@link forEachNeighbour}.
511
+ * @param cellSize Should equal the query radius — smaller means more cells to scan, larger
512
+ * means more candidates per cell.
513
+ */
514
+ build(buffer: ParticleBuffer, width: number, height: number, cellSize: number): void;
515
+ private cellIndex;
516
+ /**
517
+ * Visit candidate neighbours of particle `index` in the 3×3 cell block around it.
518
+ *
519
+ * Only pairs where `neighbour > index` are visited, so each pair is reported exactly once —
520
+ * which halves the work and means the caller doesn't need a "have I drawn this line" check.
521
+ * The callback still receives candidates outside the radius; the caller does the exact test,
522
+ * because it usually needs the distance anyway.
523
+ */
524
+ forEachNeighbour(buffer: ParticleBuffer, index: number, visit: (neighbour: number) => void): void;
525
+ }
526
+
527
+ /**
528
+ * Aurora — vertical curtains of light with filament structure.
529
+ *
530
+ * What separates this from "a few blurred gradients" is the *filaments*. A real aurora is
531
+ * ridged: bright vertical striations at many scales, because the emission follows magnetic
532
+ * field lines. We get that from ridged multifractal noise (`1 - |n|`, squared) sampled per
533
+ * vertical slice, which produces sharp bright lines with soft valleys between them — exactly
534
+ * the wrong result for terrain and exactly the right one here.
535
+ *
536
+ * Rendering is a set of vertical slices per curtain rather than a single filled path, because
537
+ * the per-slice alpha *is* the filament structure. Each slice is one gradient fill, so a
538
+ * four-curtain composition at 48 slices is under 200 fills per frame — comfortably inside
539
+ * budget at half resolution, which is where blurred imagery belongs anyway.
540
+ */
541
+ interface AuroraOptions extends BaseEffectOptions {
542
+ /** Number of overlapping curtains. More reads as depth. Default `4`. */
543
+ curtains?: number;
544
+ /** Vertical slices per curtain — the filament resolution. Default `48`. */
545
+ slices?: number;
546
+ /** Height of the curtains as a fraction of the canvas. Default `0.75`. */
547
+ height?: number;
548
+ /** Vertical position of the curtain band centre, `0` top to `1` bottom. Default `0.42`. */
549
+ position?: number;
550
+ /** Spatial frequency of the filaments. Higher is finer. Default `2.4`. */
551
+ detail?: number;
552
+ /** How far curtains drift horizontally, as a fraction of width. Default `0.18`. */
553
+ drift?: number;
554
+ /** Softness of the vertical falloff, in `[0, 1]`. Default `0.55`. */
555
+ falloff?: number;
556
+ /** Pointer influence on curtain sway, in `[0, 1]`. Default `0.35`. */
557
+ pointerInfluence?: number;
558
+ }
559
+ declare const aurora: EffectDefinition<AuroraOptions>;
560
+
561
+ /**
562
+ * Mesh Gradient — soft overlapping colour fields that drift and blend.
563
+ *
564
+ * This is the effect behind almost every modern SaaS hero section, and it is nearly always
565
+ * implemented as a static SVG or a CSS blur stack. Doing it properly on canvas buys three
566
+ * things those can't:
567
+ *
568
+ * 1. **Motion that isn't a loop.** Control points follow a two-frequency Lissajous path with
569
+ * an irrational frequency ratio, so the composition never visibly repeats. A CSS keyframe
570
+ * animation always does, and the eye finds the loop point within about fifteen seconds.
571
+ * 2. **Perceptual blending.** Overlaps are composited in linear-ish additive space with
572
+ * OKLab-derived palettes, so blue over magenta stays vivid instead of going grey.
573
+ * 3. **Pointer response.** Points lean toward the cursor, which makes a hero section feel
574
+ * inhabited rather than decorative.
575
+ *
576
+ * Rendered as one radial gradient fill per point at quarter resolution with a heavy blur. The
577
+ * blur is doing most of the visual work, which is precisely why the low resolution is free.
578
+ */
579
+ interface MeshGradientOptions extends BaseEffectOptions {
580
+ /** Number of colour control points. Default `5`. */
581
+ points?: number;
582
+ /** Radius of each field as a fraction of the canvas diagonal. Default `0.55`. */
583
+ radius?: number;
584
+ /** How far each point wanders from its home, as a fraction of the canvas. Default `0.22`. */
585
+ wander?: number;
586
+ /** Pointer attraction strength, in `[0, 1]`. Default `0.4`. */
587
+ pointerInfluence?: number;
588
+ /** Radius of the pointer's influence as a fraction of the canvas. Default `0.45`. */
589
+ pointerRadius?: number;
590
+ /**
591
+ * Fill the canvas with the darkest palette colour before drawing.
592
+ * Without it the effect composites onto whatever is behind, which is usually what you want
593
+ * for an overlay and not what you want for a hero. Default `true`.
594
+ */
595
+ backdrop?: boolean;
596
+ /** Grain overlay strength, in `[0, 1]`. A little grain hides gradient banding. Default `0.04`. */
597
+ grain?: number;
598
+ }
599
+ declare const meshGradient: EffectDefinition<MeshGradientOptions>;
600
+ /**
601
+ * Animated Gradient — a simpler two-to-many stop linear gradient that rotates and shifts.
602
+ *
603
+ * Shares the mesh gradient's palette handling but costs a single fill per frame, which makes it
604
+ * the right choice for cards, buttons and any element small enough that the mesh's radial
605
+ * fields would overlap into mush.
606
+ */
607
+ interface AnimatedGradientOptions extends BaseEffectOptions {
608
+ /** Rotations per minute of the gradient axis. `0` holds a fixed angle. Default `4`. */
609
+ rotation?: number;
610
+ /** Starting angle in degrees. Default `120`. */
611
+ angle?: number;
612
+ /** How far stops slide along the axis, in `[0, 1]`. Default `0.25`. */
613
+ travel?: number;
614
+ /** Number of interpolated stops handed to the canvas. Default `24`. */
615
+ stops?: number;
616
+ }
617
+ declare const animatedGradient: EffectDefinition<AnimatedGradientOptions>;
618
+
619
+ /**
620
+ * Particle Field — points advected by a curl-noise flow.
621
+ *
622
+ * The detail that makes this look like fluid rather than like scattered dots: the velocity
623
+ * field is **divergence-free**. Advecting particles by raw noise makes them pool into sinks and
624
+ * evacuate sources, so within a few seconds the field is visibly clumpy and half the canvas is
625
+ * empty. Taking the perpendicular gradient of a noise potential — `(∂N/∂y, −∂N/∂x)` — gives a
626
+ * field with zero divergence, so particle density is preserved indefinitely and the motion reads
627
+ * as smoke or water.
628
+ *
629
+ * Particles are immortal and wrap at the edges rather than respawning, which avoids the
630
+ * distracting pop of particles appearing and vanishing.
631
+ */
632
+ interface ParticleFieldOptions extends BaseEffectOptions {
633
+ /** Particle count at full quality. Scaled down by adaptive quality. Default `700`. */
634
+ count?: number;
635
+ /** Particle radius in CSS pixels. Default `1.6`. */
636
+ size?: number;
637
+ /** Random variation in size, in `[0, 1]`. Default `0.6`. */
638
+ sizeVariance?: number;
639
+ /** Flow field spatial scale. Smaller means larger, smoother eddies. Default `0.0016`. */
640
+ scale?: number;
641
+ /** Speed of the particles through the field, in pixels per second. Default `48`. */
642
+ flowSpeed?: number;
643
+ /** How fast the field itself evolves. `0` gives a static flow. Default `0.12`. */
644
+ fieldDrift?: number;
645
+ /** Draw particles as soft glows rather than hard dots. Costs more. Default `true`. */
646
+ glow?: boolean;
647
+ /** Trail persistence in `[0, 1)`. `0` clears each frame; `0.9` leaves long trails. Default `0`. */
648
+ trails?: number;
649
+ /** Radius in pixels within which the pointer pushes particles. Default `140`. */
650
+ pointerRadius?: number;
651
+ /** Pointer force. Negative attracts, positive repels. Default `1`. */
652
+ pointerForce?: number;
653
+ }
654
+ declare const particleField: EffectDefinition<ParticleFieldOptions>;
655
+
656
+ /**
657
+ * Constellation — drifting points joined by lines when they come close.
658
+ *
659
+ * The interesting engineering problem here is the pairing. "Draw a line between every pair
660
+ * within distance R" is O(n²): at 220 points that's 24,000 checks per frame, which is fine, but
661
+ * authors routinely push the count to 800 and then it's 320,000 checks — enough to blow the
662
+ * budget on its own before anything is drawn.
663
+ *
664
+ * So this uses the {@link SpatialGrid} with cell size R. Each point only checks the nine cells
665
+ * around it, which is effectively O(n), and the grid reports each pair exactly once so no
666
+ * duplicate-line bookkeeping is needed.
667
+ *
668
+ * Lines are drawn in a single batched path per alpha bucket rather than one `stroke()` per line:
669
+ * `strokeStyle` changes force the canvas to flush, so 400 individual strokes cost far more than
670
+ * 8 batched ones.
671
+ */
672
+ interface ConstellationOptions extends BaseEffectOptions {
673
+ /** Number of nodes at full quality. Default `120`. */
674
+ count?: number;
675
+ /** Maximum distance in pixels at which two nodes are linked. Default `140`. */
676
+ linkDistance?: number;
677
+ /** Node radius in pixels. Default `2`. */
678
+ nodeSize?: number;
679
+ /** Line width in pixels. Default `1`. */
680
+ lineWidth?: number;
681
+ /** Drift speed in pixels per second. Default `18`. */
682
+ drift?: number;
683
+ /** Radius in pixels within which the pointer becomes a node. Default `180`. */
684
+ pointerRadius?: number;
685
+ /** Link the pointer position into the graph. Default `true`. */
686
+ pointerLinks?: boolean;
687
+ /** Alpha buckets used to batch line strokes. More is smoother, slower. Default `6`. */
688
+ alphaSteps?: number;
689
+ }
690
+ declare const constellation: EffectDefinition<ConstellationOptions>;
691
+
692
+ /**
693
+ * Meteor Shower — streaks that fall across the canvas and fade.
694
+ *
695
+ * The two things that make streaks convincing:
696
+ *
697
+ * **The trail is a gradient along the direction of travel, not a fading line.** A meteor is
698
+ * bright at the head and fades behind it, and the gradient must be rebuilt as the meteor moves
699
+ * because its endpoints move. That is one gradient per meteor per frame, which is affordable
700
+ * because there are only ever a dozen or so.
701
+ *
702
+ * **Spawning is Poisson, not periodic.** Evenly spaced spawns read as a machine. Sampling the
703
+ * gap between spawns from an exponential distribution gives clustering and lulls, which is what
704
+ * real meteor showers do and what makes the effect feel observed rather than generated.
705
+ */
706
+ interface MeteorsOptions extends BaseEffectOptions {
707
+ /** Average meteors visible at once. Default `12`. */
708
+ count?: number;
709
+ /** Direction of travel in degrees. `135` falls down-and-right. Default `135`. */
710
+ angle?: number;
711
+ /** Random variation in angle, in degrees. Default `8`. */
712
+ angleVariance?: number;
713
+ /** Speed in pixels per second. Default `420`. */
714
+ velocity?: number;
715
+ /** Random variation in speed, in `[0, 1]`. Default `0.45`. */
716
+ velocityVariance?: number;
717
+ /** Trail length in pixels. Default `160`. */
718
+ trailLength?: number;
719
+ /** Head thickness in pixels. Default `2` */
720
+ thickness?: number;
721
+ /** Draw a bright point at the head. Default `true`. */
722
+ head?: boolean;
723
+ }
724
+ declare const meteors: EffectDefinition<MeteorsOptions>;
725
+
726
+ /**
727
+ * Rain and Snow.
728
+ *
729
+ * These share one implementation because they are the same simulation with different
730
+ * parameters and one different draw call — and because the part that makes both convincing is
731
+ * the same: **depth**.
732
+ *
733
+ * Each drop or flake gets a `depth` in `[0, 1]`. Depth scales size, opacity *and* speed
734
+ * together, which produces motion parallax: near particles are large, opaque and fast; distant
735
+ * ones small, faint and slow. Without that coupling, precipitation looks like a flat sheet of
736
+ * identical sprites sliding down the screen, which is the tell in almost every implementation.
737
+ *
738
+ * Snow additionally sways — a sinusoidal horizontal drift with a per-flake phase and frequency,
739
+ * because a flake's motion is dominated by air resistance rather than gravity.
740
+ */
741
+ interface PrecipitationShared extends BaseEffectOptions {
742
+ /** Particle count at full quality. */
743
+ count?: number;
744
+ /** Fall angle in degrees, measured from the positive x-axis. `90` is straight down. */
745
+ angle?: number;
746
+ /** Wind offset in degrees added to `angle`. Separated so wind can be animated. */
747
+ wind?: number;
748
+ /** Fall speed in pixels per second at maximum depth. */
749
+ velocity?: number;
750
+ /** Depth range as `[near, far]` scale factors. */
751
+ depthRange?: readonly [number, number];
752
+ }
753
+ interface RainOptions extends PrecipitationShared {
754
+ /** Streak length in pixels at maximum depth. Default `18`. */
755
+ length?: number;
756
+ /** Streak width in pixels. Default `1.2`. */
757
+ thickness?: number;
758
+ /** Draw splash rings where drops reach the bottom. Default `false`. */
759
+ splash?: boolean;
760
+ }
761
+ declare const rain: EffectDefinition<RainOptions>;
762
+ interface SnowOptions extends PrecipitationShared {
763
+ /** Flake radius in pixels at maximum depth. Default `2.6`. */
764
+ size?: number;
765
+ /** Horizontal sway amplitude in pixels. Default `28`. */
766
+ sway?: number;
767
+ /** Sway cycles per second. Default `0.35`. */
768
+ swaySpeed?: number;
769
+ /** Soften flakes with a radial falloff. Costs more; looks better on dark backgrounds. Default `true`. */
770
+ soft?: boolean;
771
+ }
772
+ declare const snow: EffectDefinition<SnowOptions>;
773
+
774
+ /**
775
+ * Fireflies — soft points that wander and pulse.
776
+ *
777
+ * Two details do all the work:
778
+ *
779
+ * **Wandering, not drifting.** Each firefly's heading is driven by a slowly-evolving noise
780
+ * sample rather than a fixed velocity, so it curves and doubles back. A constant velocity —
781
+ * even a slow one — reads as dust falling, not as something alive.
782
+ *
783
+ * **Asymmetric pulsing.** The brightness envelope rises faster than it falls (a raised sine
784
+ * with a shaped exponent), because a real bioluminescent flash does. A symmetric sine looks
785
+ * like a dimmer being turned up and down.
786
+ */
787
+ interface FirefliesOptions extends BaseEffectOptions {
788
+ /** Number of fireflies at full quality. Default `40`. */
789
+ count?: number;
790
+ /** Glow radius in pixels. Default `5`. */
791
+ size?: number;
792
+ /** Wander speed in pixels per second. Default `22`. */
793
+ wanderSpeed?: number;
794
+ /** How sharply headings change. Higher is more erratic. Default `0.35`. */
795
+ turbulence?: number;
796
+ /** Average pulses per second. Default `0.5`. */
797
+ pulseRate?: number;
798
+ /** Minimum brightness between pulses, in `[0, 1]`. Default `0.12`. */
799
+ baseGlow?: number;
800
+ /** Radius in pixels within which the pointer scatters fireflies. Default `120`. */
801
+ pointerRadius?: number;
802
+ }
803
+ declare const fireflies: EffectDefinition<FirefliesOptions>;
804
+
805
+ /**
806
+ * Waves — layered undulating bands.
807
+ *
808
+ * The crest of each layer is the sum of three sine components at incommensurable frequencies
809
+ * plus a noise term. That specific recipe matters:
810
+ *
811
+ * - **One sine** is obviously a sine wave.
812
+ * - **Two sines** beat against each other in a period the eye picks up within seconds.
813
+ * - **Three sines at non-integer frequency ratios** produce a profile with no visible period.
814
+ * - **Plus noise** breaks the remaining symmetry so crests differ in height and sharpness.
815
+ *
816
+ * Layers are drawn back to front with decreasing opacity and increasing amplitude, which reads
817
+ * as depth. The path is a Catmull-Rom spline through sampled crest points rather than a polyline,
818
+ * because at 32 samples across a wide canvas a polyline shows visible faceting on the crests.
819
+ */
820
+ interface WavesOptions extends BaseEffectOptions {
821
+ /** Number of stacked wave layers. Default `4`. */
822
+ layers?: number;
823
+ /** Crest samples per layer. More is smoother and slower. Default `28`. */
824
+ samples?: number;
825
+ /** Wave height in pixels at the frontmost layer. Default `40`. */
826
+ amplitude?: number;
827
+ /** Horizontal wavelength as a fraction of canvas width. Default `0.6`. */
828
+ wavelength?: number;
829
+ /** Vertical position of the wave band, `0` top to `1` bottom. Default `0.68`. */
830
+ position?: number;
831
+ /** Noise contribution to the crest profile, in `[0, 1]`. Default `0.35`. */
832
+ roughness?: number;
833
+ /** Fill below the crest as a solid band rather than stroking the line. Default `true`. */
834
+ filled?: boolean;
835
+ /** Stroke width when `filled` is false, or the highlight width when it is true. Default `0`. */
836
+ lineWidth?: number;
837
+ }
838
+ declare const waves: EffectDefinition<WavesOptions>;
839
+
840
+ /**
841
+ * Liquid Blob — organic shapes that morph continuously.
842
+ *
843
+ * The radius at each angle is `base * (1 + noise(angle, time) * distortion)`, sampled around
844
+ * the circle and traced as a closed Catmull-Rom spline.
845
+ *
846
+ * The subtlety that most implementations get wrong: the noise must be sampled on a **circle in
847
+ * noise space**, not on the angle as a linear coordinate. If you sample `noise(angle, t)`
848
+ * directly, the value at `angle = 0` and `angle = 2π` are different points in the noise field,
849
+ * so the shape has a visible seam where it fails to close. Sampling
850
+ * `noise(cos(angle) * r, sin(angle) * r, t)` instead means the sample path is genuinely closed
851
+ * and the blob is seamless.
852
+ */
853
+ interface BlobOptions extends BaseEffectOptions {
854
+ /** Number of overlapping blobs. Default `3`. */
855
+ count?: number;
856
+ /** Points sampled around each blob's circumference. More is smoother. Default `48`. */
857
+ segments?: number;
858
+ /** Base radius as a fraction of the smaller canvas dimension. Default `0.32`. */
859
+ radius?: number;
860
+ /** How far the radius deviates, in `[0, 1]`. `0` is a circle. Default `0.35`. */
861
+ distortion?: number;
862
+ /** Noise detail around the circumference. Higher gives more lobes. Default `1.6`. */
863
+ detail?: number;
864
+ /** How fast the shape morphs. Default `0.4`. */
865
+ morphSpeed?: number;
866
+ /** How far blobs drift from their home position, as a fraction of the canvas. Default `0.1`. */
867
+ drift?: number;
868
+ /** Draw a stroke outline instead of a fill. Default `false`. */
869
+ outline?: boolean;
870
+ /** Stroke width when `outline` is true. Default `2`. */
871
+ lineWidth?: number;
872
+ /** Pointer attraction, in `[0, 1]`. Default `0.25`. */
873
+ pointerInfluence?: number;
874
+ }
875
+ declare const blob: EffectDefinition<BlobOptions>;
876
+
877
+ /**
878
+ * Noise Layer — film grain and dithering overlay.
879
+ *
880
+ * Grain does real work in a design system: it hides the banding that 8-bit gradients produce on
881
+ * large soft fills, and it gives flat colour a physical texture that reads as premium. It is the
882
+ * single cheapest way to make a gradient look intentional rather than default.
883
+ *
884
+ * The implementation is a pre-rendered tile stamped as a repeating pattern. That choice is not
885
+ * an optimisation, it is the only viable approach: generating full-canvas grain per frame needs
886
+ * `createImageData` + a per-pixel write + `putImageData`, which at 1920×1080 is 8 million writes
887
+ * — roughly 30 ms, twice the entire frame budget.
888
+ *
889
+ * Animated grain works by cycling between a handful of pre-generated tiles and jittering the
890
+ * pattern origin, which is visually indistinguishable from true per-frame noise because the eye
891
+ * cannot track individual grain pixels anyway.
892
+ */
893
+ interface NoiseLayerOptions extends BaseEffectOptions {
894
+ /** Grain strength in `[0, 1]`. `0.05` is subtle, `0.2` is heavy. Default `0.08`. */
895
+ amount?: number;
896
+ /** Tile size in pixels. Larger tiles repeat less visibly but cost more memory. Default `160`. */
897
+ tileSize?: number;
898
+ /** Number of pre-generated tiles cycled through when animating. Default `4`. */
899
+ frames?: number;
900
+ /** Tile changes per second. `0` gives static grain. Default `12`. */
901
+ frameRate?: number;
902
+ /**
903
+ * Grain scale in pixels. `1` is per-pixel; `2` gives chunkier, more filmic grain.
904
+ * Values above 1 also reduce generation cost quadratically. Default `1`.
905
+ */
906
+ grainSize?: number;
907
+ /** Monochrome grain, or independent per-channel (chromatic) noise. Default `true`. */
908
+ monochrome?: boolean;
909
+ }
910
+ declare const noiseLayer: EffectDefinition<NoiseLayerOptions>;
911
+
912
+ /**
913
+ * Spotlight — a soft light that follows the pointer.
914
+ *
915
+ * The trick that makes this read as *light* rather than as a moving circle is that it is drawn
916
+ * once as a large, very soft radial gradient with an additive blend, and it *eases into
917
+ * existence* rather than appearing at full strength. A spotlight that snaps on at the moment
918
+ * the cursor enters reads as a bug; one that fades up over ~200 ms reads as a light being aimed.
919
+ *
920
+ * The pointer position comes from the tracker's critically-damped `smooth` values, so the light
921
+ * trails very slightly behind the cursor. That lag is deliberate — a spotlight rigidly locked to
922
+ * the cursor feels weightless and cheap.
923
+ */
924
+ interface SpotlightOptions extends BaseEffectOptions {
925
+ /** Radius in pixels. Default `320`. */
926
+ radius?: number;
927
+ /** Falloff shape. `1` is linear, `2` is quadratic (softer core). Default `1.6`. */
928
+ falloff?: number;
929
+ /** Seconds for the light to fade in and out as the pointer enters and leaves. Default `0.35`. */
930
+ fade?: number;
931
+ /**
932
+ * Keep the light visible when the pointer is elsewhere, drifting slowly.
933
+ * Useful for hero sections that should look alive before any interaction. Default `false`.
934
+ */
935
+ ambient?: boolean;
936
+ /** Drift radius for the ambient light, as a fraction of the canvas. Default `0.25`. */
937
+ ambientDrift?: number;
938
+ /** Add a second, tighter and brighter core. Default `true`. */
939
+ core?: boolean;
940
+ }
941
+ declare const spotlight: EffectDefinition<SpotlightOptions>;
942
+
943
+ /**
944
+ * Every built-in effect, keyed by name.
945
+ *
946
+ * Used by the docs site to enumerate the catalogue and by the CLI to resolve `ansh add`.
947
+ * Application code should import individual effects so the rest tree-shake away — importing this
948
+ * map pulls in all of them.
949
+ */
950
+ declare const effects: {
951
+ readonly aurora: EffectDefinition<AuroraOptions>;
952
+ readonly 'mesh-gradient': EffectDefinition<MeshGradientOptions>;
953
+ readonly 'animated-gradient': EffectDefinition<AnimatedGradientOptions>;
954
+ readonly 'particle-field': EffectDefinition<ParticleFieldOptions>;
955
+ readonly constellation: EffectDefinition<ConstellationOptions>;
956
+ readonly meteors: EffectDefinition<MeteorsOptions>;
957
+ readonly rain: EffectDefinition<RainOptions>;
958
+ readonly snow: EffectDefinition<SnowOptions>;
959
+ readonly fireflies: EffectDefinition<FirefliesOptions>;
960
+ readonly waves: EffectDefinition<WavesOptions>;
961
+ readonly blob: EffectDefinition<BlobOptions>;
962
+ readonly 'noise-layer': EffectDefinition<NoiseLayerOptions>;
963
+ readonly spotlight: EffectDefinition<SpotlightOptions>;
964
+ };
965
+ type EffectName = keyof typeof effects;
966
+
967
+ export { supportsOffscreenCanvas as $, type AnimatedGradientOptions as A, type BaseEffectOptions as B, type ConstellationOptions as C, drawGlow as D, type EffectContext as E, type FirefliesOptions as F, GradientCache as G, effects as H, fireflies as I, integrate as J, meshGradient as K, meteors as L, type MeshGradientOptions as M, type NoiseLayerOptions as N, mountEffect as O, ParticleBuffer as P, noiseLayer as Q, type RainOptions as R, type Size as S, particleField as T, type Unsub as U, rain as V, type WavesOptions as W, resetFilterSupport as X, snow as Y, spotlight as Z, supportsCanvasFilter as _, type AuroraOptions as a, supportsWorkerRendering as a0, traceClosedSpline as a1, traceOpenSpline as a2, traceRoundRect as a3, trackPointer as a4, waves as a5, wrapBounds as a6, type BlendMode as b, type BlobOptions as c, type EffectDefinition as d, type EffectFrame as e, type EffectHandle as f, type EffectInstance as g, type EffectName as h, type MeteorsOptions as i, type ParticleFieldOptions as j, type PointerOptions as k, type PointerState as l, type PointerTracker as m, type SnowOptions as n, SpatialGrid as o, type SpotlightOptions as p, type Surface as q, type SurfaceOptions as r, ageAndCull as s, animatedGradient as t, aurora as u, blob as v, constellation as w, createSurface as x, defineEffect as y, drawDot as z };