@pixerate/flair 0.23.0 → 0.25.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 (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +124 -6
  3. package/dist/catalog/index.d.ts +2 -2
  4. package/dist/catalog/index.js +2 -2
  5. package/dist/chunk-DBTLCXKY.js +332 -0
  6. package/dist/chunk-DBTLCXKY.js.map +1 -0
  7. package/dist/{chunk-VP6IEWIK.js → chunk-H6L67KHI.js} +1 -1
  8. package/dist/chunk-H6VDFBIO.js +498 -0
  9. package/dist/chunk-H6VDFBIO.js.map +1 -0
  10. package/dist/chunk-JL6TI6II.js +724 -0
  11. package/dist/chunk-JL6TI6II.js.map +1 -0
  12. package/dist/{chunk-NGKNI3QX.js → chunk-O44ZJTGG.js} +5 -485
  13. package/dist/chunk-O44ZJTGG.js.map +1 -0
  14. package/dist/chunk-OKYMKSXK.js +414 -0
  15. package/dist/chunk-OKYMKSXK.js.map +1 -0
  16. package/dist/{chunk-OHW7UG6D.js → chunk-ORAFCHX3.js} +26 -2
  17. package/dist/{chunk-OHW7UG6D.js.map → chunk-ORAFCHX3.js.map} +1 -1
  18. package/dist/{chunk-4F4KL4KU.js → chunk-PYDN5G2P.js} +769 -11
  19. package/dist/chunk-PYDN5G2P.js.map +1 -0
  20. package/dist/{chunk-ARKC7LDW.js → chunk-RDU63CDO.js} +2 -331
  21. package/dist/chunk-RDU63CDO.js.map +1 -0
  22. package/dist/chunk-S2RGHHKZ.js +902 -0
  23. package/dist/chunk-S2RGHHKZ.js.map +1 -0
  24. package/dist/{chunk-LVPIIWPP.js → chunk-STKEKXCK.js} +18 -8
  25. package/dist/chunk-STKEKXCK.js.map +1 -0
  26. package/dist/chunk-YBGRXVAE.js +407 -0
  27. package/dist/chunk-YBGRXVAE.js.map +1 -0
  28. package/dist/cli.js +68 -5
  29. package/dist/cli.js.map +1 -1
  30. package/dist/editor/index.d.ts +2 -2
  31. package/dist/editor/index.js +14 -12
  32. package/dist/editor/index.js.map +1 -1
  33. package/dist/effect-26CPF5HS.js +47 -0
  34. package/dist/embed/index.d.ts +215 -0
  35. package/dist/embed/index.js +21 -0
  36. package/dist/embed/index.js.map +1 -0
  37. package/dist/frames-VEcqa40g.d.ts +161 -0
  38. package/dist/index.d.ts +6 -163
  39. package/dist/index.js +86 -78
  40. package/dist/index.js.map +1 -1
  41. package/dist/node.d.ts +167 -7
  42. package/dist/node.js +25 -3
  43. package/dist/react/index.d.ts +33 -2
  44. package/dist/react/index.js +54 -9
  45. package/dist/react/index.js.map +1 -1
  46. package/dist/recorder-CDPPTJMG.js +31 -0
  47. package/dist/recorder-CDPPTJMG.js.map +1 -0
  48. package/dist/{renderer-C2P1qGzP.d.ts → renderer-teaa98RE.d.ts} +1 -1
  49. package/dist/svelte/index.d.ts +33 -4
  50. package/dist/svelte/index.js +51 -9
  51. package/dist/svelte/index.js.map +1 -1
  52. package/dist/{types-DLkbOX8d.d.ts → types-DUOMxZaq.d.ts} +1 -1
  53. package/dist/vector/index.d.ts +52 -1
  54. package/dist/vector/index.js +8 -4
  55. package/package.json +5 -1
  56. package/dist/chunk-4F4KL4KU.js.map +0 -1
  57. package/dist/chunk-ARKC7LDW.js.map +0 -1
  58. package/dist/chunk-LVPIIWPP.js.map +0 -1
  59. package/dist/chunk-NGKNI3QX.js.map +0 -1
  60. package/dist/chunk-UP6QQVRS.js +0 -331
  61. package/dist/chunk-UP6QQVRS.js.map +0 -1
  62. package/dist/recorder-HKFFHQES.js +0 -10
  63. /package/dist/{chunk-VP6IEWIK.js.map → chunk-H6L67KHI.js.map} +0 -0
  64. /package/dist/{recorder-HKFFHQES.js.map → effect-26CPF5HS.js.map} +0 -0
package/dist/node.d.ts CHANGED
@@ -1,24 +1,184 @@
1
1
  import { C as ControlValue } from './controls-Ec6P0Vpy.js';
2
+ import { S as SvgMotion } from './svg-BhRboN9q.js';
3
+ import { E as EffectDefinition, V as VectorEffect } from './types-DUOMxZaq.js';
2
4
 
3
- /** Finds a local Chrome or Chromium executable, or `null`. */
5
+ /**
6
+ * Finds a local Chrome or Chromium executable, or `null`: `CHROME_PATH` (or
7
+ * `CHROME_BIN`), then the usual install places, then a Chromium Playwright
8
+ * downloaded.
9
+ */
4
10
  declare function findChrome(): string | null;
11
+ /** The ffmpeg to run: `FFMPEG_PATH`, else `ffmpeg` on the `PATH`. */
12
+ declare function ffmpegPath(): string;
13
+ /** A file type the exporter writes, from the output path. */
14
+ type OutputFormat = 'mp4' | 'webm' | 'mov' | 'gif' | 'png' | 'svg';
15
+ /** What to write and how. */
16
+ interface OutputPlan {
17
+ format: OutputFormat;
18
+ /** The output path, resolved. For a PNG sequence, a printf pattern (`…/frame_%04d.png`). */
19
+ path: string;
20
+ /** Whether the frames keep their alpha. */
21
+ alpha: boolean;
22
+ /** ffmpeg's arguments after the input (for mp4, webm and mov). */
23
+ ffmpegArgs?: string[];
24
+ }
25
+ /**
26
+ * Works out the format from `outputPath`'s extension and what alpha it can
27
+ * keep. A path with no extension, or ending in a slash, is a folder of PNG
28
+ * frames; a `.png` path with a `%d` pattern (`out_%04d.png`) names each
29
+ * frame, and one without gets `_%04d` added (unless there is one frame).
30
+ * Throws a sentence for formats it cannot write, and for alpha in MP4.
31
+ */
32
+ declare function planOutput(outputPath: string, options?: {
33
+ alpha?: boolean;
34
+ frames?: number;
35
+ }): OutputPlan;
36
+ /** Where the pointer is in a recording: nowhere (`none`), or the scripted pointer of {@link scriptedPointer} (`auto`). */
37
+ type PointerMode = 'none' | 'auto';
5
38
  interface ExportOptions {
6
- /** GLSL fragment shader source */
39
+ /** GLSL fragment shader source (Shadertoy-style `mainImage`). */
7
40
  source: string;
8
- /** Uniform values override */
41
+ /**
42
+ * Control values over the declared defaults. An image control takes a
43
+ * URL, a `data:` URL, or a local file path (read into a `data:` URL).
44
+ */
9
45
  values?: Record<string, ControlValue>;
10
- /** Destination file path (.gif or .mp4) */
46
+ /** Destination: .mp4, .webm, .mov, .gif, or PNG frames (a folder or `out_%04d.png`). */
11
47
  outputPath: string;
12
48
  width?: number;
13
49
  height?: number;
14
50
  fps?: number;
51
+ /** Seconds (default 3). */
15
52
  duration?: number;
53
+ /** Keep the shader's alpha (WebM, MOV, GIF and PNG; MP4 is always opaque). */
54
+ alpha?: boolean;
55
+ /**
56
+ * The shader draws over other content (`fragColor.a` is coverage): without
57
+ * `alpha` it is composited over `background`. Otherwise its alpha is
58
+ * ignored without `alpha`, as on an opaque canvas.
59
+ */
60
+ transparent?: boolean;
61
+ /** CSS colour under transparent pixels when `alpha` is off (default black). */
62
+ background?: string;
63
+ /** The pointer: `none` (default) or `auto`, a scripted pointer that hovers, wanders and clicks. */
64
+ pointer?: PointerMode;
16
65
  onProgress?: (frame: number, total: number) => void;
17
66
  }
18
67
  /**
19
- * Renders a shader animation frame-by-frame via hardware-accelerated WebGL2 in
20
- * headless Chrome and encodes to an animated GIF or MP4 video using ffmpeg.
68
+ * Renders a shader animation frame by frame with WebGL2 in headless Chrome,
69
+ * the way `ShaderRenderer` draws it (the same GLSL wrapper and built-ins,
70
+ * images, int and bool controls), and writes MP4 (H.264), WebM (VP9, with
71
+ * alpha when asked), MOV (ProRes 4444), GIF or PNG frames. MP4, WebM and
72
+ * MOV need ffmpeg; GIF and PNG do not.
21
73
  */
22
74
  declare function exportMotionGraphic(options: ExportOptions): Promise<string>;
23
75
 
24
- export { type ExportOptions, exportMotionGraphic, findChrome };
76
+ /**
77
+ * Renders a catalog effect, applied to a subject, to a file in Node: video
78
+ * (MP4, WebM, MOV), GIF, PNG frames, or (vector effects) an animated SVG.
79
+ */
80
+
81
+ /**
82
+ * What an effect is applied to, in Node:
83
+ * - `{ image }`: a picture's file path or URL (or a `data:` URL).
84
+ * - `{ svg }`: an SVG's file path, URL, or markup (`"<svg …>"`).
85
+ * - `{ text }`: words, for a vector effect's text, or drawn with `textImage` for a shader's text or mask.
86
+ * - `{ path }`: SVG path data (`d`), for a shape control.
87
+ * - `{ video }`: a video file's path, decoded to frames with ffmpeg and shown one per frame.
88
+ */
89
+ type NodeSubject = {
90
+ image: string;
91
+ } | {
92
+ svg: string;
93
+ } | {
94
+ text: string;
95
+ font?: string;
96
+ weight?: number | string;
97
+ } | {
98
+ path: string;
99
+ } | {
100
+ video: string;
101
+ };
102
+ interface EffectExportOptions {
103
+ /** What to apply it to; several fill several controls in order (a transition's from and to). */
104
+ subject?: NodeSubject | readonly NodeSubject[];
105
+ /** A preset by name, under `values`. */
106
+ preset?: string;
107
+ /** Control values over the defaults (and the subject). An image control also takes a file path. */
108
+ values?: Record<string, ControlValue>;
109
+ /** .mp4, .webm, .mov, .gif, .svg (vector effects), or PNG frames (a folder, or `out_%04d.png`). */
110
+ outputPath: string;
111
+ /** Pixels (default: 1280 × 720 for effects that show a picture, 720 × 720 for other shaders, a vector's own shape 720 px on its long edge). */
112
+ width?: number;
113
+ height?: number;
114
+ fps?: number;
115
+ /**
116
+ * Seconds. Default: a shader's loop (when it loops in 12 s or less), a
117
+ * video subject's length (up to 10 s), else 3; a vector effect's one
118
+ * play-through (with its trigger acted out).
119
+ */
120
+ duration?: number;
121
+ /** Keep transparency (WebM, MOV, GIF, PNG and SVG; MP4 is always opaque). */
122
+ alpha?: boolean;
123
+ /** CSS colour under transparent pixels without `alpha` (default black for shaders, white for vector effects). */
124
+ background?: string;
125
+ /**
126
+ * The pointer, for effects that answer it: `auto` (the default for them),
127
+ * a scripted pointer that hovers, wanders and clicks; `none` leaves it
128
+ * still. Vector effects with a trigger have it acted out (clicked through
129
+ * their steps, or hovered and let go).
130
+ */
131
+ pointer?: PointerMode;
132
+ onProgress?: (frame: number, total: number) => void;
133
+ }
134
+ /** What a Node subject becomes: control values, and video files by image control. */
135
+ interface NodeSubjectValues {
136
+ values: Record<string, ControlValue>;
137
+ /** Image control → video file. */
138
+ video: Record<string, string>;
139
+ }
140
+ /**
141
+ * The control values that put Node subjects in `effect` (see
142
+ * `subjectValues` in `@pixerate/flair/embed`, which picks the controls):
143
+ * image files and URLs become `data:` URLs, SVG files and URLs are read as
144
+ * markup, and a video is kept as its file for its control, to be decoded to
145
+ * frames. Throws a sentence when the effect takes no such thing.
146
+ */
147
+ declare function nodeSubjectValues(effect: EffectDefinition, subject: NodeSubject | readonly NodeSubject[]): Promise<NodeSubjectValues>;
148
+ /** How a vector effect plays over time in a recording: the frame at each ms, and its loop. */
149
+ interface VectorTimeline {
150
+ /** One play-through, ms. */
151
+ length: number;
152
+ /** Step start times (ms) for the whole loop, for an animated SVG. */
153
+ steps: number[];
154
+ /** The picture's time and steps at `ms` into the recording. */
155
+ at(ms: number): {
156
+ time: number;
157
+ steps: number[];
158
+ };
159
+ }
160
+ /**
161
+ * A vector effect's timeline: a loop plays and rests `hold` ms; a hover or
162
+ * press trigger plays to the end, rests, and plays back; a click trigger is
163
+ * clicked through each of its steps (or, with none, clicked again after
164
+ * each play-through).
165
+ */
166
+ declare function vectorTimeline(effect: Pick<VectorEffect, 'hold' | 'trigger'>, motion: SvgMotion): VectorTimeline;
167
+ /**
168
+ * Renders a catalog effect (by id or definition), applied to `subject`, to
169
+ * a file: a shader through WebGL2 in headless Chrome, a vector effect by
170
+ * drawing each frame's SVG there, or (to `.svg`) a vector effect as one
171
+ * animated SVG with no browser at all. MP4, WebM, MOV and video subjects
172
+ * need ffmpeg. Throws a sentence when the effect is unknown, takes no such
173
+ * subject, or cannot be written in that format.
174
+ *
175
+ * ```ts
176
+ * await exportEffect("image-duotone", { subject: { image: "team.jpg" }, preset: "Spotify", outputPath: "team.mp4" });
177
+ * await exportEffect("click-ripple", { alpha: true, outputPath: "ripple.webm" });
178
+ * await exportEffect("typewriter", { subject: { text: "Hello" }, outputPath: "hello.gif" });
179
+ * await exportEffect("toggle-switch", { outputPath: "toggle.svg" });
180
+ * ```
181
+ */
182
+ declare function exportEffect(effect: string | EffectDefinition, options: EffectExportOptions): Promise<string>;
183
+
184
+ export { type EffectExportOptions, type ExportOptions, type NodeSubject, type NodeSubjectValues, type OutputFormat, type OutputPlan, type PointerMode, exportEffect, exportMotionGraphic, ffmpegPath, findChrome, nodeSubjectValues, planOutput, vectorTimeline };
package/dist/node.js CHANGED
@@ -1,10 +1,32 @@
1
+ import {
2
+ exportEffect,
3
+ nodeSubjectValues,
4
+ vectorTimeline
5
+ } from "./chunk-OKYMKSXK.js";
1
6
  import {
2
7
  exportMotionGraphic,
3
- findChrome
4
- } from "./chunk-UP6QQVRS.js";
8
+ ffmpegPath,
9
+ findChrome,
10
+ planOutput
11
+ } from "./chunk-JL6TI6II.js";
12
+ import "./chunk-YBGRXVAE.js";
13
+ import "./chunk-STKEKXCK.js";
14
+ import "./chunk-S2RGHHKZ.js";
15
+ import "./chunk-H6VDFBIO.js";
16
+ import "./chunk-RDU63CDO.js";
17
+ import "./chunk-PYDN5G2P.js";
18
+ import "./chunk-EHDJ3CPJ.js";
5
19
  import "./chunk-VYOMXE6I.js";
