@pixerate/flair 0.28.0 → 0.30.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 (59) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +54 -6
  3. package/dist/catalog/index.d.ts +11 -6
  4. package/dist/catalog/index.js +1 -1
  5. package/dist/{chunk-JL6TI6II.js → chunk-7KYQ4R76.js} +2 -2
  6. package/dist/chunk-7KYQ4R76.js.map +1 -0
  7. package/dist/{chunk-STKEKXCK.js → chunk-AUH3PEFW.js} +2 -2
  8. package/dist/{chunk-PFGGPIMT.js → chunk-DA4UUSDO.js} +58 -3
  9. package/dist/chunk-DA4UUSDO.js.map +1 -0
  10. package/dist/{chunk-RDU63CDO.js → chunk-HDYBY3PQ.js} +380 -1
  11. package/dist/chunk-HDYBY3PQ.js.map +1 -0
  12. package/dist/{chunk-GXP4YLAP.js → chunk-HSYDTVTD.js} +9 -9
  13. package/dist/chunk-HSYDTVTD.js.map +1 -0
  14. package/dist/{chunk-VLQLRKGW.js → chunk-MAYKC3RF.js} +260 -33
  15. package/dist/chunk-MAYKC3RF.js.map +1 -0
  16. package/dist/{chunk-H6VDFBIO.js → chunk-SMR2MJXB.js} +156 -5
  17. package/dist/chunk-SMR2MJXB.js.map +1 -0
  18. package/dist/{chunk-4LKAPRFM.js → chunk-UIODYJ6B.js} +2274 -176
  19. package/dist/chunk-UIODYJ6B.js.map +1 -0
  20. package/dist/{chunk-YBGRXVAE.js → chunk-X3C2VFTM.js} +3 -3
  21. package/dist/{chunk-CK6EV37I.js → chunk-ZULFHADW.js} +69 -6
  22. package/dist/chunk-ZULFHADW.js.map +1 -0
  23. package/dist/cli.js +6 -6
  24. package/dist/editor/index.d.ts +3 -3
  25. package/dist/editor/index.js +3 -3
  26. package/dist/{effect-GGP66JI6.js → effect-RGABT3BK.js} +10 -10
  27. package/dist/embed/index.d.ts +36 -8
  28. package/dist/embed/index.js +5 -5
  29. package/dist/{frames-VEcqa40g.d.ts → frames-CGJudAbu.d.ts} +11 -1
  30. package/dist/index.d.ts +69 -6
  31. package/dist/index.js +35 -7
  32. package/dist/index.js.map +1 -1
  33. package/dist/node.d.ts +4 -4
  34. package/dist/node.js +9 -9
  35. package/dist/react/index.d.ts +4 -4
  36. package/dist/react/index.js +7 -7
  37. package/dist/react/index.js.map +1 -1
  38. package/dist/{recorder-CDPPTJMG.js → recorder-TYMEZHOT.js} +3 -3
  39. package/dist/{renderer-teaa98RE.d.ts → renderer-tk1nbVkW.d.ts} +252 -2
  40. package/dist/svelte/index.d.ts +4 -4
  41. package/dist/svelte/index.js +7 -7
  42. package/dist/svelte/index.js.map +1 -1
  43. package/dist/{svg-BhRboN9q.d.ts → svg-DaKRPA9v.d.ts} +39 -7
  44. package/dist/{types-DUOMxZaq.d.ts → types-DuGVdJbt.d.ts} +2 -2
  45. package/dist/vector/index.d.ts +2 -2
  46. package/dist/vector/index.js +5 -3
  47. package/package.json +1 -1
  48. package/dist/chunk-4LKAPRFM.js.map +0 -1
  49. package/dist/chunk-CK6EV37I.js.map +0 -1
  50. package/dist/chunk-GXP4YLAP.js.map +0 -1
  51. package/dist/chunk-H6VDFBIO.js.map +0 -1
  52. package/dist/chunk-JL6TI6II.js.map +0 -1
  53. package/dist/chunk-PFGGPIMT.js.map +0 -1
  54. package/dist/chunk-RDU63CDO.js.map +0 -1
  55. package/dist/chunk-VLQLRKGW.js.map +0 -1
  56. /package/dist/{chunk-STKEKXCK.js.map → chunk-AUH3PEFW.js.map} +0 -0
  57. /package/dist/{chunk-YBGRXVAE.js.map → chunk-X3C2VFTM.js.map} +0 -0
  58. /package/dist/{effect-GGP66JI6.js.map → effect-RGABT3BK.js.map} +0 -0
  59. /package/dist/{recorder-CDPPTJMG.js.map → recorder-TYMEZHOT.js.map} +0 -0
@@ -168,6 +168,23 @@ declare const BUILTIN_UNIFORMS: readonly [{
168
168
  readonly type: "vec4";
169
169
  readonly size: 32;
170
170
  readonly doc: "where the pointer has just been over the canvas, newest first (about 30 points a second, so a second of path): xy = where (pixels), z = when (iTime seconds), w = 1 for a point, 0 for an empty slot";
171
+ }, {
172
+ readonly name: "iAudio";
173
+ readonly type: "vec4";
174
+ readonly doc: "the sound (see setAudioSource): x = level (loudness 0–1), y = bass, z = mid, w = treble (0–1, smoothed); all zero with no audio";
175
+ }, {
176
+ readonly name: "iSpectrum";
177
+ readonly type: "vec4";
178
+ readonly size: number;
179
+ readonly doc: "64 log-spaced frequency bins (about 30 Hz to 16 kHz), 0–1, four to a vec4, low to high: bin i is iSpectrum[i / 4][i % 4]";
180
+ }, {
181
+ readonly name: "iBeat";
182
+ readonly type: "float";
183
+ readonly doc: "seconds since the last beat (an onset on the bass), -1 before the first; 0 like every audio uniform when there is no audio at all";
184
+ }, {
185
+ readonly name: "iScroll";
186
+ readonly type: "vec4";
187
+ readonly doc: "scrolling: x = the canvas's progress through the viewport (0 as its top enters at the bottom, 1 as its bottom leaves at the top), y = page scroll (0–1), z = velocity (viewport heights per second, + down, smoothed), w = 1 while scrolling, easing to 0; all zero where nothing scrolls";
171
188
  }];
172
189
  /** A full-screen triangle from `gl_VertexID` alone: no buffers or attributes. */
173
190
  declare const VERTEX_SOURCE = "#version 300 es\nvoid main() {\n vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
@@ -209,7 +226,7 @@ declare function resetGlCaches(): void;
209
226
  * which uniform locations to look up.
210
227
  */
211
228
  declare function compileShader(gl: WebGL2RenderingContext, source: string, controls: ControlDef[]): CompileResult;