20
+ import "./chunk-PFGGPIMT.js";
21
+ import "./chunk-WMZQCUFJ.js";
22
+ import "./chunk-ORAFCHX3.js";
6
23
  export {
24
+ exportEffect,
7
25
  exportMotionGraphic,
8
- findChrome
26
+ ffmpegPath,
27
+ findChrome,
28
+ nodeSubjectValues,
29
+ planOutput,
30
+ vectorTimeline
9
31
  };
10
32
  //# sourceMappingURL=node.js.map
@@ -1,8 +1,11 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes } from 'react';
3
- import { V as VectorEffect$1 } from '../types-DLkbOX8d.js';
4
3
  import { C as ControlValue } from '../controls-Ec6P0Vpy.js';
5
4
  import { S as SvgMotion, a as SvgTrigger, C as Clock, b as SvgPlayer } from '../svg-BhRboN9q.js';
5
+ import { E as EffectDefinition, V as VectorEffect$1 } from '../types-DUOMxZaq.js';
6
+ import { EffectSubject } from '../embed/index.js';
7
+ import '../renderer-teaa98RE.js';
8
+ import '../frames-VEcqa40g.js';
6
9
 
7
10
  /** When to leave the still in place of the animation. */
8
11
  type ReducedMotion = "user" | "always" | "never";
@@ -90,4 +93,32 @@ interface VectorEffectProps extends Omit<SvgPlayerViewProps, "motion" | "id"> {
90
93
  */
91
94
  declare function VectorEffect(props: VectorEffectProps): react.JSX.Element;
92
95
 
93
- export { type ReducedMotion, SvgMotion, SvgPlayer, SvgPlayerView, type SvgPlayerViewProps, SvgTrigger, VectorEffect, type VectorEffectProps };
96
+ interface FlairEffectProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
97
+ /** A catalog effect, by id or definition: shader or vector. */
98
+ effect: string | EffectDefinition;
99
+ /** What to apply it to: a picture, a video, an SVG, words or a path (see `EffectSubject`). */
100
+ subject?: EffectSubject | readonly EffectSubject[];
101
+ /** A preset by name, under `values`. */
102
+ preset?: string;
103
+ /** Control values over the effect's defaults (and preset). */
104
+ values?: Record<string, ControlValue>;
105
+ /** Start playing at once (default `true`). */
106
+ autoplay?: boolean;
107
+ /** Pause while scrolled out of view (default `true`). */
108
+ pauseWhenHidden?: boolean;
109
+ }
110
+ /**
111
+ * Any catalog effect, shader or vector, applied to a subject: a picture, a
112
+ * video, an SVG, words or a path (see `mountEffect` in
113
+ * `@pixerate/flair/embed`). Fills its element: size it with CSS. A new
114
+ * subject or values update the running effect; a new effect, preset or
115
+ * playback option starts it again. It renders nothing on the server.
116
+ *
117
+ * ```tsx
118
+ * <FlairEffect effect="image-duotone" subject={{ image: "/team.jpg" }} preset="Spotify" style={{ aspectRatio: "16 / 9" }} />
119
+ * <FlairEffect effect="typewriter" subject={{ text: "Hello there" }} />
120
+ * ```
121
+ */
122
+ declare function FlairEffect(props: FlairEffectProps): react.JSX.Element;
123
+
124
+ export { FlairEffect, type FlairEffectProps, type ReducedMotion, SvgMotion, SvgPlayer, SvgPlayerView, type SvgPlayerViewProps, SvgTrigger, VectorEffect, type VectorEffectProps };
@@ -1,20 +1,25 @@
1
- import {
2
- effectDefaults,
3
- getEffect,
4
- renderVectorEffect
5
- } from "../chunk-4F4KL4KU.js";
6
1
  import {
7
2
  svgFrameMarkup,
8
3
  svgInlineLayout,
9
4
  svgViewBox
10
- } from "../chunk-LVPIIWPP.js";
5
+ } from "../chunk-STKEKXCK.js";
6
+ import {
7
+ mountEffect
8
+ } from "../chunk-S2RGHHKZ.js";
9
+ import "../chunk-H6VDFBIO.js";
10
+ import "../chunk-RDU63CDO.js";
11
+ import {
12
+ effectDefaults,
13
+ getEffect,
14
+ renderVectorEffect
15
+ } from "../chunk-PYDN5G2P.js";
16
+ import "../chunk-EHDJ3CPJ.js";
17
+ import "../chunk-VYOMXE6I.js";
11
18
  import {
12
19
  SvgPlayer
13
20
  } from "../chunk-PFGGPIMT.js";
14
- import "../chunk-OHW7UG6D.js";
15
- import "../chunk-EHDJ3CPJ.js";
16
21
  import "../chunk-WMZQCUFJ.js";
17
- import "../chunk-VYOMXE6I.js";
22
+ import "../chunk-ORAFCHX3.js";
18
23
 