212
- /** What one frame shows: its size in pixels, its time and the pointer. */
229
+ /** What one frame shows: its size in pixels, its time, the pointer, the sound and the scroll. */
213
230
  interface FrameState {
214
231
  width: number;
215
232
  height: number;
@@ -227,6 +244,14 @@ interface FrameState {
227
244
  clicks?: Float32Array;
228
245
  /** `iTrail`: {@link MAX_TRAIL} × (x, y, time, 1), newest first (default none; see `trailUniform`). */
229
246
  trail?: Float32Array;
247
+ /** `iAudio`: level, bass, mid, treble (default all zero: no audio). */
248
+ audio?: readonly [number, number, number, number];
249
+ /** `iSpectrum`: {@link SPECTRUM_BINS} values, 0–1, low to high (default all zero). */
250
+ spectrum?: Float32Array;
251
+ /** `iBeat`: seconds since the last beat, -1 before the first (default 0, with no audio at all). */
252
+ beat?: number;
253
+ /** `iScroll`: progress, page, velocity, scrolling (default all zero: nothing scrolls it). */
254
+ scroll?: readonly [number, number, number, number];
230
255
  }
231
256
  /**
232
257
  * Draws one frame of a compiled shader into the bound framebuffer, from (0, 0).
@@ -244,6 +269,193 @@ declare function drawFrame(gl: WebGL2RenderingContext, shader: CompiledShader, c
244
269
  */
245
270
  declare function createContext(canvas: HTMLCanvasElement | OffscreenCanvas, preserveDrawingBuffer?: boolean, alpha?: boolean): WebGL2RenderingContext | null;
246
271
 
272
+ /**
273
+ * Sound as shaders see it: `iAudio` (level, bass, mid and treble, 0–1,
274
+ * smoothed), `iSpectrum[16]` (64 log-spaced frequency bins, 0–1, four to a
275
+ * `vec4`, low to high) and `iBeat` (seconds since the last beat on the bass,
276
+ * -1 before the first).
277
+ *
278
+ * {@link AudioAnalysis} turns frequency data (an `AnalyserNode`'s, or an
279
+ * FFT's) into those uniforms, deterministically for given data and times.
280
+ * {@link LiveAudio} feeds it from a playing media element, a microphone or
281
+ * any Web Audio node (what `ShaderRenderer.setAudioSource` uses), and
282
+ * {@link analyseAudioBuffer} from a decoded `AudioBuffer` offline, frame-exact
283
+ * for recordings.
284
+ *
285
+ * With no audio at all every audio uniform is zero, `iBeat` included, so an
286
+ * audio-reactive effect can tell silence from "nothing connected" (where
287
+ * `iBeat` is -1 or above 0) and play an idle demo instead.
288
+ */
289
+ /** How many frequency bins `iSpectrum` holds (16 `vec4`s). */
290
+ declare const SPECTRUM_BINS = 64;
291
+ /** One frame of the audio uniforms. */
292
+ interface AudioFrame {
293
+ /** `iAudio`: level (loudness, 0–1), bass, mid, treble (0–1), smoothed. */
294
+ audio: readonly [number, number, number, number];
295
+ /** `iSpectrum`: {@link SPECTRUM_BINS} values, 0–1, low to high (log-spaced). */
296
+ spectrum: Float32Array;
297
+ /** `iBeat`: seconds since the last beat, or -1 before the first. */
298
+ beat: number;
299
+ }
300
+ interface AudioAnalysisOptions {
301
+ /** The sample rate of the audio analysed, in Hz (default 48000). */
302
+ sampleRate?: number;
303
+ /** The FFT size the frequency data came from (default 2048: `fftSize / 2` values). */
304
+ fftSize?: number;
305
+ /** Decibels that read as 0 and 1 for float (dB) frequency data (default -100 and -30, as `AnalyserNode`). */
306
+ minDecibels?: number;
307
+ maxDecibels?: number;
308
+ /** Seconds a value takes to rise most of the way to a louder reading (default 0.03). */
309
+ attack?: number;
310
+ /** Seconds a value takes to fall most of the way to a quieter one (default 0.25). */
311
+ release?: number;
312
+ /** The lowest and highest frequencies the spectrum covers, in Hz (default 30 and 16000). */
313
+ minHz?: number;
314
+ maxHz?: number;
315
+ /**
316
+ * How far the bass must jump over its recent average to count as a beat,
317
+ * 0–1 (default 0.12). Lower finds more beats.
318
+ */
319
+ beatThreshold?: number;
320
+ /** The least time between two beats, in seconds (default 0.22: about 270 beats a minute). */
321
+ beatGap?: number;
322
+ }
323
+ /** The bands `iAudio` averages, in Hz. */
324
+ declare const AUDIO_BANDS: {
325
+ readonly bass: readonly [20, 250];
326
+ readonly mid: readonly [250, 2000];
327
+ readonly treble: readonly [2000, 16000];
328
+ };
329
+ /**
330
+ * Turns frequency data into the audio uniforms: 64 log-spaced bins, band
331
+ * averages, attack and release smoothing, a loudness level, and simple beat
332
+ * detection (onsets on the bass band, over its recent average). Feed it
333
+ * frequency data with the time it was read, in seconds, increasing; read
334
+ * {@link AudioAnalysis.frame} for the uniforms. The same data at the same
335
+ * times always gives the same uniforms.
336
+ *
337
+ * ```ts
338
+ * const analysis = new AudioAnalysis({ sampleRate: ctx.sampleRate, fftSize: analyser.fftSize });
339
+ * analyser.getFloatFrequencyData(freq);
340
+ * analysis.update(freq, performance.now() / 1000);
341
+ * const { audio, spectrum, beat } = analysis.frame(performance.now() / 1000);
342
+ * ```
343
+ */
344
+ declare class AudioAnalysis {
345
+ readonly sampleRate: number;
346
+ readonly fftSize: number;
347
+ private readonly minDb;
348
+ private readonly maxDb;
349
+ private readonly attack;
350
+ private readonly release;
351
+ private readonly beatThreshold;
352
+ private readonly beatGap;
353
+ /** Each spectrum bin's range of FFT bins, [from, to), fractional. */
354
+ private readonly ranges;
355
+ private readonly bands;
356
+ private readonly raw;
357
+ private readonly smoothed;
358
+ private values;
359
+ private last;
360
+ private bassAverage;
361
+ private armed;
362
+ private lastBeat;
363
+ private normalised;
364
+ constructor(options?: AudioAnalysisOptions);
365
+ /** Forgets everything heard: the next update starts afresh. */
366
+ reset(): void;
367
+ /**
368
+ * Takes one reading at `time` seconds: `frequency` is `fftSize / 2`
369
+ * values, bytes 0–255 (`getByteFrequencyData`) or decibels
370
+ * (`getFloatFrequencyData`). `waveform` (time-domain samples, -1–1, or
371
+ * bytes centred on 128) gives the level its RMS; without it the level
372
+ * comes from the spectrum.
373
+ */
374
+ update(frequency: Uint8Array | Float32Array, time: number, waveform?: Uint8Array | Float32Array): void;
375
+ /** The uniforms after the latest update, with `iBeat` measured to `time` (default the latest update's). */
376
+ frame(time?: number): AudioFrame;
377
+ }
378
+ /** Loudness from an RMS amplitude: 0 at -60 dBFS (or below), 1 at full scale. */
379
+ declare function rmsLevel(rms: number): number;
380
+ /**
381
+ * An in-place radix-2 FFT of `re` and `im` (their length a power of two).
382
+ * Forward, unscaled: `X[k] = Σ x[n] e^(-2πikn/N)`.
383
+ */
384
+ declare function fft(re: Float32Array | Float64Array, im: Float32Array | Float64Array): void;
385
+ /**
386
+ * The decibel spectrum of the `fftSize` samples of `samples` ending at
387
+ * `end` (exclusive; samples before the start are silence), as
388
+ * `AnalyserNode.getFloatFrequencyData` gives it with no smoothing: a
389
+ * Blackman window, an FFT, magnitudes over `fftSize`, in dB. Writes
390
+ * `fftSize / 2` values into `out`, and the window's samples into `waveform`
391
+ * when given.
392
+ */
393
+ declare function frequencyData(samples: Float32Array, end: number, fftSize: number, out: Float32Array, waveform?: Float32Array): void;
394
+ /** What {@link analyseAudioBuffer} reads: an `AudioBuffer`, or anything shaped like one. */
395
+ interface AudioSamples {
396
+ sampleRate: number;
397
+ length: number;
398
+ numberOfChannels: number;
399
+ getChannelData(channel: number): Float32Array;
400
+ }
401
+ interface AudioBufferAnalysisOptions extends Omit<AudioAnalysisOptions, "sampleRate"> {
402
+ /** Readings per second (default 120). Recordings sample the reading at or just before each frame. */
403
+ rate?: number;
404
+ }
405
+ /** The audio uniforms of a whole decoded track, at any time (see {@link analyseAudioBuffer}). */
406
+ interface AudioBufferAnalysis {
407
+ /** The track's length in seconds. */
408
+ readonly duration: number;
409
+ /** The uniforms at `time` seconds into the track (silence before 0 and after the end). */
410
+ at(time: number): AudioFrame;
411
+ }
412
+ /**
413
+ * Analyses a decoded track offline, so a recording of an audio-reactive
414
+ * effect follows the sound frame for frame: the uniforms at time `t` come
415
+ * from an FFT of the samples just before `t`, as a live `AnalyserNode`
416
+ * would hear them, through an {@link AudioAnalysis} stepped at a fixed
417
+ * `rate` from the start of the track. Readings are worked out as far as
418
+ * asked and kept, so asking for frames in any order gives the same answer.
419
+ *
420
+ * ```ts
421
+ * const track = analyseAudioBuffer(await ctx.decodeAudioData(bytes));
422
+ * frames.render(t, { audio: track.at(t) });
423
+ * ```
424
+ */
425
+ declare function analyseAudioBuffer(buffer: AudioSamples, options?: AudioBufferAnalysisOptions): AudioBufferAnalysis;
426
+ /** What `ShaderRenderer.setAudioSource` listens to: a media element (it keeps playing to the speakers), a stream (a microphone: not played) or any Web Audio node. */
427
+ type AudioSource = HTMLMediaElement | MediaStream | AudioNode;
428
+ /**
429
+ * The `AudioContext` Flair listens with, made on first use and shared by
430
+ * every renderer (browsers limit how many a page may have). Make nodes in it
431
+ * to pass to `setAudioSource` (an oscillator, a decoded buffer). Browsers
432
+ * start it suspended until the page has had a click or key press: Flair
433
+ * resumes it on the first one, or call `resume()` from your own gesture.
434
+ * Closing it is allowed: the next use makes a new one.
435
+ */
436
+ declare function sharedAudioContext(): AudioContext;
437
+ /**
438
+ * Listens to a live {@link AudioSource} through an `AnalyserNode` (in the
439
+ * shared context, or the node's own) and reads the audio uniforms from it.
440
+ * A media element keeps playing to the speakers through Web Audio; a stream
441
+ * (a microphone) is only listened to. A media element from another site
442
+ * needs `crossOrigin = "anonymous"` and CORS, or the browser feeds silence.
443
+ */
444
+ declare class LiveAudio {
445
+ readonly context: BaseAudioContext;
446
+ readonly analyser: AnalyserNode;
447
+ private readonly analysis;
448
+ private readonly input;
449
+ private readonly ownsInput;
450
+ private readonly freq;
451
+ private readonly wave;
452
+ constructor(source: AudioSource, options?: AudioAnalysisOptions);
453
+ /** Reads the analyser now and returns the uniforms, `time` in seconds (a steady clock, such as `performance.now() / 1000`). */
454
+ read(time: number): AudioFrame;
455
+ /** Stops listening. A media element goes on playing to the speakers. */
456
+ dispose(): void;
457
+ }
458
+
247
459
  /** Whether the latest source compiled, and if not, why. */
248
460
  interface CompileStatus {
249
461
  ok: boolean;
@@ -304,6 +516,17 @@ declare class ShaderRenderer {
304
516
  private mouse;
305
517
  /** `iPointer` and `iClicks`. */
306
518
  private pointer;
519
+ /** `iAudio`, `iSpectrum` and `iBeat`, from `setAudioSource`. */
520
+ private audio;
521
+ private audioFrame;
522
+ /** `iScroll`. */
523
+ private scrollTracker;
524
+ private scrollOverride;
525
+ private scrollMoved;
526
+ /** The page clock's latest reading, in seconds, for `iScroll`'s easing. */
527
+ private scrollNow;
528
+ /** `iScroll` as last drawn, so a paused preview redraws only when it changes. */
529
+ private drawnScroll;
307
530
  private disposed;
308
531
  /** The latest source's status. */
309
532
  status: CompileStatus;
@@ -346,6 +569,30 @@ declare class ShaderRenderer {
346
569
  * agents keep seeing the URL.
347
570
  */
348
571
  setLiveSource(name: string, source: LiveSource | null): void;
572
+ /**
573
+ * Feeds `iAudio`, `iSpectrum` and `iBeat` from a sound (`null` stops):
574
+ * - an `<audio>` or `<video>` element: it keeps playing to the speakers,
575
+ * now through Web Audio (an element can only be wired in once, so the
576
+ * same node is reused by every renderer that listens to it);
577
+ * - a `MediaStream`, such as a microphone from `getUserMedia`: listened
578
+ * to, not played;
579
+ * - any Web Audio node (an oscillator, a gain, a player's output), in its
580
+ * own context.
581
+ *
582
+ * The `AudioContext` and `AnalyserNode` are made on first use (see
583
+ * `sharedAudioContext`). Browsers keep a new context suspended until the
584
+ * page has a click or key press (autoplay policy): call this from one, or
585
+ * the context resumes on the first. While a source is set, a paused
586
+ * preview still follows the sound. A media element from another site needs
587
+ * `crossOrigin = "anonymous"` and CORS, or the browser feeds silence.
588
+ */
589
+ setAudioSource(source: AudioSource | null): void;
590
+ /**
591
+ * Pins `iScroll` to a scroll position of your own (0–1, for x and y; the
592
+ * velocity and "scrolling" follow its changes), for scrubbers and hosts
593
+ * that scroll in their own way; `null` goes back to following the page.
594
+ */
595
+ setScrollProgress(progress: number | null): void;
349
596
  /**
350
597
  * Settles once the images the current controls show have loaded, and
351
598
  * every live video has a frame. Rejects with an {@link ImageLoadError}
@@ -379,6 +626,9 @@ declare class ShaderRenderer {
379
626
  private syncTextures;
380
627
  private tick;
381
628
  private emit;
629
+ /** Where the canvas is in the viewport (or the pinned progress), into the scroll tracker. */
630
+ private sampleScroll;
631
+ private onScroll;
382
632
  /** Pointer position in shader pixels (origin bottom-left, like gl_FragCoord). */
383
633
  private toPixels;
384
634
  private onPointerDown;
@@ -390,4 +640,4 @@ declare class ShaderRenderer {
390
640
  private onRestored;
391
641
  }
392
642
 
393
- export { BUILTIN_UNIFORMS as B, type CompileError as C, type FrameState as F, ImageLoadError as I, type LiveSource as L, type RendererStats as R, ShaderRenderer as S, TextureCache as T, VERTEX_SOURCE as V, type CompileStatus as a, type CompileResult as b, type CompiledShader as c, SVG_EDGE as d, ShaderCompileError as e, type ShaderRendererOptions as f, type TextureSource as g, applyTextureSettings as h, compileShader as i, createContext as j, drawFrame as k, formatCompileError as l, isSvgUrl as m, loadImage as n, preloadImages as o, parseCompileLog as p, resetGlCaches as q, releaseImages as r, seekVideo as s, sizeSvg as t, wrapSource as w };
643
+ export { type AudioSource as A, BUILTIN_UNIFORMS as B, type CompileError as C, parseCompileLog as D, preloadImages as E, type FrameState as F, releaseImages as G, resetGlCaches as H, ImageLoadError as I, rmsLevel as J, seekVideo as K, type LiveSource as L, sharedAudioContext as M, sizeSvg as N, wrapSource as O, type RendererStats as R, ShaderRenderer as S, TextureCache as T, VERTEX_SOURCE as V, type AudioFrame as a, type CompileStatus as b, AUDIO_BANDS as c, AudioAnalysis as d, type AudioAnalysisOptions as e, type AudioBufferAnalysis as f, type AudioBufferAnalysisOptions as g, type AudioSamples as h, type CompileResult as i, type CompiledShader as j, LiveAudio as k, SPECTRUM_BINS as l, SVG_EDGE as m, ShaderCompileError as n, type ShaderRendererOptions as o, type TextureSource as p, analyseAudioBuffer as q, applyTextureSettings as r, compileShader as s, createContext as t, drawFrame as u, fft as v, formatCompileError as w, frequencyData as x, isSvgUrl as y, loadImage as z };
@@ -1,10 +1,10 @@
1
1
  import { ActionReturn } from 'svelte/action';
2
2
  import { C as ControlValue } from '../controls-Ec6P0Vpy.js';
3
- import { a as SvgTrigger, C as Clock, b as SvgPlayer, S as SvgMotion } from '../svg-BhRboN9q.js';
4
- import { E as EffectDefinition, V as VectorEffect } from '../types-DUOMxZaq.js';
3
+ import { a as SvgTrigger, C as Clock, b as SvgPlayer, S as SvgMotion } from '../svg-DaKRPA9v.js';
4
+ import { E as EffectDefinition, V as VectorEffect } from '../types-DuGVdJbt.js';
5
5
  import { EffectSubject } from '../embed/index.js';
6
- import '../renderer-teaa98RE.js';
7
- import '../frames-VEcqa40g.js';
6
+ import '../renderer-tk1nbVkW.js';
7
+ import '../frames-CGJudAbu.js';
8
8
 
9
9
  /**
10
10
  * Svelte 5 bindings for Flair, written as plain TypeScript so they need no
@@ -2,22 +2,22 @@ import {
2
2
  svgFrameMarkup,
3
3
  svgInlineLayout,
4
4
  svgViewBox
5
- } from "../chunk-STKEKXCK.js";
5
+ } from "../chunk-AUH3PEFW.js";
6
6
  import {
7
7
  mountEffect
8
- } from "../chunk-VLQLRKGW.js";
9
- import "../chunk-H6VDFBIO.js";
10
- import "../chunk-RDU63CDO.js";
8
+ } from "../chunk-MAYKC3RF.js";
9
+ import "../chunk-SMR2MJXB.js";
10
+ import "../chunk-HDYBY3PQ.js";
11
11
  import {
12
12
  effectDefaults,
13
13
  getEffect,
14
14
  renderVectorEffect
15
- } from "../chunk-4LKAPRFM.js";
15
+ } from "../chunk-UIODYJ6B.js";
16
16
  import "../chunk-EHDJ3CPJ.js";
17
17
  import "../chunk-VYOMXE6I.js";
18
18
  import {
19
19
  SvgPlayer
20
- } from "../chunk-PFGGPIMT.js";
20
+ } from "../chunk-DA4UUSDO.js";
21
21
  import "../chunk-WMZQCUFJ.js";
22
22
  import "../chunk-ORAFCHX3.js";
23
23
 
@@ -62,7 +62,7 @@ function hostStyle(svg, inline, contain) {
62
62
  }
63
63
  var enters = (r, p) => p.playOnEnter ?? (p.loop === false && !r.trigger);
64
64
  function stillMarkup(r, params) {
65
- const at = params.poster ?? (r.trigger === "hover" || r.trigger === "press" ? "first" : "last");
65
+ const at = params.poster ?? (r.trigger === "hover" || r.trigger === "press" || r.trigger === "scroll" ? "first" : "last");
66
66
  return svgFrameMarkup(r.motion, { at, steps: params.steps }) ?? "";
67
67
  }
68
68
  function vectorEffectStill(params) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/svelte/index.ts"],"sourcesContent":["/**\n * Svelte 5 bindings for Flair, written as plain TypeScript so they need no\n * Svelte compiler: the `flairEffect` action runs any catalog effect (shader or\n * vector) applied to a picture, video, SVG or words; the `vectorEffect` action plays a catalog\n * effect (or any {@link SvgMotion}) in an element, and `vectorEffectStill`\n * gives the markup and style of its still for server rendering.\n *\n * ```svelte\n * <script>\n * import { vectorEffect, vectorEffectStill } from \"@pixerate/flair/svelte\";\n * const params = { id: \"drip-text\", values: { word: \"Slop\" }, loop: false };\n * const still = vectorEffectStill(params);\n * </script>\n *\n * <span use:vectorEffect={params} style={still.style}>{@html still.markup}</span>\n * ```\n *\n * The still is in place from the first paint; the live `SvgPlayer` takes over\n * after mount and is disposed on destroy. Under `prefers-reduced-motion:\n * reduce` the still stays.\n *\n * Needs Svelte 5 (an optional peer dependency of the package); only its types\n * are imported.\n *\n * @packageDocumentation\n */\n\nimport type { ActionReturn } from \"svelte/action\";\nimport { getEffect, effectDefaults, type EffectDefinition } from \"../catalog/index.js\";\nimport { mountEffect, type EffectSubject } from \"../embed/index.js\";\nimport { renderVectorEffect, type VectorEffect as VectorEffectDefinition } from \"../catalog/types.js\";\nimport type { ControlValue } from \"../shader/controls.js\";\nimport type { Clock } from \"../time/clock.js\";\nimport type { SvgMotion } from \"../vector/motion.js\";\nimport { svgFrameMarkup, svgInlineLayout, svgViewBox } from \"../vector/static.js\";\nimport { SvgPlayer, type SvgTrigger } from \"../vector/svg.js\";\n\n/** Cap height of a heavy sans, as a share of its font size: what an inline word is framed to. */\nconst CAP_HEIGHT = \"0.72em\";\n\n/** Marks the still's layer, so the action can find it (and hide it while the player runs). */\nconst STILL_ATTR = \"data-flair-still\";\n\nconst LAYER = \"position:absolute;inset:0;display:block\";\n\n/** When to leave the still in place of the animation. */\nexport type ReducedMotion = \"user\" | \"always\" | \"never\";\n\n/** Options shared by an effect by id and a motion of your own. */\nexport interface SvgPlayerParams {\n /** Start again at the end (default `true`); `false` plays once and stays on the last frame. */\n loop?: boolean;\n /** Start playing at once (default `true`). */\n autoplay?: boolean;\n /** Playback speed (default 1); changed without remaking the player. */\n speed?: number;\n /** Ms to rest on the last frame before looping (default 600, or the effect's). */\n hold?: number;\n /** Loop length in ms (default: the picture's own, plus `hold`). */\n duration?: number;\n /** Step start times in ms, for pictures that change on steps. */\n steps?: readonly number[];\n /** Plays in answer to the pointer (see `SvgTrigger`; default the effect's). */\n trigger?: SvgTrigger;\n /** Pause while scrolled out of view. */\n pauseWhenHidden?: boolean;\n /**\n * Wait to start until scrolled into view. Default `true` when it plays once\n * (`loop: false`, no trigger), so a one-shot further down the page does not\n * finish before anyone sees it; `false` plays at mount.\n */\n playOnEnter?: boolean;\n /** Hide the picture while paused or reset. */\n hideWhenPaused?: boolean;\n /** Time source (default animation frames); a `ManualClock` steps frames yourself. */\n clock?: Clock;\n /** Called with the id of the innermost element with an id that is clicked. */\n onPick?: (id: string) => void;\n /** Called with the player once it is made, and `undefined` when it is disposed. */\n onPlayer?: (player: SvgPlayer | undefined) => void;\n /**\n * The still shown before the player starts and instead of it under reduced\n * motion: `\"last\"` (default; the rest frame, `\"first\"` for a hover or press\n * trigger), `\"first\"` or a time in ms.\n */\n poster?: \"first\" | \"last\" | number;\n /**\n * `\"user\"` (default) shows the still when the visitor prefers reduced\n * motion, `\"always\"` always does, `\"never\"` always plays.\n */\n reducedMotion?: ReducedMotion;\n /**\n * Size it as a word in a line of text: `display: inline-block` with the\n * viewBox's aspect ratio and `height` 0.72em (the cap height of a heavy\n * sans; pass a CSS length for another). Default: the effect's `inline`\n * control. Otherwise it is a block as wide as its container with the\n * viewBox's aspect ratio.\n */\n inline?: boolean | string;\n /**\n * With `inline`, keep room for what spills past the word (descenders,\n * outlines, glows, drips): a margin of the picture's `data-overhang` (see\n * `svgInlineLayout`), so the lines around it make space instead of being\n * painted over. Default `false`.\n */\n contain?: boolean;\n}\n\n/** A catalog vector effect by id (or the effect itself), with its values. */\nexport interface VectorEffectByIdParams extends SvgPlayerParams {\n /** A vector effect's catalog id (`\"drip-text\"`), or the effect itself. */\n id: string | VectorEffectDefinition;\n /** Control values; missing ones take the preset's, then the defaults. Compared by content. */\n values?: Record<string, ControlValue>;\n /** A preset's name to start from. */\n preset?: string;\n motion?: never;\n}\n\n/** Any {@link SvgMotion}; a new object restarts the player. */\nexport interface SvgMotionParams extends SvgPlayerParams {\n /** The picture and its animations. */\n motion: SvgMotion;\n id?: never;\n values?: never;\n preset?: never;\n}\n\n/** What `vectorEffect` and `vectorEffectStill` take: an effect by `id`, or a `motion`. */\nexport type VectorEffectParams = VectorEffectByIdParams | SvgMotionParams;\n\n/** A still of the picture, for `{@html markup}` inside an element with `style`. */\nexport interface VectorEffectStill {\n /** The still's layer: safe `<svg>` markup (see `svgFrameMarkup`) in an absolutely placed `<span>`. */\n markup: string;\n /** The host's inline style: its position and size from the viewBox. */\n style: string;\n}\n\ninterface Resolved {\n motion: SvgMotion;\n hold?: number;\n trigger?: SvgTrigger;\n inline?: boolean | string;\n contain?: boolean;\n}\n\n/** Remembers the last motion made from an effect, so values compared by content do not remake it. */\ntype MotionCache = { key: string; effect: VectorEffectDefinition; motion: SvgMotion };\n\nfunction resolve(params: VectorEffectParams, cache?: { current?: MotionCache }): Resolved {\n if (params.motion) return { motion: params.motion, hold: params.hold, trigger: params.trigger, inline: params.inline, contain: params.contain };\n const { id, values, preset } = params as VectorEffectByIdParams;\n const effect = typeof id === \"string\" ? getEffect(id) : id;\n if (!effect || effect.kind !== \"vector\")\n throw new Error(\n `\"${typeof id === \"string\" ? id : (effect?.id ?? id)}\" is not a vector effect; pick one from findEffects({ kind: \"vector\" }), or pass a motion`,\n );\n const key = JSON.stringify([preset ?? null, values ?? {}]);\n let motion: SvgMotion;\n if (cache?.current && cache.current.effect === effect && cache.current.key === key) motion = cache.current.motion;\n else {\n motion = renderVectorEffect(effect, effectDefaults(effect, { preset, values }));\n if (cache) cache.current = { key, effect, motion };\n }\n return {\n motion,\n hold: params.hold ?? effect.hold,\n trigger: params.trigger ?? effect.trigger,\n inline: params.inline ?? values?.inline === true,\n contain: params.contain,\n };\n}\n\n/** The host's sizing from the picture's viewBox, so the still and the player take the same room. */\nfunction hostStyle(svg: string, inline: boolean | string | undefined, contain?: boolean): [string, string][] {\n const box = svgViewBox(svg);\n const style: [string, string][] = [\n [\"position\", \"relative\"],\n [\"display\", inline ? \"inline-block\" : \"block\"],\n ];\n if (inline) style.push([\"height\", typeof inline === \"string\" ? inline : CAP_HEIGHT]);\n if (box) style.push([\"aspect-ratio\", `${box[2]} / ${box[3]}`]);\n const spill = inline && contain ? svgInlineLayout(svg)?.overhang : undefined;\n if (spill) style.push([\"margin\", `${spill.top}em ${spill.right}em ${spill.bottom}em ${spill.left}em`]);\n return style;\n}\n\n/** Whether to wait until scrolled into view: as asked, else when it plays once. */\nconst enters = (r: Resolved, p: SvgPlayerParams) => p.playOnEnter ?? (p.loop === false && !r.trigger);\n\nfunction stillMarkup(r: Resolved, params: SvgPlayerParams): string {\n const at = params.poster ?? (r.trigger === \"hover\" || r.trigger === \"press\" ? \"first\" : \"last\");\n return svgFrameMarkup(r.motion, { at, steps: params.steps }) ?? \"\";\n}\n\n/**\n * The still of an effect or motion, for server rendering: put `markup` in the\n * element that has the `vectorEffect` action, with `style` as its style, so\n * the page shows the picture before the player starts (and instead of it\n * under reduced motion), in the room the player will take.\n * Throws when `id` is not a vector effect.\n */\nexport function vectorEffectStill(params: VectorEffectParams): VectorEffectStill {\n const r = resolve(params);\n return {\n markup: `<span ${STILL_ATTR} aria-hidden=\"true\" style=\"${LAYER}\">${stillMarkup(r, params)}</span>`,\n style: hostStyle(r.motion.svg, r.inline, r.contain)\n .map(([k, v]) => `${k}: ${v}`)\n .join(\"; \"),\n };\n}\n\nfunction prefersReducedMotion(): boolean {\n return typeof matchMedia === \"function\" && matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/** The options whose change remakes the player (`speed` is set on it instead). */\nfunction playerKey(r: Resolved, p: SvgPlayerParams): unknown[] {\n return [\n r.motion,\n p.loop,\n p.autoplay,\n r.hold,\n p.duration,\n p.steps?.join(\",\"),\n r.trigger,\n p.pauseWhenHidden,\n enters(r, p),\n p.hideWhenPaused,\n p.clock,\n p.reducedMotion ?? \"user\",\n ];\n}\n\n/**\n * A Svelte action that plays a vector effect in its element:\n * `<span use:vectorEffect={{ id: \"drip-text\", values: { word: \"Slop\" }, loop: false }} />`.\n *\n * It makes an `SvgPlayer` in a layer of its own, remakes it when the params\n * change (values by content; `speed` without remaking) and disposes it on\n * destroy. A still from {@link vectorEffectStill} in the element is hidden\n * while the player runs and shown again after; without one, the action makes\n * its own (so reduced motion still shows the picture) and sizes the element\n * from the viewBox, leaving alone any of `position`, `display`, `height`,\n * `aspect-ratio` and `margin` the element's style already sets.\n *\n * For an attachment, `{@attach fromAction(vectorEffect, () => params)}`\n * (`fromAction` is from `svelte/attachments`). Throws when `id` is not a\n * vector effect.\n */\nexport function vectorEffect(node: HTMLElement, initial: VectorEffectParams): ActionReturn<VectorEffectParams> {\n const cache: { current?: MotionCache } = {};\n const doc = node.ownerDocument;\n const mount = doc.createElement(\"span\");\n mount.style.cssText = LAYER;\n node.appendChild(mount);\n\n let params = initial;\n let resolved = resolve(params, cache);\n let player: SvgPlayer | undefined;\n let key: unknown[] = [];\n /** The still the action made itself, when the element had none. */\n let ownStill: HTMLElement | undefined;\n /** Sizing properties the action set (the element's own style had them empty). */\n const sized = new Set<string>();\n\n const stills = () => node.querySelectorAll<HTMLElement>(`:scope > [${STILL_ATTR}]`);\n const showStills = (show: boolean) => {\n for (const s of stills()) s.style.display = show ? \"block\" : \"none\";\n };\n // When the params change Svelte replaces a server still (`{@html}`), and when\n // it is the element's only child, by setting `innerHTML`, which takes the\n // player's layer out too: put the layer back, and keep a new still hidden.\n const reattach = () => {\n if (mount.parentNode !== node) node.appendChild(mount);\n if (player) showStills(false);\n };\n const observer = typeof MutationObserver === \"function\" ? new MutationObserver(reattach) : undefined;\n observer?.observe(node, { childList: true });\n\n const size = () => {\n const style = hostStyle(resolved.motion.svg, resolved.inline, resolved.contain);\n for (const [prop, value] of style) {\n if (!sized.has(prop) && node.style.getPropertyValue(prop)) continue;\n node.style.setProperty(prop, value);\n sized.add(prop);\n }\n // A property the new sizing drops (`height` when no longer inline) is cleared.\n for (const prop of sized)\n if (!style.some(([p]) => p === prop)) {\n node.style.removeProperty(prop);\n sized.delete(prop);\n }\n };\n\n if (stills().length === 0) {\n ownStill = doc.createElement(\"span\");\n ownStill.setAttribute(STILL_ATTR, \"\");\n ownStill.setAttribute(\"aria-hidden\", \"true\");\n ownStill.style.cssText = LAYER;\n node.insertBefore(ownStill, mount);\n size();\n }\n\n const stop = () => {\n if (!player) return;\n player.dispose();\n player = undefined;\n showStills(true);\n params.onPlayer?.(undefined);\n };\n\n const start = () => {\n const reduced = params.reducedMotion ?? \"user\";\n if (reduced === \"always\" || (reduced === \"user\" && prefersReducedMotion())) return;\n player = new SvgPlayer(mount, resolved.motion, {\n loop: params.loop,\n autoplay: params.autoplay,\n speed: params.speed,\n hold: resolved.hold,\n duration: params.duration,\n steps: params.steps,\n trigger: resolved.trigger,\n pauseWhenHidden: params.pauseWhenHidden,\n playOnEnter: enters(resolved, params),\n hideWhenPaused: params.hideWhenPaused,\n clock: params.clock,\n // The latest params' callback, without remaking the player.\n onPick: (id) => params.onPick?.(id),\n });\n showStills(false);\n params.onPlayer?.(player);\n };\n\n const apply = () => {\n reattach();\n if (ownStill) {\n ownStill.innerHTML = stillMarkup(resolved, params);\n size();\n }\n const next = playerKey(resolved, params);\n if (player && next.every((v, i) => v === key[i])) {\n player.setSpeed(params.speed ?? 1);\n return;\n }\n key = next;\n stop();\n start();\n };\n\n apply();\n\n return {\n update(next) {\n params = next;\n resolved = resolve(params, cache);\n apply();\n },\n destroy() {\n observer?.disconnect();\n stop();\n mount.remove();\n ownStill?.remove();\n for (const prop of sized) node.style.removeProperty(prop);\n },\n };\n}\n\nexport type { SvgPlayer, SvgMotion, SvgTrigger };\n\n/** What the `flairEffect` action runs: any catalog effect, applied to a subject. */\nexport interface FlairEffectParams {\n /** A catalog effect, by id or definition: shader or vector. */\n effect: string | EffectDefinition;\n /** What to apply it to: a picture, a video, an SVG, words or a path (see `EffectSubject`). */\n subject?: EffectSubject | readonly EffectSubject[];\n preset?: string;\n values?: Record<string, ControlValue>;\n /** Start playing at once (default `true`). */\n autoplay?: boolean;\n /** Pause while scrolled out of view (default `true`). */\n pauseWhenHidden?: boolean;\n}\n\n/**\n * Runs any catalog effect, shader or vector, in an element, applied to a\n * subject (see `mountEffect` in `@pixerate/flair/embed`); it fills the\n * element, so size it with CSS. A new subject or values update the running\n * effect; a new effect, preset or playback option starts it again.\n *\n * ```svelte\n * <div use:flairEffect={{ effect: \"image-duotone\", subject: { image: \"/team.jpg\" } }} style=\"aspect-ratio: 16 / 9\"></div>\n * ```\n */\nexport function flairEffect(node: HTMLElement, initial: FlairEffectParams): ActionReturn<FlairEffectParams> {\n let params = initial;\n if (!node.style.position) node.style.position = \"relative\";\n const start = () => {\n const m = mountEffect(node, params.effect, {\n ...(params.subject ? { subject: params.subject } : {}),\n ...(params.preset ? { preset: params.preset } : {}),\n ...(params.values ? { values: params.values } : {}),\n autoplay: params.autoplay ?? true,\n pauseWhenHidden: params.pauseWhenHidden ?? true,\n });\n m.element.style.display = \"block\";\n m.element.style.width = \"100%\";\n m.element.style.height = \"100%\";\n return m;\n };\n let mounted = start();\n return {\n update(next) {\n const restart =\n next.effect !== params.effect || next.preset !== params.preset || next.autoplay !== params.autoplay || next.pauseWhenHidden !== params.pauseWhenHidden;\n const subjectChanged = next.subject !== params.subject;\n const valuesChanged = JSON.stringify(next.values ?? {}) !== JSON.stringify(params.values ?? {});\n params = next;\n if (restart) {\n mounted.dispose();\n mounted = start();\n return;\n }\n if (subjectChanged && next.subject) mounted.setSubject(next.subject);\n if (valuesChanged && next.values) mounted.setValues(next.values);\n },\n destroy() {\n mounted.dispose();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAsCA,IAAM,aAAa;AAGnB,IAAM,aAAa;AAEnB,IAAM,QAAQ;AA2Gd,SAAS,QAAQ,QAA4B,OAA6C;AACxF,MAAI,OAAO,OAAQ,QAAO,EAAE,QAAQ,OAAO,QAAQ,MAAM,OAAO,MAAM,SAAS,OAAO,SAAS,QAAQ,OAAO,QAAQ,SAAS,OAAO,QAAQ;AAC9I,QAAM,EAAE,IAAI,QAAQ,OAAO,IAAI;AAC/B,QAAM,SAAS,OAAO,OAAO,WAAW,UAAU,EAAE,IAAI;AACxD,MAAI,CAAC,UAAU,OAAO,SAAS;AAC7B,UAAM,IAAI;AAAA,MACR,IAAI,OAAO,OAAO,WAAW,KAAM,QAAQ,MAAM,EAAG;AAAA,IACtD;AACF,QAAM,MAAM,KAAK,UAAU,CAAC,UAAU,MAAM,UAAU,CAAC,CAAC,CAAC;AACzD,MAAI;AACJ,MAAI,OAAO,WAAW,MAAM,QAAQ,WAAW,UAAU,MAAM,QAAQ,QAAQ,IAAK,UAAS,MAAM,QAAQ;AAAA,OACtG;AACH,aAAS,mBAAmB,QAAQ,eAAe,QAAQ,EAAE,QAAQ,OAAO,CAAC,CAAC;AAC9E,QAAI,MAAO,OAAM,UAAU,EAAE,KAAK,QAAQ,OAAO;AAAA,EACnD;AACA,SAAO;AAAA,IACL;AAAA,IACA,MAAM,OAAO,QAAQ,OAAO;AAAA,IAC5B,SAAS,OAAO,WAAW,OAAO;AAAA,IAClC,QAAQ,OAAO,UAAU,QAAQ,WAAW;AAAA,IAC5C,SAAS,OAAO;AAAA,EAClB;AACF;AAGA,SAAS,UAAU,KAAa,QAAsC,SAAuC;AAC3G,QAAM,MAAM,WAAW,GAAG;AAC1B,QAAM,QAA4B;AAAA,IAChC,CAAC,YAAY,UAAU;AAAA,IACvB,CAAC,WAAW,SAAS,iBAAiB,OAAO;AAAA,EAC/C;AACA,MAAI,OAAQ,OAAM,KAAK,CAAC,UAAU,OAAO,WAAW,WAAW,SAAS,UAAU,CAAC;AACnF,MAAI,IAAK,OAAM,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC;AAC7D,QAAM,QAAQ,UAAU,UAAU,gBAAgB,GAAG,GAAG,WAAW;AACnE,MAAI,MAAO,OAAM,KAAK,CAAC,UAAU,GAAG,MAAM,GAAG,MAAM,MAAM,KAAK,MAAM,MAAM,MAAM,MAAM,MAAM,IAAI,IAAI,CAAC;AACrG,SAAO;AACT;AAGA,IAAM,SAAS,CAAC,GAAa,MAAuB,EAAE,gBAAgB,EAAE,SAAS,SAAS,CAAC,EAAE;AAE7F,SAAS,YAAY,GAAa,QAAiC;AACjE,QAAM,KAAK,OAAO,WAAW,EAAE,YAAY,WAAW,EAAE,YAAY,UAAU,UAAU;AACxF,SAAO,eAAe,EAAE,QAAQ,EAAE,IAAI,OAAO,OAAO,MAAM,CAAC,KAAK;AAClE;AASO,SAAS,kBAAkB,QAA+C;AAC/E,QAAM,IAAI,QAAQ,MAAM;AACxB,SAAO;AAAA,IACL,QAAQ,SAAS,UAAU,8BAA8B,KAAK,KAAK,YAAY,GAAG,MAAM,CAAC;AAAA,IACzF,OAAO,UAAU,EAAE,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAC/C,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,EAC5B,KAAK,IAAI;AAAA,EACd;AACF;AAEA,SAAS,uBAAgC;AACvC,SAAO,OAAO,eAAe,cAAc,WAAW,kCAAkC,EAAE;AAC5F;AAGA,SAAS,UAAU,GAAa,GAA+B;AAC7D,SAAO;AAAA,IACL,EAAE;AAAA,IACF,EAAE;AAAA,IACF,EAAE;AAAA,IACF,EAAE;AAAA,IACF,EAAE;AAAA,IACF,EAAE,OAAO,KAAK,GAAG;AAAA,IACjB,EAAE;AAAA,IACF,EAAE;AAAA,IACF,OAAO,GAAG,CAAC;AAAA,IACX,EAAE;AAAA,IACF,EAAE;AAAA,IACF,EAAE,iBAAiB;AAAA,EACrB;AACF;AAkBO,SAAS,aAAa,MAAmB,SAA+D;AAC7G,QAAM,QAAmC,CAAC;AAC1C,QAAM,MAAM,KAAK;AACjB,QAAM,QAAQ,IAAI,cAAc,MAAM;AACtC,QAAM,MAAM,UAAU;AACtB,OAAK,YAAY,KAAK;AAEtB,MAAI,SAAS;AACb,MAAI,WAAW,QAAQ,QAAQ,KAAK;AACpC,MAAI;AACJ,MAAI,MAAiB,CAAC;AAEtB,MAAI;AAEJ,QAAM,QAAQ,oBAAI,IAAY;AAE9B,QAAM,SAAS,MAAM,KAAK,iBAA8B,aAAa,UAAU,GAAG;AAClF,QAAM,aAAa,CAAC,SAAkB;AACpC,eAAW,KAAK,OAAO,EAAG,GAAE,MAAM,UAAU,OAAO,UAAU;AAAA,EAC/D;AAIA,QAAM,WAAW,MAAM;AACrB,QAAI,MAAM,eAAe,KAAM,MAAK,YAAY,KAAK;AACrD,QAAI,OAAQ,YAAW,KAAK;AAAA,EAC9B;AACA,QAAM,WAAW,OAAO,qBAAqB,aAAa,IAAI,iBAAiB,QAAQ,IAAI;AAC3F,YAAU,QAAQ,MAAM,EAAE,WAAW,KAAK,CAAC;AAE3C,QAAM,OAAO,MAAM;AACjB,UAAM,QAAQ,UAAU,SAAS,OAAO,KAAK,SAAS,QAAQ,SAAS,OAAO;AAC9E,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO;AACjC,UAAI,CAAC,MAAM,IAAI,IAAI,KAAK,KAAK,MAAM,iBAAiB,IAAI,EAAG;AAC3D,WAAK,MAAM,YAAY,MAAM,KAAK;AAClC,YAAM,IAAI,IAAI;AAAA,IAChB;AAEA,eAAW,QAAQ;AACjB,UAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,MAAM,MAAM,IAAI,GAAG;AACpC,aAAK,MAAM,eAAe,IAAI;AAC9B,cAAM,OAAO,IAAI;AAAA,MACnB;AAAA,EACJ;AAEA,MAAI,OAAO,EAAE,WAAW,GAAG;AACzB,eAAW,IAAI,cAAc,MAAM;AACnC,aAAS,aAAa,YAAY,EAAE;AACpC,aAAS,aAAa,eAAe,MAAM;AAC3C,aAAS,MAAM,UAAU;AACzB,SAAK,aAAa,UAAU,KAAK;AACjC,SAAK;AAAA,EACP;AAEA,QAAM,OAAO,MAAM;AACjB,QAAI,CAAC,OAAQ;AACb,WAAO,QAAQ;AACf,aAAS;AACT,eAAW,IAAI;AACf,WAAO,WAAW,MAAS;AAAA,EAC7B;AAEA,QAAM,QAAQ,MAAM;AAClB,UAAM,UAAU,OAAO,iBAAiB;AACxC,QAAI,YAAY,YAAa,YAAY,UAAU,qBAAqB,EAAI;AAC5E,aAAS,IAAI,UAAU,OAAO,SAAS,QAAQ;AAAA,MAC7C,MAAM,OAAO;AAAA,MACb,UAAU,OAAO;AAAA,MACjB,OAAO,OAAO;AAAA,MACd,MAAM,SAAS;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO,OAAO;AAAA,MACd,SAAS,SAAS;AAAA,MAClB,iBAAiB,OAAO;AAAA,MACxB,aAAa,OAAO,UAAU,MAAM;AAAA,MACpC,gBAAgB,OAAO;AAAA,MACvB,OAAO,OAAO;AAAA;AAAA,MAEd,QAAQ,CAAC,OAAO,OAAO,SAAS,EAAE;AAAA,IACpC,CAAC;AACD,eAAW,KAAK;AAChB,WAAO,WAAW,MAAM;AAAA,EAC1B;AAEA,QAAM,QAAQ,MAAM;AAClB,aAAS;AACT,QAAI,UAAU;AACZ,eAAS,YAAY,YAAY,UAAU,MAAM;AACjD,WAAK;AAAA,IACP;AACA,UAAM,OAAO,UAAU,UAAU,MAAM;AACvC,QAAI,UAAU,KAAK,MAAM,CAAC,GAAG,MAAM,MAAM,IAAI,CAAC,CAAC,GAAG;AAChD,aAAO,SAAS,OAAO,SAAS,CAAC;AACjC;AAAA,IACF;AACA,UAAM;AACN,SAAK;AACL,UAAM;AAAA,EACR;AAEA,QAAM;AAEN,SAAO;AAAA,IACL,OAAO,MAAM;AACX,eAAS;AACT,iBAAW,QAAQ,QAAQ,KAAK;AAChC,YAAM;AAAA,IACR;AAAA,IACA,UAAU;AACR,gBAAU,WAAW;AACrB,WAAK;AACL,YAAM,OAAO;AACb,gBAAU,OAAO;AACjB,iBAAW,QAAQ,MAAO,MAAK,MAAM,eAAe,IAAI;AAAA,IAC1D;AAAA,EACF;AACF;AA4BO,SAAS,YAAY,MAAmB,SAA6D;AAC1G,MAAI,SAAS;AACb,MAAI,CAAC,KAAK,MAAM,SAAU,MAAK,MAAM,WAAW;AAChD,QAAM,QAAQ,MAAM;AAClB,UAAM,IAAI,YAAY,MAAM,OAAO,QAAQ;AAAA,MACzC,GAAI,OAAO,UAAU,EAAE,SAAS,OAAO,QAAQ,IAAI,CAAC;AAAA,MACpD,GAAI,OAAO,SAAS,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,MACjD,GAAI,OAAO,SAAS,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,MACjD,UAAU,OAAO,YAAY;AAAA,MAC7B,iBAAiB,OAAO,mBAAmB;AAAA,IAC7C,CAAC;AACD,MAAE,QAAQ,MAAM,UAAU;AAC1B,MAAE,QAAQ,MAAM,QAAQ;AACxB,MAAE,QAAQ,MAAM,SAAS;AACzB,WAAO;AAAA,EACT;AACA,MAAI,UAAU,MAAM;AACpB,SAAO;AAAA,IACL,OAAO,MAAM;AACX,YAAM,UACJ,KAAK,WAAW,OAAO,UAAU,KAAK,WAAW,OAAO,UAAU,KAAK,aAAa,OAAO,YAAY,KAAK,oBAAoB,OAAO;AACzI,YAAM,iBAAiB,KAAK,YAAY,OAAO;AAC/C,YAAM,gBAAgB,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,MAAM,KAAK,UAAU,OAAO,UAAU,CAAC,CAAC;AAC9F,eAAS;AACT,UAAI,SAAS;AACX,gBAAQ,QAAQ;AAChB,kBAAU,MAAM;AAChB;AAAA,MACF;AACA,UAAI,kBAAkB,KAAK,QAAS,SAAQ,WAAW,KAAK,OAAO;AACnE,UAAI,iBAAiB,KAAK,OAAQ,SAAQ,UAAU,KAAK,MAAM;AAAA,IACjE;AAAA,IACA,UAAU;AACR,cAAQ,QAAQ;AAAA,IAClB;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/svelte/index.ts"],"sourcesContent":["/**\n * Svelte 5 bindings for Flair, written as plain TypeScript so they need no\n * Svelte compiler: the `flairEffect` action runs any catalog effect (shader or\n * vector) applied to a picture, video, SVG or words; the `vectorEffect` action plays a catalog\n * effect (or any {@link SvgMotion}) in an element, and `vectorEffectStill`\n * gives the markup and style of its still for server rendering.\n *\n * ```svelte\n * <script>\n * import { vectorEffect, vectorEffectStill } from \"@pixerate/flair/svelte\";\n * const params = { id: \"drip-text\", values: { word: \"Slop\" }, loop: false };\n * const still = vectorEffectStill(params);\n * </script>\n *\n * <span use:vectorEffect={params} style={still.style}>{@html still.markup}</span>\n * ```\n *\n * The still is in place from the first paint; the live `SvgPlayer` takes over\n * after mount and is disposed on destroy. Under `prefers-reduced-motion:\n * reduce` the still stays.\n *\n * Needs Svelte 5 (an optional peer dependency of the package); only its types\n * are imported.\n *\n * @packageDocumentation\n */\n\nimport type { ActionReturn } from \"svelte/action\";\nimport { getEffect, effectDefaults, type EffectDefinition } from \"../catalog/index.js\";\nimport { mountEffect, type EffectSubject } from \"../embed/index.js\";\nimport { renderVectorEffect, type VectorEffect as VectorEffectDefinition } from \"../catalog/types.js\";\nimport type { ControlValue } from \"../shader/controls.js\";\nimport type { Clock } from \"../time/clock.js\";\nimport type { SvgMotion } from \"../vector/motion.js\";\nimport { svgFrameMarkup, svgInlineLayout, svgViewBox } from \"../vector/static.js\";\nimport { SvgPlayer, type SvgTrigger } from \"../vector/svg.js\";\n\n/** Cap height of a heavy sans, as a share of its font size: what an inline word is framed to. */\nconst CAP_HEIGHT = \"0.72em\";\n\n/** Marks the still's layer, so the action can find it (and hide it while the player runs). */\nconst STILL_ATTR = \"data-flair-still\";\n\nconst LAYER = \"position:absolute;inset:0;display:block\";\n\n/** When to leave the still in place of the animation. */\nexport type ReducedMotion = \"user\" | \"always\" | \"never\";\n\n/** Options shared by an effect by id and a motion of your own. */\nexport interface SvgPlayerParams {\n /** Start again at the end (default `true`); `false` plays once and stays on the last frame. */\n loop?: boolean;\n /** Start playing at once (default `true`). */\n autoplay?: boolean;\n /** Playback speed (default 1); changed without remaking the player. */\n speed?: number;\n /** Ms to rest on the last frame before looping (default 600, or the effect's). */\n hold?: number;\n /** Loop length in ms (default: the picture's own, plus `hold`). */\n duration?: number;\n /** Step start times in ms, for pictures that change on steps. */\n steps?: readonly number[];\n /** Plays in answer to the pointer (see `SvgTrigger`; default the effect's). */\n trigger?: SvgTrigger;\n /** Pause while scrolled out of view. */\n pauseWhenHidden?: boolean;\n /**\n * Wait to start until scrolled into view. Default `true` when it plays once\n * (`loop: false`, no trigger), so a one-shot further down the page does not\n * finish before anyone sees it; `false` plays at mount.\n */\n playOnEnter?: boolean;\n /** Hide the picture while paused or reset. */\n hideWhenPaused?: boolean;\n /** Time source (default animation frames); a `ManualClock` steps frames yourself. */\n clock?: Clock;\n /** Called with the id of the innermost element with an id that is clicked. */\n onPick?: (id: string) => void;\n /** Called with the player once it is made, and `undefined` when it is disposed. */\n onPlayer?: (player: SvgPlayer | undefined) => void;\n /**\n * The still shown before the player starts and instead of it under reduced\n * motion: `\"last\"` (default; the rest frame, `\"first\"` for a hover or press\n * trigger), `\"first\"` or a time in ms.\n */\n poster?: \"first\" | \"last\" | number;\n /**\n * `\"user\"` (default) shows the still when the visitor prefers reduced\n * motion, `\"always\"` always does, `\"never\"` always plays.\n */\n reducedMotion?: ReducedMotion;\n /**\n * Size it as a word in a line of text: `display: inline-block` with the\n * viewBox's aspect ratio and `height` 0.72em (the cap height of a heavy\n * sans; pass a CSS length for another). Default: the effect's `inline`\n * control. Otherwise it is a block as wide as its container with the\n * viewBox's aspect ratio.\n */\n inline?: boolean | string;\n /**\n * With `inline`, keep room for what spills past the word (descenders,\n * outlines, glows, drips): a margin of the picture's `data-overhang` (see\n * `svgInlineLayout`), so the lines around it make space instead of being\n * painted over. Default `false`.\n */\n contain?: boolean;\n}\n\n/** A catalog vector effect by id (or the effect itself), with its values. */\nexport interface VectorEffectByIdParams extends SvgPlayerParams {\n /** A vector effect's catalog id (`\"drip-text\"`), or the effect itself. */\n id: string | VectorEffectDefinition;\n /** Control values; missing ones take the preset's, then the defaults. Compared by content. */\n values?: Record<string, ControlValue>;\n /** A preset's name to start from. */\n preset?: string;\n motion?: never;\n}\n\n/** Any {@link SvgMotion}; a new object restarts the player. */\nexport interface SvgMotionParams extends SvgPlayerParams {\n /** The picture and its animations. */\n motion: SvgMotion;\n id?: never;\n values?: never;\n preset?: never;\n}\n\n/** What `vectorEffect` and `vectorEffectStill` take: an effect by `id`, or a `motion`. */\nexport type VectorEffectParams = VectorEffectByIdParams | SvgMotionParams;\n\n/** A still of the picture, for `{@html markup}` inside an element with `style`. */\nexport interface VectorEffectStill {\n /** The still's layer: safe `<svg>` markup (see `svgFrameMarkup`) in an absolutely placed `<span>`. */\n markup: string;\n /** The host's inline style: its position and size from the viewBox. */\n style: string;\n}\n\ninterface Resolved {\n motion: SvgMotion;\n hold?: number;\n trigger?: SvgTrigger;\n inline?: boolean | string;\n contain?: boolean;\n}\n\n/** Remembers the last motion made from an effect, so values compared by content do not remake it. */\ntype MotionCache = { key: string; effect: VectorEffectDefinition; motion: SvgMotion };\n\nfunction resolve(params: VectorEffectParams, cache?: { current?: MotionCache }): Resolved {\n if (params.motion) return { motion: params.motion, hold: params.hold, trigger: params.trigger, inline: params.inline, contain: params.contain };\n const { id, values, preset } = params as VectorEffectByIdParams;\n const effect = typeof id === \"string\" ? getEffect(id) : id;\n if (!effect || effect.kind !== \"vector\")\n throw new Error(\n `\"${typeof id === \"string\" ? id : (effect?.id ?? id)}\" is not a vector effect; pick one from findEffects({ kind: \"vector\" }), or pass a motion`,\n );\n const key = JSON.stringify([preset ?? null, values ?? {}]);\n let motion: SvgMotion;\n if (cache?.current && cache.current.effect === effect && cache.current.key === key) motion = cache.current.motion;\n else {\n motion = renderVectorEffect(effect, effectDefaults(effect, { preset, values }));\n if (cache) cache.current = { key, effect, motion };\n }\n return {\n motion,\n hold: params.hold ?? effect.hold,\n trigger: params.trigger ?? effect.trigger,\n inline: params.inline ?? values?.inline === true,\n contain: params.contain,\n };\n}\n\n/** The host's sizing from the picture's viewBox, so the still and the player take the same room. */\nfunction hostStyle(svg: string, inline: boolean | string | undefined, contain?: boolean): [string, string][] {\n const box = svgViewBox(svg);\n const style: [string, string][] = [\n [\"position\", \"relative\"],\n [\"display\", inline ? \"inline-block\" : \"block\"],\n ];\n if (inline) style.push([\"height\", typeof inline === \"string\" ? inline : CAP_HEIGHT]);\n if (box) style.push([\"aspect-ratio\", `${box[2]} / ${box[3]}`]);\n const spill = inline && contain ? svgInlineLayout(svg)?.overhang : undefined;\n if (spill) style.push([\"margin\", `${spill.top}em ${spill.right}em ${spill.bottom}em ${spill.left}em`]);\n return style;\n}\n\n/** Whether to wait until scrolled into view: as asked, else when it plays once. */\nconst enters = (r: Resolved, p: SvgPlayerParams) => p.playOnEnter ?? (p.loop === false && !r.trigger);\n\nfunction stillMarkup(r: Resolved, params: SvgPlayerParams): string {\n const at = params.poster ?? (r.trigger === \"hover\" || r.trigger === \"press\" || r.trigger === \"scroll\" ? \"first\" : \"last\");\n return svgFrameMarkup(r.motion, { at, steps: params.steps }) ?? \"\";\n}\n\n/**\n * The still of an effect or motion, for server rendering: put `markup` in the\n * element that has the `vectorEffect` action, with `style` as its style, so\n * the page shows the picture before the player starts (and instead of it\n * under reduced motion), in the room the player will take.\n * Throws when `id` is not a vector effect.\n */\nexport function vectorEffectStill(params: VectorEffectParams): VectorEffectStill {\n const r = resolve(params);\n return {\n markup: `<span ${STILL_ATTR} aria-hidden=\"true\" style=\"${LAYER}\">${stillMarkup(r, params)}</span>`,\n style: hostStyle(r.motion.svg, r.inline, r.contain)\n .map(([k, v]) => `${k}: ${v}`)\n .join(\"; \"),\n };\n}\n\nfunction prefersReducedMotion(): boolean {\n return typeof matchMedia === \"function\" && matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/** The options whose change remakes the player (`speed` is set on it instead). */\nfunction playerKey(r: Resolved, p: SvgPlayerParams): unknown[] {\n return [\n r.motion,\n p.loop,\n p.autoplay,\n r.hold,\n p.duration,\n p.steps?.join(\",\"),\n r.trigger,\n p.pauseWhenHidden,\n enters(r, p),\n p.hideWhenPaused,\n p.clock,\n p.reducedMotion ?? \"user\",\n ];\n}\n\n/**\n * A Svelte action that plays a vector effect in its element:\n * `<span use:vectorEffect={{ id: \"drip-text\", values: { word: \"Slop\" }, loop: false }} />`.\n *\n * It makes an `SvgPlayer` in a layer of its own, remakes it when the params\n * change (values by content; `speed` without remaking) and disposes it on\n * destroy. A still from {@link vectorEffectStill} in the element is hidden\n * while the player runs and shown again after; without one, the action makes\n * its own (so reduced motion still shows the picture) and sizes the element\n * from the viewBox, leaving alone any of `position`, `display`, `height`,\n * `aspect-ratio` and `margin` the element's style already sets.\n *\n * For an attachment, `{@attach fromAction(vectorEffect, () => params)}`\n * (`fromAction` is from `svelte/attachments`). Throws when `id` is not a\n * vector effect.\n */\nexport function vectorEffect(node: HTMLElement, initial: VectorEffectParams): ActionReturn<VectorEffectParams> {\n const cache: { current?: MotionCache } = {};\n const doc = node.ownerDocument;\n const mount = doc.createElement(\"span\");\n mount.style.cssText = LAYER;\n node.appendChild(mount);\n\n let params = initial;\n let resolved = resolve(params, cache);\n let player: SvgPlayer | undefined;\n let key: unknown[] = [];\n /** The still the action made itself, when the element had none. */\n let ownStill: HTMLElement | undefined;\n /** Sizing properties the action set (the element's own style had them empty). */\n const sized = new Set<string>();\n\n const stills = () => node.querySelectorAll<HTMLElement>(`:scope > [${STILL_ATTR}]`);\n const showStills = (show: boolean) => {\n for (const s of stills()) s.style.display = show ? \"block\" : \"none\";\n };\n // When the params change Svelte replaces a server still (`{@html}`), and when\n // it is the element's only child, by setting `innerHTML`, which takes the\n // player's layer out too: put the layer back, and keep a new still hidden.\n const reattach = () => {\n if (mount.parentNode !== node) node.appendChild(mount);\n if (player) showStills(false);\n };\n const observer = typeof MutationObserver === \"function\" ? new MutationObserver(reattach) : undefined;\n observer?.observe(node, { childList: true });\n\n const size = () => {\n const style = hostStyle(resolved.motion.svg, resolved.inline, resolved.contain);\n for (const [prop, value] of style) {\n if (!sized.has(prop) && node.style.getPropertyValue(prop)) continue;\n node.style.setProperty(prop, value);\n sized.add(prop);\n }\n // A property the new sizing drops (`height` when no longer inline) is cleared.\n for (const prop of sized)\n if (!style.some(([p]) => p === prop)) {\n node.style.removeProperty(prop);\n sized.delete(prop);\n }\n };\n\n if (stills().length === 0) {\n ownStill = doc.createElement(\"span\");\n ownStill.setAttribute(STILL_ATTR, \"\");\n ownStill.setAttribute(\"aria-hidden\", \"true\");\n ownStill.style.cssText = LAYER;\n node.insertBefore(ownStill, mount);\n size();\n }\n\n const stop = () => {\n if (!player) return;\n player.dispose();\n player = undefined;\n showStills(true);\n params.onPlayer?.(undefined);\n };\n\n const start = () => {\n const reduced = params.reducedMotion ?? \"user\";\n if (reduced === \"always\" || (reduced === \"user\" && prefersReducedMotion())) return;\n player = new SvgPlayer(mount, resolved.motion, {\n loop: params.loop,\n autoplay: params.autoplay,\n speed: params.speed,\n hold: resolved.hold,\n duration: params.duration,\n steps: params.steps,\n trigger: resolved.trigger,\n pauseWhenHidden: params.pauseWhenHidden,\n playOnEnter: enters(resolved, params),\n hideWhenPaused: params.hideWhenPaused,\n clock: params.clock,\n // The latest params' callback, without remaking the player.\n onPick: (id) => params.onPick?.(id),\n });\n showStills(false);\n params.onPlayer?.(player);\n };\n\n const apply = () => {\n reattach();\n if (ownStill) {\n ownStill.innerHTML = stillMarkup(resolved, params);\n size();\n }\n const next = playerKey(resolved, params);\n if (player && next.every((v, i) => v === key[i])) {\n player.setSpeed(params.speed ?? 1);\n return;\n }\n key = next;\n stop();\n start();\n };\n\n apply();\n\n return {\n update(next) {\n params = next;\n resolved = resolve(params, cache);\n apply();\n },\n destroy() {\n observer?.disconnect();\n stop();\n mount.remove();\n ownStill?.remove();\n for (const prop of sized) node.style.removeProperty(prop);\n },\n };\n}\n\nexport type { SvgPlayer, SvgMotion, SvgTrigger };\n\n/** What the `flairEffect` action runs: any catalog effect, applied to a subject. */\nexport interface FlairEffectParams {\n /** A catalog effect, by id or definition: shader or vector. */\n effect: string | EffectDefinition;\n /** What to apply it to: a picture, a video, an SVG, words or a path (see `EffectSubject`). */\n subject?: EffectSubject | readonly EffectSubject[];\n preset?: string;\n values?: Record<string, ControlValue>;\n /** Start playing at once (default `true`). */\n autoplay?: boolean;\n /** Pause while scrolled out of view (default `true`). */\n pauseWhenHidden?: boolean;\n}\n\n/**\n * Runs any catalog effect, shader or vector, in an element, applied to a\n * subject (see `mountEffect` in `@pixerate/flair/embed`); it fills the\n * element, so size it with CSS. A new subject or values update the running\n * effect; a new effect, preset or playback option starts it again.\n *\n * ```svelte\n * <div use:flairEffect={{ effect: \"image-duotone\", subject: { image: \"/team.jpg\" } }} style=\"aspect-ratio: 16 / 9\"></div>\n * ```\n */\nexport function flairEffect(node: HTMLElement, initial: FlairEffectParams): ActionReturn<FlairEffectParams> {\n let params = initial;\n if (!node.style.position) node.style.position = \"relative\";\n const start = () => {\n const m = mountEffect(node, params.effect, {\n ...(params.subject ? { subject: params.subject } : {}),\n ...(params.preset ? { preset: params.preset } : {}),\n ...(params.values ? { values: params.values } : {}),\n autoplay: params.autoplay ?? true,\n pauseWhenHidden: params.pauseWhenHidden ?? true,\n });\n m.element.style.display = \"block\";\n m.element.style.width = \"100%\";\n m.element.style.height = \"100%\";\n return m;\n };\n let mounted = start();\n return {\n update(next) {\n const restart =\n next.effect !== params.effect || next.preset !== params.preset || next.autoplay !== params.autoplay || next.pauseWhenHidden !== params.pauseWhenHidden;\n const subjectChanged = next.subject !== params.subject;\n const valuesChanged = JSON.stringify(next.values ?? {}) !== JSON.stringify(params.values ?? {});\n params = next;\n if (restart) {\n mounted.dispose();\n mounted = start();\n return;\n }\n if (subjectChanged && next.subject) mounted.setSubject(next.subject);\n if (valuesChanged && next.values) mounted.setValues(next.values);\n },\n destroy() {\n mounted.dispose();\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -289,17 +289,30 @@ interface SvgPlayerOptions extends SvgViewOptions {
289
289
  /** Hide the view (`visibility: hidden`) whenever playback is paused or reset (default false). */
290
290
  hideWhenPaused?: boolean;
291
291
  /**
292
- * Plays in answer to the pointer instead of on its own (see {@link SvgTrigger}):
293
- * `"hover"` and `"press"` play to the end while hovered or held and back to
294
- * the start after; `"click"` plays the next step on each click, starting
295
- * over after the last. `autoplay`, `loop` and `steps` are ignored.
292
+ * Plays in answer to the pointer or the page instead of on its own (see
293
+ * {@link SvgTrigger}): `"hover"` and `"press"` play to the end while
294
+ * hovered or held and back to the start after; `"click"` plays the next
295
+ * step on each click, starting over after the last; `"scroll"` follows
296
+ * the trigger target's progress through the viewport (see `scrollRange`),
297
+ * scrubbing back and forth as the page scrolls. `autoplay`, `loop` and
298
+ * `steps` are ignored.
296
299
  */
297
300
  trigger?: SvgTrigger;
298
- /** The element whose pointer events trigger it (default the container). */
301
+ /** The element whose pointer events trigger it, or whose scrolling a scroll trigger follows (default the container). */
299
302
  triggerTarget?: HTMLElement;
303
+ /**
304
+ * For a scroll trigger: the stretch of the target's progress through the
305
+ * viewport (0 as its top comes in at the bottom, 1 as its bottom leaves at
306
+ * the top) over which the picture plays from start to end (default
307
+ * `[0.1, 0.6]`: from just after it comes into view to a little past the
308
+ * middle).
309
+ */
310
+ scrollRange?: readonly [number, number];
300
311
  }
301
312
  /** What makes a triggered {@link SvgPlayer} play. */
302
- type SvgTrigger = "hover" | "press" | "click";
313
+ type SvgTrigger = "hover" | "press" | "click" | "scroll";
314
+ /** The stretch of the viewport a scroll-triggered picture plays over (see `SvgPlayerOptions.scrollRange`). */
315
+ declare const SCROLL_RANGE: readonly [number, number];
303
316
  /**
304
317
  * Plays an {@link SvgMotion} inside `container`: loops by default, and can be
305
318
  * paused, sought and sped up. Dispose it to stop and remove the picture.
@@ -327,6 +340,10 @@ declare class SvgPlayer {
327
340
  private wasPlayingBeforeHidden;
328
341
  private hasEnteredViewport;
329
342
  private intersectionObserver?;
343
+ private readonly scrollRange;
344
+ /** A scroll trigger's pending update (one per frame). */
345
+ private scrollFrame;
346
+ private followingScroll;
330
347
  constructor(container: HTMLElement, motion: SvgMotion, options?: SvgPlayerOptions);
331
348
  /** The loop length in ms. */
332
349
  get duration(): number;
@@ -339,6 +356,17 @@ declare class SvgPlayer {
339
356
  reset(): void;
340
357
  /** Shows the picture at `ms` (wrapped into the loop when looping). */
341
358
  seek(ms: number): void;
359
+ /**
360
+ * Shows the picture at `progress` of the way through it, 0–1: how hosts
361
+ * scrub it themselves (a slider, a scroll position of their own). A scroll
362
+ * trigger stops following the page once a host calls this.
363
+ */
364
+ setProgress(progress: number): void;
365
+ /**
366
+ * A scroll trigger's playback position, 0–1, for the trigger target's
367
+ * `progress` through the viewport (see {@link SvgPlayerOptions.scrollRange}).
368
+ */
369
+ scrollPosition(progress: number): number;
342
370
  setSpeed(speed: number): void;
343
371
  /**
344
372
  * Plays to the end (`true`) or back to the start (`false`) from wherever it
@@ -356,6 +384,10 @@ declare class SvgPlayer {
356
384
  private onEnter;
357
385
  private onLeave;
358
386
  private onClick;
387
+ /** Seeks to where the trigger target is in the viewport. */
388
+ private followScroll;
389
+ /** A scroll or resize: follow it on the next frame (or now, without frames). */
390
+ private onScroll;
359
391
  private listen;
360
392
  private run;
361
393
  }
@@ -386,4 +418,4 @@ interface AnimateCountOptions {
386
418
  */
387
419
  declare function animateCount(options: AnimateCountOptions): () => void;
388
420
 
389
- export { type AnimateCountOptions as A, svgMotionIssues as B, type Clock as C, type DataKey as D, EASES as E, svgOpeningTag as F, svgView as G, writtenText as H, ManualClock as M, type SvgMotion as S, type SvgTrigger as a, SvgPlayer as b, type EaseName as c, SCRAMBLE_MS as d, STEP_MS as e, SVG_PICTURE as f, type SvgAnimation as g, type SvgBox as h, type SvgElementFrame as i, type SvgPlayerOptions as j, type SvgView as k, type SvgViewOptions as l, animateCount as m, defaultDataKey as n, formatCount as o, jitter as p, lerpAttr as q, lerpData as r, pageHidden as s, parseSvg as t, realClock as u, stepData as v, stepProgress as w, stepStart as x, svgDuration as y, svgFrame as z };
421
+ export { type AnimateCountOptions as A, svgFrame as B, type Clock as C, type DataKey as D, EASES as E, svgMotionIssues as F, svgOpeningTag as G, svgView as H, writtenText as I, ManualClock as M, type SvgMotion as S, type SvgTrigger as a, SvgPlayer as b, type EaseName as c, SCRAMBLE_MS as d, SCROLL_RANGE as e, STEP_MS as f, SVG_PICTURE as g, type SvgAnimation as h, type SvgBox as i, type SvgElementFrame as j, type SvgPlayerOptions as k, type SvgView as l, type SvgViewOptions as m, animateCount as n, defaultDataKey as o, formatCount as p, jitter as q, lerpAttr as r, lerpData as s, pageHidden as t, parseSvg as u, realClock as v, stepData as w, stepProgress as x, stepStart as y, svgDuration as z };
@@ -1,8 +1,8 @@
1
1
  import { C as ControlValue, a as ControlDef } from './controls-Ec6P0Vpy.js';
2
- import { S as SvgMotion, a as SvgTrigger } from './svg-BhRboN9q.js';
2
+ import { S as SvgMotion, a as SvgTrigger } from './svg-DaKRPA9v.js';
3
3
 
4
4
  /** What an effect is for, used to group the catalog. */
5
- type EffectCategory = "gradient" | "organic" | "pattern" | "light" | "lighting" | "space" | "retro" | "image" | "overlay" | "text" | "morph" | "interaction" | "pyrotechnics" | "timewarp" | "melt" | "electric" | "splatter" | "bubble" | "sticker" | "trail" | "particles" | "debris" | "impact" | "comic" | "atmosphere" | "glow" | "shadow" | "distortion" | "transition" | "loader" | "data" | "icon";
5
+ type EffectCategory = "gradient" | "organic" | "pattern" | "light" | "lighting" | "space" | "retro" | "image" | "overlay" | "text" | "morph" | "interaction" | "pyrotechnics" | "timewarp" | "melt" | "electric" | "splatter" | "bubble" | "sticker" | "trail" | "particles" | "debris" | "impact" | "comic" | "atmosphere" | "glow" | "shadow" | "distortion" | "audio" | "scroll" | "broadcast" | "annotation" | "transition" | "loader" | "data" | "icon";
6
6
  /** Every category, in the order the catalog lists them. */
7
7
  declare const EFFECT_CATEGORIES: readonly EffectCategory[];
8
8
  /**
@@ -1,5 +1,5 @@
1
- import { h as SvgBox, S as SvgMotion } from '../svg-BhRboN9q.js';
2
- export { A as AnimateCountOptions, D as DataKey, E as EASES, c as EaseName, d as SCRAMBLE_MS, e as STEP_MS, f as SVG_PICTURE, g as SvgAnimation, i as SvgElementFrame, b as SvgPlayer, j as SvgPlayerOptions, a as SvgTrigger, k as SvgView, l as SvgViewOptions, m as animateCount, n as defaultDataKey, o as formatCount, q as lerpAttr, r as lerpData, t as parseSvg, v as stepData, w as stepProgress, x as stepStart, y as svgDuration, z as svgFrame, B as svgMotionIssues, F as svgOpeningTag, G as svgView, H as writtenText } from '../svg-BhRboN9q.js';
1
+ import { i as SvgBox, S as SvgMotion } from '../svg-DaKRPA9v.js';
2
+ export { A as AnimateCountOptions, D as DataKey, E as EASES, c as EaseName, d as SCRAMBLE_MS, e as SCROLL_RANGE, f as STEP_MS, g as SVG_PICTURE, h as SvgAnimation, j as SvgElementFrame, b as SvgPlayer, k as SvgPlayerOptions, a as SvgTrigger, l as SvgView, m as SvgViewOptions, n as animateCount, o as defaultDataKey, p as formatCount, r as lerpAttr, s as lerpData, u as parseSvg, w as stepData, x as stepProgress, y as stepStart, z as svgDuration, B as svgFrame, F as svgMotionIssues, G as svgOpeningTag, H as svgView, I as writtenText } from '../svg-DaKRPA9v.js';
3
3
 
4
4
  /**
5
5
  * Shape morphing: any SVG paths turned into paths with the same structure, so
@@ -7,16 +7,17 @@ import {
7
7
  } from "../chunk-H6L67KHI.js";
8
8
  import {
9
9
  svgAnimatedMarkup
10
- } from "../chunk-YBGRXVAE.js";
10
+ } from "../chunk-X3C2VFTM.js";
11
11
  import {
12
12
  parseViewBox,
13
13
  svgFrameMarkup,
14
14
  svgInlineLayout,
15
15
  svgViewBox
16
- } from "../chunk-STKEKXCK.js";
16
+ } from "../chunk-AUH3PEFW.js";
17
17
  import {
18
18
  EASES,
19
19
  SCRAMBLE_MS,
20
+ SCROLL_RANGE,
20
21
  STEP_MS,
21
22
  SVG_PICTURE,
22
23
  SvgPlayer,
@@ -35,7 +36,7 @@ import {
35
36
  svgOpeningTag,
36
37
  svgView,
37
38
  writtenText
38
- } from "../chunk-PFGGPIMT.js";
39
+ } from "../chunk-DA4UUSDO.js";
39
40
  import "../chunk-WMZQCUFJ.js";
40
41
  import {
41
42
  morphPaths,
@@ -44,6 +45,7 @@ import {
44
45
  export {
45
46
  EASES,
46
47
  SCRAMBLE_MS,
48
+ SCROLL_RANGE,
47
49
  STEP_MS,
48
50
  SVG_PICTURE,
49
51
  SvgPlayer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/flair",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "Web visual effects and motion graphics: WebGL2 shader effects, animated vector effects, a catalog of ready-made effects, and a UI-agnostic, agent-ready effect editor.",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",