19
24
  // src/react/index.tsx
20
25
  import {
@@ -134,7 +139,47 @@ function VectorEffect(props) {
134
139
  }
135
140
  );
136
141
  }
142
+ function FlairEffect(props) {
143
+ const { effect, subject, preset, values, autoplay, pauseWhenHidden, style, ...rest } = props;
144
+ const host = useRef(null);
145
+ const mounted = useRef(void 0);
146
+ const valuesKey = JSON.stringify(values ?? {});
147
+ const applied = useRef({ valuesKey: "" });
148
+ const latest = useRef({ subject, values, valuesKey });
149
+ latest.current = { subject, values, valuesKey };
150
+ useEffect(() => {
151
+ const now = latest.current;
152
+ const m = mountEffect(host.current, effect, {
153
+ ...now.subject ? { subject: now.subject } : {},
154
+ ...preset ? { preset } : {},
155
+ ...now.values ? { values: now.values } : {},
156
+ autoplay: autoplay ?? true,
157
+ pauseWhenHidden: pauseWhenHidden ?? true
158
+ });
159
+ m.element.style.display = "block";
160
+ m.element.style.width = "100%";
161
+ m.element.style.height = "100%";
162
+ mounted.current = m;
163
+ applied.current = { subject: now.subject, valuesKey: now.valuesKey };
164
+ return () => {
165
+ m.dispose();
166
+ mounted.current = void 0;
167
+ };
168
+ }, [effect, preset, autoplay, pauseWhenHidden]);
169
+ useEffect(() => {
170
+ if (!mounted.current || !subject || subject === applied.current.subject) return;
171
+ mounted.current.setSubject(subject);
172
+ applied.current.subject = subject;
173
+ }, [subject]);
174
+ useEffect(() => {
175
+ if (!mounted.current || !values || valuesKey === applied.current.valuesKey) return;
176
+ mounted.current.setValues(values);
177
+ applied.current.valuesKey = valuesKey;
178
+ }, [valuesKey]);
179
+ return /* @__PURE__ */ jsx("div", { ref: host, style: { position: "relative", ...style }, ...rest });
180
+ }
137
181
  export {
182
+ FlairEffect,
138
183
  SvgPlayerView,
139
184
  VectorEffect
140
185
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/index.tsx"],"sourcesContent":["/**\n * React components for Flair's vector effects: `<VectorEffect>` plays a\n * catalog effect by id, `<SvgPlayerView>` plays any {@link SvgMotion}.\n *\n * Both render a still of the picture on the server (its last frame, by\n * default), so a page hydrates with the picture in place rather than a blank\n * box that jumps; the live `SvgPlayer` takes over after mount and is disposed\n * on unmount. Under `prefers-reduced-motion: reduce` the still stays.\n *\n * Needs React 18 or later (an optional peer dependency of the package).\n *\n * @packageDocumentation\n */\n\nimport {\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n type CSSProperties,\n type HTMLAttributes,\n} from \"react\";\nimport { getEffect, effectDefaults } from \"../catalog/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// Layout effects swap the still for the player before paint; on a server neither runs.\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\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/** When to leave the still in place of the animation. */\nexport type ReducedMotion = \"user\" | \"always\" | \"never\";\n\nexport interface SvgPlayerViewProps extends Omit<HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** The picture and its animations. */\n motion: SvgMotion;\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). */\n speed?: number;\n /** Ms to rest on the last frame before looping (default 600). */\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`). */\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 (server render, hydration) and\n * instead of it under reduced motion: `\"last\"` (default; the rest frame,\n * `\"first\"` for a hover or press 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). Meant for pictures framed from cap\n * height to baseline, like drip text's `inline`. Otherwise it is a block\n * as wide as its container with the 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\nfunction prefersReducedMotion(): boolean {\n return typeof matchMedia === \"function\" && matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/** The host's size 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 | undefined, style?: CSSProperties): CSSProperties {\n const box = svgViewBox(svg);\n const aspectRatio = box ? `${box[2]} / ${box[3]}` : undefined;\n const spill = inline && contain ? svgInlineLayout(svg)?.overhang : undefined;\n const base: CSSProperties = inline\n ? {\n display: \"inline-block\",\n height: typeof inline === \"string\" ? inline : CAP_HEIGHT,\n aspectRatio,\n ...(spill && { margin: `${spill.top}em ${spill.right}em ${spill.bottom}em ${spill.left}em` }),\n }\n : { display: \"block\", aspectRatio };\n return { position: \"relative\", ...base, ...style };\n}\n\nconst LAYER: CSSProperties = { position: \"absolute\", inset: 0, display: \"block\" };\n\n/**\n * Plays an {@link SvgMotion}: a still on the server and before mount, then\n * an `SvgPlayer` (remade when the motion or its options change, disposed on\n * unmount). Pass a memoised `motion`: a new object each render restarts it.\n */\nexport function SvgPlayerView(props: SvgPlayerViewProps) {\n const {\n motion,\n loop,\n autoplay,\n speed,\n hold,\n duration,\n steps,\n trigger,\n pauseWhenHidden,\n playOnEnter,\n hideWhenPaused,\n clock,\n onPick,\n onPlayer,\n poster,\n reducedMotion = \"user\",\n inline,\n contain,\n style,\n ...rest\n } = props;\n const still = useRef<HTMLSpanElement>(null);\n const mount = useRef<HTMLSpanElement>(null);\n const player = useRef<SvgPlayer | undefined>(undefined);\n // Callbacks change every render; the player calls the latest without being remade.\n const callbacks = useRef({ onPick, onPlayer });\n callbacks.current = { onPick, onPlayer };\n\n const at = poster ?? (trigger === \"hover\" || trigger === \"press\" ? \"first\" : \"last\");\n const markup = useMemo(() => svgFrameMarkup(motion, { at, steps }) ?? \"\", [motion, at, steps]);\n const stepsKey = steps?.join(\",\");\n // A one-shot waits until it is seen.\n const enter = playOnEnter ?? (loop === false && !trigger);\n\n useIsomorphicLayoutEffect(() => {\n const host = mount.current;\n if (!host) return;\n if (reducedMotion === \"always\" || (reducedMotion === \"user\" && prefersReducedMotion())) return;\n const p = new SvgPlayer(host, motion, {\n loop,\n autoplay,\n speed,\n hold,\n duration,\n steps,\n trigger,\n pauseWhenHidden,\n playOnEnter: enter,\n hideWhenPaused,\n clock,\n onPick: (id) => callbacks.current.onPick?.(id),\n });\n player.current = p;\n // Set directly, not through props, so React leaves it be on later renders.\n if (still.current) still.current.style.display = \"none\";\n callbacks.current.onPlayer?.(p);\n return () => {\n p.dispose();\n player.current = undefined;\n if (still.current) still.current.style.display = \"block\";\n callbacks.current.onPlayer?.(undefined);\n };\n // `speed` is applied below without remaking; `steps` by value.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [motion, loop, autoplay, hold, duration, stepsKey, trigger, pauseWhenHidden, enter, hideWhenPaused, clock, reducedMotion]);\n\n useEffect(() => {\n player.current?.setSpeed(speed ?? 1);\n }, [speed]);\n\n return (\n <span {...rest} style={hostStyle(motion.svg, inline, contain, style)}>\n <span ref={still} aria-hidden=\"true\" style={LAYER} dangerouslySetInnerHTML={{ __html: markup }} />\n <span ref={mount} style={LAYER} />\n </span>\n );\n}\n\nexport interface VectorEffectProps extends Omit<SvgPlayerViewProps, \"motion\" | \"id\"> {\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. */\n values?: Record<string, ControlValue>;\n /** A preset's name to start from. */\n preset?: string;\n /** The host element's own `id`. */\n elementId?: string;\n}\n\n/**\n * Plays a catalog vector effect: `<VectorEffect id=\"drip-text\" values={{ word: \"Slop\" }} loop={false} />`.\n * Its `hold` and `trigger` are used unless given. With an `inline` control\n * set (drip text's), it is sized as a word in the line (see `inline`).\n * Throws when `id` is not a vector effect.\n */\nexport function VectorEffect(props: VectorEffectProps) {\n const { id, values, preset, elementId, ...rest } = props;\n const effect = typeof id === \"string\" ? getEffect(id) : id;\n if (!effect || effect.kind !== \"vector\")\n throw new Error(`\"${typeof id === \"string\" ? id : effect?.id}\" is not a vector effect; pick one from findEffects({ kind: \"vector\" })`);\n const valuesKey = JSON.stringify(values ?? {});\n const motion = useMemo(\n () => renderVectorEffect(effect, effectDefaults(effect, { preset, values })),\n // Values by content, so an inline object literal does not restart it.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [effect, preset, valuesKey],\n );\n return (\n <SvgPlayerView\n id={elementId}\n motion={motion}\n hold={effect.hold}\n trigger={effect.trigger}\n inline={values?.inline === true}\n {...rest}\n />\n );\n}\n\nexport type { SvgPlayer, SvgMotion, SvgTrigger };\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/index.tsx"],"sourcesContent":["/**\n * React components for Flair: `<FlairEffect>` runs any catalog effect (shader\n * or vector) applied to a picture, video, SVG or words; `<VectorEffect>` plays\n * a catalog vector effect by id, `<SvgPlayerView>` plays any {@link SvgMotion}.\n *\n * Both render a still of the picture on the server (its last frame, by\n * default), so a page hydrates with the picture in place rather than a blank\n * box that jumps; the live `SvgPlayer` takes over after mount and is disposed\n * on unmount. Under `prefers-reduced-motion: reduce` the still stays.\n *\n * Needs React 18 or later (an optional peer dependency of the package).\n *\n * @packageDocumentation\n */\n\nimport {\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n type CSSProperties,\n type HTMLAttributes,\n} from \"react\";\nimport { getEffect, effectDefaults, type EffectDefinition } from \"../catalog/index.js\";\nimport { mountEffect, type EffectSubject, type MountedEffect } 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// Layout effects swap the still for the player before paint; on a server neither runs.\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\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/** When to leave the still in place of the animation. */\nexport type ReducedMotion = \"user\" | \"always\" | \"never\";\n\nexport interface SvgPlayerViewProps extends Omit<HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** The picture and its animations. */\n motion: SvgMotion;\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). */\n speed?: number;\n /** Ms to rest on the last frame before looping (default 600). */\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`). */\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 (server render, hydration) and\n * instead of it under reduced motion: `\"last\"` (default; the rest frame,\n * `\"first\"` for a hover or press 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). Meant for pictures framed from cap\n * height to baseline, like drip text's `inline`. Otherwise it is a block\n * as wide as its container with the 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\nfunction prefersReducedMotion(): boolean {\n return typeof matchMedia === \"function\" && matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/** The host's size 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 | undefined, style?: CSSProperties): CSSProperties {\n const box = svgViewBox(svg);\n const aspectRatio = box ? `${box[2]} / ${box[3]}` : undefined;\n const spill = inline && contain ? svgInlineLayout(svg)?.overhang : undefined;\n const base: CSSProperties = inline\n ? {\n display: \"inline-block\",\n height: typeof inline === \"string\" ? inline : CAP_HEIGHT,\n aspectRatio,\n ...(spill && { margin: `${spill.top}em ${spill.right}em ${spill.bottom}em ${spill.left}em` }),\n }\n : { display: \"block\", aspectRatio };\n return { position: \"relative\", ...base, ...style };\n}\n\nconst LAYER: CSSProperties = { position: \"absolute\", inset: 0, display: \"block\" };\n\n/**\n * Plays an {@link SvgMotion}: a still on the server and before mount, then\n * an `SvgPlayer` (remade when the motion or its options change, disposed on\n * unmount). Pass a memoised `motion`: a new object each render restarts it.\n */\nexport function SvgPlayerView(props: SvgPlayerViewProps) {\n const {\n motion,\n loop,\n autoplay,\n speed,\n hold,\n duration,\n steps,\n trigger,\n pauseWhenHidden,\n playOnEnter,\n hideWhenPaused,\n clock,\n onPick,\n onPlayer,\n poster,\n reducedMotion = \"user\",\n inline,\n contain,\n style,\n ...rest\n } = props;\n const still = useRef<HTMLSpanElement>(null);\n const mount = useRef<HTMLSpanElement>(null);\n const player = useRef<SvgPlayer | undefined>(undefined);\n // Callbacks change every render; the player calls the latest without being remade.\n const callbacks = useRef({ onPick, onPlayer });\n callbacks.current = { onPick, onPlayer };\n\n const at = poster ?? (trigger === \"hover\" || trigger === \"press\" ? \"first\" : \"last\");\n const markup = useMemo(() => svgFrameMarkup(motion, { at, steps }) ?? \"\", [motion, at, steps]);\n const stepsKey = steps?.join(\",\");\n // A one-shot waits until it is seen.\n const enter = playOnEnter ?? (loop === false && !trigger);\n\n useIsomorphicLayoutEffect(() => {\n const host = mount.current;\n if (!host) return;\n if (reducedMotion === \"always\" || (reducedMotion === \"user\" && prefersReducedMotion())) return;\n const p = new SvgPlayer(host, motion, {\n loop,\n autoplay,\n speed,\n hold,\n duration,\n steps,\n trigger,\n pauseWhenHidden,\n playOnEnter: enter,\n hideWhenPaused,\n clock,\n onPick: (id) => callbacks.current.onPick?.(id),\n });\n player.current = p;\n // Set directly, not through props, so React leaves it be on later renders.\n if (still.current) still.current.style.display = \"none\";\n callbacks.current.onPlayer?.(p);\n return () => {\n p.dispose();\n player.current = undefined;\n if (still.current) still.current.style.display = \"block\";\n callbacks.current.onPlayer?.(undefined);\n };\n // `speed` is applied below without remaking; `steps` by value.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [motion, loop, autoplay, hold, duration, stepsKey, trigger, pauseWhenHidden, enter, hideWhenPaused, clock, reducedMotion]);\n\n useEffect(() => {\n player.current?.setSpeed(speed ?? 1);\n }, [speed]);\n\n return (\n <span {...rest} style={hostStyle(motion.svg, inline, contain, style)}>\n <span ref={still} aria-hidden=\"true\" style={LAYER} dangerouslySetInnerHTML={{ __html: markup }} />\n <span ref={mount} style={LAYER} />\n </span>\n );\n}\n\nexport interface VectorEffectProps extends Omit<SvgPlayerViewProps, \"motion\" | \"id\"> {\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. */\n values?: Record<string, ControlValue>;\n /** A preset's name to start from. */\n preset?: string;\n /** The host element's own `id`. */\n elementId?: string;\n}\n\n/**\n * Plays a catalog vector effect: `<VectorEffect id=\"drip-text\" values={{ word: \"Slop\" }} loop={false} />`.\n * Its `hold` and `trigger` are used unless given. With an `inline` control\n * set (drip text's), it is sized as a word in the line (see `inline`).\n * Throws when `id` is not a vector effect.\n */\nexport function VectorEffect(props: VectorEffectProps) {\n const { id, values, preset, elementId, ...rest } = props;\n const effect = typeof id === \"string\" ? getEffect(id) : id;\n if (!effect || effect.kind !== \"vector\")\n throw new Error(`\"${typeof id === \"string\" ? id : effect?.id}\" is not a vector effect; pick one from findEffects({ kind: \"vector\" })`);\n const valuesKey = JSON.stringify(values ?? {});\n const motion = useMemo(\n () => renderVectorEffect(effect, effectDefaults(effect, { preset, values })),\n // Values by content, so an inline object literal does not restart it.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [effect, preset, valuesKey],\n );\n return (\n <SvgPlayerView\n id={elementId}\n motion={motion}\n hold={effect.hold}\n trigger={effect.trigger}\n inline={values?.inline === true}\n {...rest}\n />\n );\n}\n\nexport type { SvgPlayer, SvgMotion, SvgTrigger };\n\nexport interface FlairEffectProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\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 /** A preset by name, under `values`. */\n preset?: string;\n /** Control values over the effect's defaults (and preset). */\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 * Any catalog effect, shader or vector, applied to a subject: a picture, a\n * video, an SVG, words or a path (see `mountEffect` in\n * `@pixerate/flair/embed`). Fills its element: size it with CSS. A new\n * subject or values update the running effect; a new effect, preset or\n * playback option starts it again. It renders nothing on the server.\n *\n * ```tsx\n * <FlairEffect effect=\"image-duotone\" subject={{ image: \"/team.jpg\" }} preset=\"Spotify\" style={{ aspectRatio: \"16 / 9\" }} />\n * <FlairEffect effect=\"typewriter\" subject={{ text: \"Hello there\" }} />\n * ```\n */\nexport function FlairEffect(props: FlairEffectProps) {\n const { effect, subject, preset, values, autoplay, pauseWhenHidden, style, ...rest } = props;\n const host = useRef<HTMLDivElement>(null);\n const mounted = useRef<MountedEffect | undefined>(undefined);\n const valuesKey = JSON.stringify(values ?? {});\n // What the running effect last took, so a re-render with the same subject or values changes nothing.\n const applied = useRef<{ subject?: FlairEffectProps[\"subject\"]; valuesKey: string }>({ valuesKey: \"\" });\n const latest = useRef({ subject, values, valuesKey });\n latest.current = { subject, values, valuesKey };\n useEffect(() => {\n const now = latest.current;\n const m = mountEffect(host.current!, effect, {\n ...(now.subject ? { subject: now.subject } : {}),\n ...(preset ? { preset } : {}),\n ...(now.values ? { values: now.values } : {}),\n autoplay: autoplay ?? true,\n pauseWhenHidden: pauseWhenHidden ?? true,\n });\n m.element.style.display = \"block\";\n m.element.style.width = \"100%\";\n m.element.style.height = \"100%\";\n mounted.current = m;\n applied.current = { subject: now.subject, valuesKey: now.valuesKey };\n return () => {\n m.dispose();\n mounted.current = undefined;\n };\n }, [effect, preset, autoplay, pauseWhenHidden]);\n useEffect(() => {\n if (!mounted.current || !subject || subject === applied.current.subject) return;\n mounted.current.setSubject(subject);\n applied.current.subject = subject;\n }, [subject]);\n useEffect(() => {\n if (!mounted.current || !values || valuesKey === applied.current.valuesKey) return;\n mounted.current.setValues(values);\n applied.current.valuesKey = valuesKey;\n }, [valuesKey]);\n return <div ref={host} style={{ position: \"relative\", ...style }} {...rest} />;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAeA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAoLH,SACE,KADF;AAzKJ,IAAM,4BAA4B,OAAO,WAAW,cAAc,YAAY;AAG9E,IAAM,aAAa;AAkEnB,SAAS,uBAAgC;AACvC,SAAO,OAAO,eAAe,cAAc,WAAW,kCAAkC,EAAE;AAC5F;AAGA,SAAS,UAAU,KAAa,QAAsC,SAA8B,OAAsC;AACxI,QAAM,MAAM,WAAW,GAAG;AAC1B,QAAM,cAAc,MAAM,GAAG,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK;AACpD,QAAM,QAAQ,UAAU,UAAU,gBAAgB,GAAG,GAAG,WAAW;AACnE,QAAM,OAAsB,SACxB;AAAA,IACE,SAAS;AAAA,IACT,QAAQ,OAAO,WAAW,WAAW,SAAS;AAAA,IAC9C;AAAA,IACA,GAAI,SAAS,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,MAAM,KAAK,MAAM,MAAM,MAAM,MAAM,MAAM,IAAI,KAAK;AAAA,EAC7F,IACA,EAAE,SAAS,SAAS,YAAY;AACpC,SAAO,EAAE,UAAU,YAAY,GAAG,MAAM,GAAG,MAAM;AACnD;AAEA,IAAM,QAAuB,EAAE,UAAU,YAAY,OAAO,GAAG,SAAS,QAAQ;AAOzE,SAAS,cAAc,OAA2B;AACvD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AACJ,QAAM,QAAQ,OAAwB,IAAI;AAC1C,QAAM,QAAQ,OAAwB,IAAI;AAC1C,QAAM,SAAS,OAA8B,MAAS;AAEtD,QAAM,YAAY,OAAO,EAAE,QAAQ,SAAS,CAAC;AAC7C,YAAU,UAAU,EAAE,QAAQ,SAAS;AAEvC,QAAM,KAAK,WAAW,YAAY,WAAW,YAAY,UAAU,UAAU;AAC7E,QAAM,SAAS,QAAQ,MAAM,eAAe,QAAQ,EAAE,IAAI,MAAM,CAAC,KAAK,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC;AAC7F,QAAM,WAAW,OAAO,KAAK,GAAG;AAEhC,QAAM,QAAQ,gBAAgB,SAAS,SAAS,CAAC;AAEjD,4BAA0B,MAAM;AAC9B,UAAM,OAAO,MAAM;AACnB,QAAI,CAAC,KAAM;AACX,QAAI,kBAAkB,YAAa,kBAAkB,UAAU,qBAAqB,EAAI;AACxF,UAAM,IAAI,IAAI,UAAU,MAAM,QAAQ;AAAA,MACpC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa;AAAA,MACb;AAAA,MACA;AAAA,MACA,QAAQ,CAAC,OAAO,UAAU,QAAQ,SAAS,EAAE;AAAA,IAC/C,CAAC;AACD,WAAO,UAAU;AAEjB,QAAI,MAAM,QAAS,OAAM,QAAQ,MAAM,UAAU;AACjD,cAAU,QAAQ,WAAW,CAAC;AAC9B,WAAO,MAAM;AACX,QAAE,QAAQ;AACV,aAAO,UAAU;AACjB,UAAI,MAAM,QAAS,OAAM,QAAQ,MAAM,UAAU;AACjD,gBAAU,QAAQ,WAAW,MAAS;AAAA,IACxC;AAAA,EAGF,GAAG,CAAC,QAAQ,MAAM,UAAU,MAAM,UAAU,UAAU,SAAS,iBAAiB,OAAO,gBAAgB,OAAO,aAAa,CAAC;AAE5H,YAAU,MAAM;AACd,WAAO,SAAS,SAAS,SAAS,CAAC;AAAA,EACrC,GAAG,CAAC,KAAK,CAAC;AAEV,SACE,qBAAC,UAAM,GAAG,MAAM,OAAO,UAAU,OAAO,KAAK,QAAQ,SAAS,KAAK,GACjE;AAAA,wBAAC,UAAK,KAAK,OAAO,eAAY,QAAO,OAAO,OAAO,yBAAyB,EAAE,QAAQ,OAAO,GAAG;AAAA,IAChG,oBAAC,UAAK,KAAK,OAAO,OAAO,OAAO;AAAA,KAClC;AAEJ;AAmBO,SAAS,aAAa,OAA0B;AACrD,QAAM,EAAE,IAAI,QAAQ,QAAQ,WAAW,GAAG,KAAK,IAAI;AACnD,QAAM,SAAS,OAAO,OAAO,WAAW,UAAU,EAAE,IAAI;AACxD,MAAI,CAAC,UAAU,OAAO,SAAS;AAC7B,UAAM,IAAI,MAAM,IAAI,OAAO,OAAO,WAAW,KAAK,QAAQ,EAAE,yEAAyE;AACvI,QAAM,YAAY,KAAK,UAAU,UAAU,CAAC,CAAC;AAC7C,QAAM,SAAS;AAAA,IACb,MAAM,mBAAmB,QAAQ,eAAe,QAAQ,EAAE,QAAQ,OAAO,CAAC,CAAC;AAAA;AAAA;AAAA,IAG3E,CAAC,QAAQ,QAAQ,SAAS;AAAA,EAC5B;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,IAAI;AAAA,MACJ;AAAA,MACA,MAAM,OAAO;AAAA,MACb,SAAS,OAAO;AAAA,MAChB,QAAQ,QAAQ,WAAW;AAAA,MAC1B,GAAG;AAAA;AAAA,EACN;AAEJ;AA+BO,SAAS,YAAY,OAAyB;AACnD,QAAM,EAAE,QAAQ,SAAS,QAAQ,QAAQ,UAAU,iBAAiB,OAAO,GAAG,KAAK,IAAI;AACvF,QAAM,OAAO,OAAuB,IAAI;AACxC,QAAM,UAAU,OAAkC,MAAS;AAC3D,QAAM,YAAY,KAAK,UAAU,UAAU,CAAC,CAAC;AAE7C,QAAM,UAAU,OAAqE,EAAE,WAAW,GAAG,CAAC;AACtG,QAAM,SAAS,OAAO,EAAE,SAAS,QAAQ,UAAU,CAAC;AACpD,SAAO,UAAU,EAAE,SAAS,QAAQ,UAAU;AAC9C,YAAU,MAAM;AACd,UAAM,MAAM,OAAO;AACnB,UAAM,IAAI,YAAY,KAAK,SAAU,QAAQ;AAAA,MAC3C,GAAI,IAAI,UAAU,EAAE,SAAS,IAAI,QAAQ,IAAI,CAAC;AAAA,MAC9C,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,MAC3B,GAAI,IAAI,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;AAAA,MAC3C,UAAU,YAAY;AAAA,MACtB,iBAAiB,mBAAmB;AAAA,IACtC,CAAC;AACD,MAAE,QAAQ,MAAM,UAAU;AAC1B,MAAE,QAAQ,MAAM,QAAQ;AACxB,MAAE,QAAQ,MAAM,SAAS;AACzB,YAAQ,UAAU;AAClB,YAAQ,UAAU,EAAE,SAAS,IAAI,SAAS,WAAW,IAAI,UAAU;AACnE,WAAO,MAAM;AACX,QAAE,QAAQ;AACV,cAAQ,UAAU;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,QAAQ,QAAQ,UAAU,eAAe,CAAC;AAC9C,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,WAAW,CAAC,WAAW,YAAY,QAAQ,QAAQ,QAAS;AACzE,YAAQ,QAAQ,WAAW,OAAO;AAClC,YAAQ,QAAQ,UAAU;AAAA,EAC5B,GAAG,CAAC,OAAO,CAAC;AACZ,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,WAAW,CAAC,UAAU,cAAc,QAAQ,QAAQ,UAAW;AAC5E,YAAQ,QAAQ,UAAU,MAAM;AAChC,YAAQ,QAAQ,YAAY;AAAA,EAC9B,GAAG,CAAC,SAAS,CAAC;AACd,SAAO,oBAAC,SAAI,KAAK,MAAM,OAAO,EAAE,UAAU,YAAY,GAAG,MAAM,GAAI,GAAG,MAAM;AAC9E;","names":[]}
@@ -0,0 +1,31 @@
1
+ import {
2
+ exportMotionGraphic,
3
+ ffmpegPath,
4
+ findChrome,
5
+ fragmentSource,
6
+ frameStates,
7
+ imageDataUrl,
8
+ pageControls,
9
+ planOutput,
10
+ renderPageHtml,
11
+ runRender,
12
+ scriptedPointer,
13
+ sequencePath
14
+ } from "./chunk-JL6TI6II.js";
15
+ import "./chunk-RDU63CDO.js";
16
+ import "./chunk-VYOMXE6I.js";
17
+ export {
18
+ exportMotionGraphic,
19
+ ffmpegPath,
20
+ findChrome,
21
+ fragmentSource,
22
+ frameStates,
23
+ imageDataUrl,
24
+ pageControls,
25
+ planOutput,
26
+ renderPageHtml,
27
+ runRender,
28
+ scriptedPointer,
29
+ sequencePath
30
+ };
31
+ //# sourceMappingURL=recorder-CDPPTJMG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -390,4 +390,4 @@ declare class ShaderRenderer {
390
390
  private onRestored;
391
391
  }
392
392
 
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, SVG_EDGE as S, TextureCache as T, VERTEX_SOURCE as V, type CompileStatus as a, type CompileResult as b, type CompiledShader as c, ShaderCompileError as d, ShaderRenderer 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 };
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 };
@@ -1,11 +1,15 @@
1
1
  import { ActionReturn } from 'svelte/action';
2
- import { V as VectorEffect } from '../types-DLkbOX8d.js';
3
2
  import { C as ControlValue } from '../controls-Ec6P0Vpy.js';
4
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';
5
+ import { EffectSubject } from '../embed/index.js';
6
+ import '../renderer-teaa98RE.js';
7
+ import '../frames-VEcqa40g.js';
5
8
 
6
9
  /**
7
- * Svelte 5 bindings for Flair's vector effects, written as plain TypeScript so
8
- * they need no Svelte compiler: the `vectorEffect` action plays a catalog
10
+ * Svelte 5 bindings for Flair, written as plain TypeScript so they need no
11
+ * Svelte compiler: the `flairEffect` action runs any catalog effect (shader or
12
+ * vector) applied to a picture, video, SVG or words; the `vectorEffect` action plays a catalog
9
13
  * effect (or any {@link SvgMotion}) in an element, and `vectorEffectStill`
10
14
  * gives the markup and style of its still for server rendering.
11
15
  *
@@ -143,4 +147,29 @@ declare function vectorEffectStill(params: VectorEffectParams): VectorEffectStil
143
147
  */
144
148
  declare function vectorEffect(node: HTMLElement, initial: VectorEffectParams): ActionReturn<VectorEffectParams>;
145
149
 
146
- export { type ReducedMotion, SvgMotion, type SvgMotionParams, SvgPlayer, type SvgPlayerParams, SvgTrigger, type VectorEffectByIdParams, type VectorEffectParams, type VectorEffectStill, vectorEffect, vectorEffectStill };
150
+ /** What the `flairEffect` action runs: any catalog effect, applied to a subject. */
151
+ interface FlairEffectParams {
152
+ /** A catalog effect, by id or definition: shader or vector. */
153
+ effect: string | EffectDefinition;
154
+ /** What to apply it to: a picture, a video, an SVG, words or a path (see `EffectSubject`). */
155
+ subject?: EffectSubject | readonly EffectSubject[];
156
+ preset?: string;
157
+ values?: Record<string, ControlValue>;
158
+ /** Start playing at once (default `true`). */
159
+ autoplay?: boolean;
160
+ /** Pause while scrolled out of view (default `true`). */
161
+ pauseWhenHidden?: boolean;
162
+ }
163
+ /**
164
+ * Runs any catalog effect, shader or vector, in an element, applied to a
165
+ * subject (see `mountEffect` in `@pixerate/flair/embed`); it fills the
166
+ * element, so size it with CSS. A new subject or values update the running
167
+ * effect; a new effect, preset or playback option starts it again.
168
+ *
169
+ * ```svelte
170
+ * <div use:flairEffect={{ effect: "image-duotone", subject: { image: "/team.jpg" } }} style="aspect-ratio: 16 / 9"></div>
171
+ * ```
172
+ */
173
+ declare function flairEffect(node: HTMLElement, initial: FlairEffectParams): ActionReturn<FlairEffectParams>;
174
+
175
+ export { type FlairEffectParams, type ReducedMotion, SvgMotion, type SvgMotionParams, SvgPlayer, type SvgPlayerParams, SvgTrigger, type VectorEffectByIdParams, type VectorEffectParams, type VectorEffectStill, flairEffect, vectorEffect, vectorEffectStill };
@@ -1,20 +1,25 @@
1
- import {
2
- effectDefaults,
3
- getEffect,
4
- renderVectorEffect
5
- } from "../chunk-4F4KL4KU.js";
6
1
  import {
7
2
  svgFrameMarkup,
8
3
  svgInlineLayout,
9
4
  svgViewBox
10
- } from "../chunk-LVPIIWPP.js";
5
+ } from "../chunk-STKEKXCK.js";
6
+ import {
7
+ mountEffect
8
+ } from "../chunk-S2RGHHKZ.js";
9
+ import "../chunk-H6VDFBIO.js";
10
+ import "../chunk-RDU63CDO.js";
11
+ import {
12
+ effectDefaults,
13
+ getEffect,
14
+ renderVectorEffect
15
+ } from "../chunk-PYDN5G2P.js";
16
+ import "../chunk-EHDJ3CPJ.js";
17
+ import "../chunk-VYOMXE6I.js";
11
18
  import {
12
19
  SvgPlayer
13
20
  } from "../chunk-PFGGPIMT.js";
14
- import "../chunk-OHW7UG6D.js";
15
- import "../chunk-EHDJ3CPJ.js";
16
21
  import "../chunk-WMZQCUFJ.js";
17
- import "../chunk-VYOMXE6I.js";
22
+ import "../chunk-ORAFCHX3.js";
18
23
 
19
24
  // src/svelte/index.ts
20
25
  var CAP_HEIGHT = "0.72em";
@@ -188,7 +193,44 @@ function vectorEffect(node, initial) {
188
193
  }
189
194
  };
190
195
  }
196
+ function flairEffect(node, initial) {
197
+ let params = initial;
198
+ if (!node.style.position) node.style.position = "relative";
199
+ const start = () => {
200
+ const m = mountEffect(node, params.effect, {
201
+ ...params.subject ? { subject: params.subject } : {},
202
+ ...params.preset ? { preset: params.preset } : {},
203
+ ...params.values ? { values: params.values } : {},
204
+ autoplay: params.autoplay ?? true,
205
+ pauseWhenHidden: params.pauseWhenHidden ?? true
206
+ });
207
+ m.element.style.display = "block";
208
+ m.element.style.width = "100%";
209
+ m.element.style.height = "100%";
210
+ return m;
211
+ };
212
+ let mounted = start();
213
+ return {
214
+ update(next) {
215
+ const restart = next.effect !== params.effect || next.preset !== params.preset || next.autoplay !== params.autoplay || next.pauseWhenHidden !== params.pauseWhenHidden;
216
+ const subjectChanged = next.subject !== params.subject;
217
+ const valuesChanged = JSON.stringify(next.values ?? {}) !== JSON.stringify(params.values ?? {});
218
+ params = next;
219
+ if (restart) {
220
+ mounted.dispose();
221
+ mounted = start();
222
+ return;
223
+ }
224
+ if (subjectChanged && next.subject) mounted.setSubject(next.subject);
225
+ if (valuesChanged && next.values) mounted.setValues(next.values);
226
+ },
227
+ destroy() {
228
+ mounted.dispose();
229
+ }
230
+ };
231
+ }
191
232
  export {
233
+ flairEffect,
192
234
  vectorEffect,
193
235
  vectorEffectStill
194
236
  };