@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.
- package/CHANGELOG.md +19 -0
- package/README.md +54 -6
- package/dist/catalog/index.d.ts +11 -6
- package/dist/catalog/index.js +1 -1
- package/dist/{chunk-JL6TI6II.js → chunk-7KYQ4R76.js} +2 -2
- package/dist/chunk-7KYQ4R76.js.map +1 -0
- package/dist/{chunk-STKEKXCK.js → chunk-AUH3PEFW.js} +2 -2
- package/dist/{chunk-PFGGPIMT.js → chunk-DA4UUSDO.js} +58 -3
- package/dist/chunk-DA4UUSDO.js.map +1 -0
- package/dist/{chunk-RDU63CDO.js → chunk-HDYBY3PQ.js} +380 -1
- package/dist/chunk-HDYBY3PQ.js.map +1 -0
- package/dist/{chunk-GXP4YLAP.js → chunk-HSYDTVTD.js} +9 -9
- package/dist/chunk-HSYDTVTD.js.map +1 -0
- package/dist/{chunk-VLQLRKGW.js → chunk-MAYKC3RF.js} +260 -33
- package/dist/chunk-MAYKC3RF.js.map +1 -0
- package/dist/{chunk-H6VDFBIO.js → chunk-SMR2MJXB.js} +156 -5
- package/dist/chunk-SMR2MJXB.js.map +1 -0
- package/dist/{chunk-4LKAPRFM.js → chunk-UIODYJ6B.js} +2274 -176
- package/dist/chunk-UIODYJ6B.js.map +1 -0
- package/dist/{chunk-YBGRXVAE.js → chunk-X3C2VFTM.js} +3 -3
- package/dist/{chunk-CK6EV37I.js → chunk-ZULFHADW.js} +69 -6
- package/dist/chunk-ZULFHADW.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/editor/index.d.ts +3 -3
- package/dist/editor/index.js +3 -3
- package/dist/{effect-GGP66JI6.js → effect-RGABT3BK.js} +10 -10
- package/dist/embed/index.d.ts +36 -8
- package/dist/embed/index.js +5 -5
- package/dist/{frames-VEcqa40g.d.ts → frames-CGJudAbu.d.ts} +11 -1
- package/dist/index.d.ts +69 -6
- package/dist/index.js +35 -7
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +4 -4
- package/dist/node.js +9 -9
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +7 -7
- package/dist/react/index.js.map +1 -1
- package/dist/{recorder-CDPPTJMG.js → recorder-TYMEZHOT.js} +3 -3
- package/dist/{renderer-teaa98RE.d.ts → renderer-tk1nbVkW.d.ts} +252 -2
- package/dist/svelte/index.d.ts +4 -4
- package/dist/svelte/index.js +7 -7
- package/dist/svelte/index.js.map +1 -1
- package/dist/{svg-BhRboN9q.d.ts → svg-DaKRPA9v.d.ts} +39 -7
- package/dist/{types-DUOMxZaq.d.ts → types-DuGVdJbt.d.ts} +2 -2
- package/dist/vector/index.d.ts +2 -2
- package/dist/vector/index.js +5 -3
- package/package.json +1 -1
- package/dist/chunk-4LKAPRFM.js.map +0 -1
- package/dist/chunk-CK6EV37I.js.map +0 -1
- package/dist/chunk-GXP4YLAP.js.map +0 -1
- package/dist/chunk-H6VDFBIO.js.map +0 -1
- package/dist/chunk-JL6TI6II.js.map +0 -1
- package/dist/chunk-PFGGPIMT.js.map +0 -1
- package/dist/chunk-RDU63CDO.js.map +0 -1
- package/dist/chunk-VLQLRKGW.js.map +0 -1
- /package/dist/{chunk-STKEKXCK.js.map → chunk-AUH3PEFW.js.map} +0 -0
- /package/dist/{chunk-YBGRXVAE.js.map → chunk-X3C2VFTM.js.map} +0 -0
- /package/dist/{effect-GGP66JI6.js.map → effect-RGABT3BK.js.map} +0 -0
- /package/dist/{recorder-CDPPTJMG.js.map → recorder-TYMEZHOT.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @pixerate/flair
|
|
2
2
|
|
|
3
|
+
## 0.30.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 94bac1b: Broadcast graphics and annotations: two new catalog categories of animated SVG effects in a 16:9, title-safe frame. `broadcast` has a lower third, a stacked lower third with a logo, a title card, a logo reveal, a logo sting, an end card, a countdown, a subscribe button, a news ticker, a scoreboard and a chapter marker; `annotation` has a hand-drawn circle and arrow, a callout, a bracket note, a scribble-out, step badges, a focus box, a product-demo cursor and a tooltip tour, all transparent to lay over video or screenshots. Each animates in, holds for its `hold` control and animates out, and takes a logo as SVG path data where it has one.
|
|
8
|
+
|
|
9
|
+
## 0.29.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- aae5438: Sound and scrolling as shader inputs, and effects that use them.
|
|
14
|
+
|
|
15
|
+
- New built-ins: `iAudio` (level, bass, mid, treble), `iSpectrum[16]` (64 log-spaced bins), `iBeat` (seconds since the last beat) and `iScroll` (progress through the viewport, page scroll, velocity, scrolling).
|
|
16
|
+
- `ShaderRenderer.setAudioSource(element | stream | node)` listens through a shared `AudioContext` and `AnalyserNode`; `ShaderRenderer.setScrollProgress(p)` pins the scroll for scrubbers. `ShaderFrames.render(t, { audio, scroll })` scripts both.
|
|
17
|
+
- `AudioAnalysis` (pure, deterministic), `analyseAudioBuffer` (offline, frame-exact, with a small radix-2 `fft`), `LiveAudio`, `sharedAudioContext`, `ScrollTracker` and `viewportProgress`.
|
|
18
|
+
- `SvgPlayer` takes `trigger: "scroll"` (scrubbing with the page, over `scrollRange`) and gains `setProgress(p)` and `scrollPosition(p)`.
|
|
19
|
+
- `effectInputs` reports `audio` in `accepts` and `scroll` in `interactions`; `mountEffect` takes an `{ audio }` subject; `recordEffect` takes `audio` (an `AudioBuffer` or a URL) to drive audio-reactive shaders frame for frame and mux the sound as AAC (mp4) or Opus (webm). `standaloneHtml` takes `{ audio: url | "mic" }`.
|
|
20
|
+
- New `audio` category: spectrum-bars, circular-spectrum, waveform-line, bass-pulse, audio-particles, audio-plasma, vinyl. New `scroll` category: scroll-reveal, parallax-layers, scroll-progress-glow, scroll-velocity-blur, scroll-zoom, scroll-draw, scroll-counter, scroll-steps. All play idle demos with no sound or scrolling.
|
|
21
|
+
|
|
3
22
|
## 0.28.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -24,9 +24,11 @@ Flair gives products a library of good-looking, configurable visual effects (her
|
|
|
24
24
|
- Shadertoy-style GLSL ES 3.00 fragment shaders, with controls declared in the source (`// @control`), including image inputs.
|
|
25
25
|
- Compile errors come back with line numbers that match the source.
|
|
26
26
|
- Pointer input beyond Shadertoy's `iMouse`: `iPointer` (position while over the canvas, eased hover amount, seconds held or since release) `iClicks[8]` (the latest clicks' positions and times) and `iTrail[32]` (where the pointer has been over the last second, newest first), so effects answer hovers, presses, taps and moves. `ShaderRenderer` and standalone pages feed them; `ShaderFrames` takes scripted `pointer`, `clicks` and `trail` for recordings.
|
|
27
|
+
- Sound: `iAudio` (level, bass, mid, treble), `iSpectrum[16]` (64 log-spaced bins) and `iBeat` (seconds since the last beat). `ShaderRenderer.setAudioSource` listens to an `<audio>`/`<video>`, a microphone or any Web Audio node; `analyseAudioBuffer` analyses a decoded track offline, so recordings follow it frame for frame (see [Sound and scrolling](#sound-and-scrolling)).
|
|
28
|
+
- Scrolling: `iScroll` (the canvas's progress through the viewport, page scroll, velocity, and whether it is scrolling), fed by `ShaderRenderer` and scriptable in `ShaderFrames`; vector effects scrub with the page under `trigger: "scroll"`.
|
|
27
29
|
- Live previews (`ShaderRenderer`, with optional `{ alpha: true }` for transparent overlays) and many animated or hover-activated thumbnails on one context (`ThumbnailPool`).
|
|
28
30
|
- Frames at exact times (`ShaderFrames`), looping GIFs, and standalone HTML pages.
|
|
29
|
-
- ✒️ **Vector motion (`@pixerate/flair/vector`)**: animated SVG described as data. Catalog vector effects declare their controls in the picture (`{{name: type …}}`), text included. It covers attribute tweens, draw-on strokes, motion along paths, counters (`animateCount` / `formatCount` for DOM elements too), text written letter by letter (`write`, with `scramble` and `caret`), shape morphs between any paths (`morphPaths`, `morphSequence`) and view zooms. Every frame is a pure function of time (`svgFrame`), and `SvgPlayer` plays it (with `scrollTrigger`, `hideWhenPaused`, `reset()`, and `trigger: "hover" | "press" | "click"` to play in answer to the pointer).
|
|
31
|
+
- ✒️ **Vector motion (`@pixerate/flair/vector`)**: animated SVG described as data. Catalog vector effects declare their controls in the picture (`{{name: type …}}`), text included. It covers attribute tweens, draw-on strokes, motion along paths, counters (`animateCount` / `formatCount` for DOM elements too), text written letter by letter (`write`, with `scramble` and `caret`), shape morphs between any paths (`morphPaths`, `morphSequence`) and view zooms. Every frame is a pure function of time (`svgFrame`), and `SvgPlayer` plays it (with `scrollTrigger`, `hideWhenPaused`, `reset()`, and `trigger: "hover" | "press" | "click" | "scroll"` to play in answer to the pointer or scrub with the page).
|
|
30
32
|
- 🎛️ **Effect editor (`@pixerate/flair/editor`)**:
|
|
31
33
|
- A headless editor for an effect's source, values, keyframes, presets and timeline.
|
|
32
34
|
- Built on [`@pixerate/editor`](https://github.com/Pixerate/editor)'s undo history and command registry, with no UI framework assumed.
|
|
@@ -170,10 +172,10 @@ findEffects({ accepts: ["image"], interaction: ["hover"] }); // pictures that re
|
|
|
170
172
|
findEffects({ transparent: true }); // overlays to lay over a page, picture or video
|
|
171
173
|
```
|
|
172
174
|
|
|
173
|
-
- `accepts`: `image` (any picture URL), `video` (a live `<video>`, with `setLiveSource`), `svg` (an SVG URL, drawn with its transparency), `text` (words for a text control, or for a text mask through `textImage("…")`), `mask` (a shape with transparency whose silhouette the effect uses: an icon, a logo, a word), `path` (SVG path data for a shape control).
|
|
175
|
+
- `accepts`: `image` (any picture URL), `video` (a live `<video>`, with `setLiveSource`), `svg` (an SVG URL, drawn with its transparency), `text` (words for a text control, or for a text mask through `textImage("…")`), `mask` (a shape with transparency whose silhouette the effect uses: an icon, a logo, a word), `path` (SVG path data for a shape control), `audio` (sound it reacts to, from reading `iAudio`, `iSpectrum` or `iBeat`).
|
|
174
176
|
- `images`: each image control's name, and whether it `wants` a `picture`, a `mask` or `text`.
|
|
175
177
|
- `texts`: each text control's name, and whether it `holds` `copy`, a `font`, a `path` or `glyphs`.
|
|
176
|
-
- `interactions`: `hover`, `press`, `click`, `trail` (follows where the pointer has been)
|
|
178
|
+
- `interactions`: `hover`, `press`, `click`, `trail` (follows where the pointer has been), `pointer` (follows where it is) or `scroll` (follows the page's scrolling), from the pointer and scroll built-ins a shader reads or a vector effect's `trigger`.
|
|
177
179
|
- `transparent`, `video` and `trigger`, as on the effect.
|
|
178
180
|
|
|
179
181
|
`findEffects` filters on `accepts` (all of them), `interaction` (any of them; `[]` for effects that don't answer the pointer) and `transparent`, and the `catalog.search` command takes the same filters and lists each effect's `accepts` and `interactions`. The `interactive` tag is kept in step with `interactions`.
|
|
@@ -186,6 +188,7 @@ findEffects({ transparent: true }); // overlays to lay over a page, picture or v
|
|
|
186
188
|
- `{ svg }`: an SVG, as markup or a URL.
|
|
187
189
|
- `{ text }`: words, typed into a vector effect, or drawn with `textImage` for a shader's text or mask.
|
|
188
190
|
- `{ path }`: path data.
|
|
191
|
+
- `{ audio }`: sound for an audio-reactive shader: a playing `<audio>` or `<video>` (still heard), a microphone's `MediaStream` (not played back) or a Web Audio node.
|
|
189
192
|
|
|
190
193
|
It puts the subject in the right control (see `effectInputs`) and throws a sentence if the effect can't take it.
|
|
191
194
|
|
|
@@ -197,6 +200,8 @@ mountEffect(el, "crt", { subject: { video: document.querySelector("video")! } })
|
|
|
197
200
|
mountEffect(el, "neon-outline", { subject: { svg: logoMarkup } });
|
|
198
201
|
mountEffect(el, "typewriter", { subject: { text: "Hello there" } });
|
|
199
202
|
mountEffect(el, "transition-wipe", { subject: [{ image: "/a.jpg" }, { image: "/b.jpg" }] });
|
|
203
|
+
mountEffect(el, "spectrum-bars", { subject: { audio: document.querySelector("audio")! } });
|
|
204
|
+
mountEffect(el, "scroll-draw"); // scroll effects follow the page by themselves
|
|
200
205
|
const m = mountEffect(el, "click-ripple"); // overlays get a transparent canvas, interactive effects answer the pointer
|
|
201
206
|
m.setValues({ uSpeed: 2 });
|
|
202
207
|
m.dispose();
|
|
@@ -217,6 +222,7 @@ import { recordEffect } from "@pixerate/flair/embed";
|
|
|
217
222
|
|
|
218
223
|
const mp4 = await recordEffect("image-duotone", { subject: { image: "/team.jpg" }, preset: "Spotify" });
|
|
219
224
|
const webm = await recordEffect("crt", { subject: { video }, format: "webm", duration: 5 });
|
|
225
|
+
const music = await recordEffect("spectrum-bars", { audio: "/song.mp3", duration: 10 }); // the sound drives it and is muxed in
|
|
220
226
|
const gif = await recordEffect("typewriter", { subject: { text: "Hello there" }, format: "gif", width: 640, height: 360, fps: 25 });
|
|
221
227
|
const overlay = await recordEffect("click-ripple", {
|
|
222
228
|
format: "webm",
|
|
@@ -230,7 +236,8 @@ download(mp4); // a Blob
|
|
|
230
236
|
- **Formats.** `mp4` (H.264) or `webm` (VP9, else VP8) are encoded with WebCodecs and written by small built-in muxers: a faststart MP4 (`moov` before `mdat`) and a WebM with no `Cues`. `gif` uses gifenc. `png` gives a ZIP of numbered frames (`frame-0001.png`, …), so every format is one `Blob`. The default is `mp4` where the browser encodes H.264, else `webm`. When the browser can't make a format, it rejects with a sentence that names the formats that would work.
|
|
231
237
|
- **Exact frames.** Frame `i` is drawn at `start + i / fps` off screen, so the result is the same whatever the page is doing. Shaders render with `ShaderFrames`. A `<video>` subject is paused and sought to each frame's time, then put back. A canvas subject is drawn as it is, so draw it for each frame in `beforeFrame(time)`. Vector effects are drawn with `svgView` and rasterised.
|
|
232
238
|
- **Defaults.** 1280 × 720 at 30 fps. The length is a shader's `loop`, or a vector effect's picture plus its `hold`, else 4 s. The bitrate is `width × height × fps × 0.15`. Video sizes round up to even numbers. `recordingPlan(effect, options)` tells you what a recording will be (format, size, frames, encoder) before any frame is drawn.
|
|
233
|
-
- **Interaction.** Shaders that answer the pointer play their idle demo unless `pointer` scripts it, with the same `pointer`, `clicks` and `trail` that `ShaderFrames.render` takes. Click-triggered vector effects take `steps` (ms).
|
|
239
|
+
- **Interaction.** Shaders that answer the pointer play their idle demo unless `pointer` scripts it, with the same `pointer`, `clicks` and `trail` that `ShaderFrames.render` takes. Click-triggered vector effects take `steps` (ms). Scroll effects play their idle demos too.
|
|
240
|
+
- **Sound.** `audio` takes a decoded `AudioBuffer` or an audio file's URL (fetched and decoded at 48 kHz). Audio-reactive shaders follow it frame for frame: each frame's `iAudio`, `iSpectrum` and `iBeat` come from `analyseAudioBuffer` at that frame's time, so the same file always makes the same video. The stretch from `start` to `start + duration` becomes the video's sound track: AAC in an `mp4`, Opus in a `webm` (encoded with WebCodecs' `AudioEncoder`, muxed by the same built-in muxers). Where the browser has no AAC encoder, the default format falls back to `webm`, and an explicit `mp4` is refused with a sentence. Pass `audioTrack: false` for the picture alone. `gif` and `png` have no sound, so there the audio only drives the picture.
|
|
234
241
|
- **Transparency.** `alpha` defaults to the effect's `transparent`. It is kept in `webm` (VP9/VP8 with an alpha channel, encoded as a second stream and stored as `BlockAdditions`, as Chrome does), in `png`, and in `gif` (one see-through colour, so edges are hard). An `mp4` has no alpha, so asking for both is refused. When alpha is off, transparent pixels are laid over `background` (black for shaders, white for vector effects).
|
|
235
242
|
|
|
236
243
|
Limits: video needs WebCodecs (Chrome and Edge 94+, Safari 16.4+ for H.264, and Firefox 130+). Safari doesn't play alpha WebM, so use PNG frames there. Everything is held in memory until the end, so record long or 4K clips in parts. Vector effects are rasterised once per frame, which is slower than shaders. A picture from another site needs CORS, as on screen.
|
|
@@ -355,7 +362,7 @@ const status = renderer.setSource(source); // { ok, errors: [{ line, message }]
|
|
|
355
362
|
parseControls(source); // [{ name: "uTint", type: "vec3", color: true, default: [...] }, ...]
|
|
356
363
|
const gif = await recordGif({ source, width: 480, height: 270, fps: 30, duration: 3 });
|
|
357
364
|
const frames = new ShaderFrames({ source, width: 480, height: 270, alpha: true }); // alpha: keep fragColor.a
|
|
358
|
-
const page = standaloneHtml("My effect", source, { uSpeed: 2 });
|
|
365
|
+
const page = standaloneHtml("My effect", source, { uSpeed: 2 }); // { audio: "song.mp3" } or { audio: "mic" } for sound
|
|
359
366
|
```
|
|
360
367
|
|
|
361
368
|
### 3. Build an editor (any UI)
|
|
@@ -451,6 +458,39 @@ The exporters carry the camera too:
|
|
|
451
458
|
- **FCP7 XML:** a keyframed Distort corner pin on each shot's clip, plus Motion Blur.
|
|
452
459
|
- **EDL:** a `* CAMERA:` note, since CMX 3600 has no transforms.
|
|
453
460
|
|
|
461
|
+
#### Sound and scrolling
|
|
462
|
+
|
|
463
|
+
Two more inputs, beside the pointer, that shaders read as built-ins (declared by the wrapper, so never in a source):
|
|
464
|
+
|
|
465
|
+
| Built-in | What it holds |
|
|
466
|
+
| --- | --- |
|
|
467
|
+
| `vec4 iAudio` | x level (loudness from RMS, 0–1), y bass (20–250 Hz), z mid (250–2000 Hz), w treble (2–16 kHz), each 0–1, smoothed (quick attack, slower release) |
|
|
468
|
+
| `vec4 iSpectrum[16]` | 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]` |
|
|
469
|
+
| `float iBeat` | seconds since the last beat (an onset on the bass band over its recent average), -1 before the first |
|
|
470
|
+
| `vec4 iScroll` | x the canvas's progress through the viewport (0 as its top comes in at the bottom, 1 as its bottom leaves at the top), y the page's scroll (0–1), z velocity (viewport heights per second, positive scrolling down, smoothed), w 1 while scrolling, easing back to 0 |
|
|
471
|
+
|
|
472
|
+
With no audio at all every audio uniform is 0, `iBeat` included; with a source connected `iBeat` is -1 or above 0. So an effect can tell silence from "nothing connected". Where nothing scrolls (thumbnails, recordings), `iScroll` is all zero. The catalog's audio and scroll effects play an idle demo then (a synthetic 120 bpm beat, or a page scrolled down and back), which their "Demo" control turns off.
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import { ShaderRenderer, sharedAudioContext, analyseAudioBuffer, ShaderFrames } from "@pixerate/flair";
|
|
476
|
+
|
|
477
|
+
const preview = new ShaderRenderer(host);
|
|
478
|
+
preview.setSource(getEffect("spectrum-bars").source);
|
|
479
|
+
button.onclick = () => preview.setAudioSource(audioElement); // or a mic MediaStream, or any AudioNode; null stops
|
|
480
|
+
preview.setScrollProgress(0.5); // pin iScroll for a scrubber; null follows the page again
|
|
481
|
+
|
|
482
|
+
// Frame-exact, offline: the uniforms at any time of a decoded track.
|
|
483
|
+
const track = analyseAudioBuffer(await sharedAudioContext().decodeAudioData(bytes));
|
|
484
|
+
frames.render(t, { audio: track.at(t), scroll: [0.5, 0.5, 0, 0] });
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
- **Live sound.** `setAudioSource` makes an `AudioContext` (shared by every renderer: `sharedAudioContext()`) and an `AnalyserNode` on first use, and reads them each frame. A media element keeps playing to the speakers through Web Audio (an element can only be wired in once, so the node is reused). A microphone `MediaStream` is listened to, not played. Any `AudioNode` is analysed in its own context. Browsers start an audio context suspended until the page has a click or key press (the autoplay policy). Call `setAudioSource` from a click, or the context resumes on the first one. A media file from another site needs `crossOrigin = "anonymous"` and CORS, or the browser feeds silence. While a source is set, a paused preview still follows the sound. Dispose (or `null`) disconnects it.
|
|
488
|
+
- **The analysis.** `AudioAnalysis` is pure and unit-tested: it turns byte or decibel frequency data (`fftSize / 2` values) into the uniforms with log binning, band averages, attack/release smoothing and beat detection, and the same data at the same times always gives the same result. `analyseAudioBuffer(buffer)` runs it over a decoded track: a Blackman-windowed radix-2 FFT (`fft`, no dependencies) of the samples just before each time, as `AnalyserNode` would hear them, stepped 120 times a second from the start and kept, so frames asked in any order agree.
|
|
489
|
+
- **Scrolling.** `ShaderRenderer` follows the page with passive scroll listeners and `getBoundingClientRect` each frame (only when the shader reads `iScroll`), so a page or a scrolling element moving the canvas both count, and a paused preview still follows. `ScrollTracker` and `viewportProgress` are the pure parts.
|
|
490
|
+
- **Vector effects.** `new SvgPlayer(host, motion, { trigger: "scroll" })` scrubs the picture with its element's progress through the viewport instead of playing it on a clock: from the start at progress 0.1 to the end at 0.6 by default (`scrollRange`), back again as you scroll up. Hosts can drive any player with `player.setProgress(p)` (0–1), which stops a scroll trigger following the page.
|
|
491
|
+
- **Standalone pages.** `standaloneHtml(title, source, values, { audio: "song.mp3" })` adds a looping player in the corner that the page listens to once it plays, and `{ audio: "mic" }` a microphone button. The page itself does not scroll, so scroll effects play their idle demos there.
|
|
492
|
+
- **Thumbnails and the Node exporter.** `ThumbnailPool` and `flair render`/`flair export` declare the built-ins but feed no sound or scroll, so audio and scroll effects show their idle demos.
|
|
493
|
+
|
|
454
494
|
## 🎨 The catalog
|
|
455
495
|
|
|
456
496
|
| Category | Effects |
|
|
@@ -482,9 +522,17 @@ The exporters carry the camera too:
|
|
|
482
522
|
| glow | aura, bloom, pulsing halo, neon outline, backlight glow, rim light, glow ring pulse, glow button |
|
|
483
523
|
| shadow | long shadow, drop shadow lift, light sweep shadows, contact shadow, window shadows, shadow puppets, lift card, exploded layers |
|
|
484
524
|
| distortion | twirl, pinch & bulge, fisheye, mirror tiles, wave warp, ribbed glass, shower glass, datamosh, polar warp, texture displace |
|
|
525
|
+
| audio | spectrum bars, circular spectrum, waveform line, bass pulse, audio particles, audio plasma, vinyl |
|
|
526
|
+
| scroll | scroll reveal, parallax layers, scroll progress glow, scroll velocity blur, scroll zoom, scroll draw, scroll counter, scroll steps |
|
|
527
|
+
| broadcast | lower third, stacked lower third, title card, logo reveal, logo sting, end card, countdown, subscribe button, news ticker, scoreboard, chapter marker |
|
|
528
|
+
| annotation | hand circle, hand arrow, callout, bracket note, scribble out, step badges, focus box, fake cursor, tooltip tour |
|
|
485
529
|
| transition | liquid morph, displacement, luma fade, particle swap, ink bleed, pixel dissolve, comic panels, wipe, iris, block dissolve, grid wipe, glass wipe, twister, light leak, cross zoom, whip pan |
|
|
486
530
|
| loader / icon / data | spinner, bouncing dots, pulse ping, equalizer, progress ring, bar chart, line chart, stat counter, route, timeline track, success check, error cross, heartbeat, signature, confetti, flow network, upload card |
|
|
487
531
|
|
|
532
|
+
#### Broadcast graphics and annotations
|
|
533
|
+
|
|
534
|
+
The `broadcast` and `annotation` effects are animated SVGs in a 1920 × 1080 (16:9) frame, with their text inside the title-safe area. Each animates in, holds for its `hold` control (seconds) and animates out, ending as it started, so it loops; the news ticker is a seamless crawl instead. Lower thirds, the ticker, the score bug, the chapter marker, the subscribe button and every annotation draw on a transparent frame, to lay over video or a screenshot (`flair render lower-third --text "Ada Lovelace" --alpha -o lt.webm`); annotations are placed with position controls in percent of the frame. Every label is a text control, fitted to its length. Logos (`lower-third-stacked`, `logo-reveal`, `logo-sting`) take SVG path data in their `logo` control (`--path "M…"`), scaled to fit their slot and recoloured by the effect, so they draw on, shine and export as animated SVG like the rest; anything that is not path data draws a stand-in mark.
|
|
535
|
+
|
|
488
536
|
Every shader is self-contained and compiles on WebGL2. Open [`#check`](https://pixerate.github.io/flair/#check) on the demo to compile the catalog on your own GPU. [`#bench`](https://pixerate.github.io/flair/#bench) times every shader per frame on your GPU (`#bench?size=720&only=aurora,fur` for a size or a few effects; keep the tab in front, since a hidden tab or other drawing inflates every number).
|
|
489
537
|
|
|
490
538
|
---
|
|
@@ -514,7 +562,7 @@ npx flair export ./effect.frag -o out.mp4 -w 1280 -h 720 -d 5 --values '{"speed"
|
|
|
514
562
|
|
|
515
563
|
#### Rendering in Node
|
|
516
564
|
|
|
517
|
-
`flair render` and `flair export` render in headless Chrome, so a shader draws exactly as `ShaderRenderer` draws it: the same GLSL wrapper and built-ins (`iPointer`, `iClicks`, `iTrail`), image controls (SVGs sized up first), `int` and `bool` controls. The output format comes from the extension:
|
|
565
|
+
`flair render` and `flair export` render in headless Chrome, so a shader draws exactly as `ShaderRenderer` draws it: the same GLSL wrapper and built-ins (`iPointer`, `iClicks`, `iTrail`, and `iAudio`, `iSpectrum`, `iBeat`, `iScroll`, left at 0 so those effects play their idle demos), image controls (SVGs sized up first), `int` and `bool` controls. The output format comes from the extension:
|
|
518
566
|
|
|
519
567
|
| Output | Notes |
|
|
520
568
|
| --- | --- |
|
package/dist/catalog/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as ControlDef, C as ControlValue } from '../controls-Ec6P0Vpy.js';
|
|
2
|
-
import { a as SvgTrigger } from '../svg-
|
|
3
|
-
import { a as VideoUse, E as EffectDefinition, b as EffectCategory, S as ShaderEffect } from '../types-
|
|
4
|
-
export { C as CURRENT_COLOR, c as EFFECT_CATEGORIES, d as EffectPreset, e as VECTOR_CONTROL_TYPES, f as VIDEO_USES, g as VectorControl, V as VectorEffect, h as colorControl, i as isCurrentColor, n as numberControl, r as renderVectorEffect, v as vectorControlDef, j as vectorControlIssues, k as vectorControls, l as vectorEffectControls } from '../types-
|
|
2
|
+
import { a as SvgTrigger } from '../svg-DaKRPA9v.js';
|
|
3
|
+
import { a as VideoUse, E as EffectDefinition, b as EffectCategory, S as ShaderEffect } from '../types-DuGVdJbt.js';
|
|
4
|
+
export { C as CURRENT_COLOR, c as EFFECT_CATEGORIES, d as EffectPreset, e as VECTOR_CONTROL_TYPES, f as VIDEO_USES, g as VectorControl, V as VectorEffect, h as colorControl, i as isCurrentColor, n as numberControl, r as renderVectorEffect, v as vectorControlDef, j as vectorControlIssues, k as vectorControls, l as vectorEffectControls } from '../types-DuGVdJbt.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* What an effect takes and how it answers the pointer, worked out from the
|
|
@@ -18,8 +18,11 @@ export { C as CURRENT_COLOR, c as EFFECT_CATEGORIES, d as EffectPreset, e as VEC
|
|
|
18
18
|
* - `text`: words, for a text control, or for a text mask through `textImage`.
|
|
19
19
|
* - `mask`: a shape with transparency (an icon, a logo, `textImage`) whose silhouette the effect uses.
|
|
20
20
|
* - `path`: SVG path data (`d`), for a shape control.
|
|
21
|
+
* - `audio`: sound (a playing `<audio>` or `<video>`, a microphone, a Web
|
|
22
|
+
* Audio node, or an `AudioBuffer` for recordings) that it reacts to
|
|
23
|
+
* through `iAudio`, `iSpectrum` or `iBeat`.
|
|
21
24
|
*/
|
|
22
|
-
type EffectInput = "image" | "video" | "svg" | "text" | "mask" | "path";
|
|
25
|
+
type EffectInput = "image" | "video" | "svg" | "text" | "mask" | "path" | "audio";
|
|
23
26
|
/** Every {@link EffectInput}. */
|
|
24
27
|
declare const EFFECT_INPUTS: readonly EffectInput[];
|
|
25
28
|
/**
|
|
@@ -29,8 +32,10 @@ declare const EFFECT_INPUTS: readonly EffectInput[];
|
|
|
29
32
|
* - `click`: answers each click or tap.
|
|
30
33
|
* - `trail`: follows where the pointer has just been.
|
|
31
34
|
* - `pointer`: follows where the pointer is.
|
|
35
|
+
* - `scroll`: follows the page's scrolling (`iScroll`, or a vector effect's
|
|
36
|
+
* `trigger: "scroll"`).
|
|
32
37
|
*/
|
|
33
|
-
type EffectInteraction = "hover" | "press" | "click" | "trail" | "pointer";
|
|
38
|
+
type EffectInteraction = "hover" | "press" | "click" | "trail" | "pointer" | "scroll";
|
|
34
39
|
/** Every {@link EffectInteraction}. */
|
|
35
40
|
declare const EFFECT_INTERACTIONS: readonly EffectInteraction[];
|
|
36
41
|
/** A shader's image control and what it wants. */
|
|
@@ -159,7 +164,7 @@ declare function getEffect(id: string): EffectDefinition | undefined;
|
|
|
159
164
|
* (`accepts`: image, video, svg, text, mask, path), its image controls and
|
|
160
165
|
* whether each wants a picture, a mask or text, its text controls (copy,
|
|
161
166
|
* font, path, glyphs), how it suits video, how it answers the pointer
|
|
162
|
-
* (`interactions`: hover, press, click, trail, pointer) and whether it draws
|
|
167
|
+
* (`interactions`: hover, press, click, trail, pointer, scroll) and whether it draws
|
|
163
168
|
* over other content. Worked out from the effect itself, so it always agrees
|
|
164
169
|
* with its controls and source.
|
|
165
170
|
*/
|
package/dist/catalog/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TRAIL_SECONDS,
|
|
7
7
|
VERTEX_SOURCE,
|
|
8
8
|
wrapSource
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-HDYBY3PQ.js";
|
|
10
10
|
import {
|
|
11
11
|
controlValue,
|
|
12
12
|
parseControls
|
|
@@ -721,4 +721,4 @@ export {
|
|
|
721
721
|
frameStates,
|
|
722
722
|
exportMotionGraphic
|
|
723
723
|
};
|
|
724
|
-
//# sourceMappingURL=chunk-
|
|
724
|
+
//# sourceMappingURL=chunk-7KYQ4R76.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/export/recorder.ts"],"sourcesContent":["// gifenc ships without types; the reference brings them along.\n/// <reference path=\"../shader/gifenc.d.ts\" />\nimport http from 'node:http';\nimport { spawn, type ChildProcess } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { controlValue, parseControls, type ControlValue, type TextureSettings } from '../shader/controls.js';\nimport { MAX_CLICKS, MAX_TRAIL, PointerTracker, TRAIL_SECONDS } from '../shader/pointer.js';\nimport { VERTEX_SOURCE, wrapSource } from '../shader/program.js';\nimport { SVG_EDGE } from '../shader/textures.js';\n\n// ==========================================\n// Tools: Chrome and ffmpeg\n// ==========================================\n\nconst CHROME_CANDIDATES = [\n '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n '/Applications/Chromium.app/Contents/MacOS/Chromium',\n 'C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',\n '/usr/bin/google-chrome',\n '/usr/bin/google-chrome-stable',\n '/usr/bin/chromium',\n '/usr/bin/chromium-browser',\n];\n\n/** Chromium builds Playwright downloaded (`npx playwright install chromium`), newest first. */\nfunction playwrightChromes(): string[] {\n const roots = [\n process.env.PLAYWRIGHT_BROWSERS_PATH,\n path.join(os.homedir(), 'Library/Caches/ms-playwright'),\n path.join(os.homedir(), '.cache/ms-playwright'),\n ].filter((r): r is string => Boolean(r));\n const found: string[] = [];\n for (const root of roots) {\n let dirs: string[] = [];\n try {\n dirs = fs.readdirSync(root).filter((d) => /^chromium-\\d+$/.test(d));\n } catch {\n continue;\n }\n dirs.sort((a, b) => Number(b.split('-')[1]) - Number(a.split('-')[1]));\n for (const dir of dirs) {\n for (const bin of [\n 'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',\n 'chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',\n 'chrome-mac/Chromium.app/Contents/MacOS/Chromium',\n 'chrome-linux/chrome',\n 'chrome-linux64/chrome',\n ]) found.push(path.join(root, dir, bin));\n }\n }\n return found;\n}\n\n/**\n * Finds a local Chrome or Chromium executable, or `null`: `CHROME_PATH` (or\n * `CHROME_BIN`), then the usual install places, then a Chromium Playwright\n * downloaded.\n */\nexport function findChrome(): string | null {\n const env = process.env.CHROME_PATH || process.env.CHROME_BIN;\n if (env) return env;\n return [...CHROME_CANDIDATES, ...playwrightChromes()].find((candidate) => fs.existsSync(candidate)) ?? null;\n}\n\n/** The ffmpeg to run: `FFMPEG_PATH`, else `ffmpeg` on the `PATH`. */\nexport function ffmpegPath(): string {\n return process.env.FFMPEG_PATH || 'ffmpeg';\n}\n\nconst NO_FFMPEG =\n 'ffmpeg is not installed (or not on the PATH). Install it (brew install ffmpeg, apt install ffmpeg), or set FFMPEG_PATH; GIF, PNG and SVG output do not need it.';\n\n// ==========================================\n// Output formats\n// ==========================================\n\n/** A file type the exporter writes, from the output path. */\nexport type OutputFormat = 'mp4' | 'webm' | 'mov' | 'gif' | 'png' | 'svg';\n\n/** What to write and how. */\nexport interface OutputPlan {\n format: OutputFormat;\n /** The output path, resolved. For a PNG sequence, a printf pattern (`…/frame_%04d.png`). */\n path: string;\n /** Whether the frames keep their alpha. */\n alpha: boolean;\n /** ffmpeg's arguments after the input (for mp4, webm and mov). */\n ffmpegArgs?: string[];\n}\n\n/**\n * Works out the format from `outputPath`'s extension and what alpha it can\n * keep. A path with no extension, or ending in a slash, is a folder of PNG\n * frames; a `.png` path with a `%d` pattern (`out_%04d.png`) names each\n * frame, and one without gets `_%04d` added (unless there is one frame).\n * Throws a sentence for formats it cannot write, and for alpha in MP4.\n */\nexport function planOutput(outputPath: string, options: { alpha?: boolean; frames?: number } = {}): OutputPlan {\n const resolved = path.resolve(process.cwd(), outputPath);\n const trailingSlash = /[\\\\/]$/.test(outputPath);\n const ext = trailingSlash ? '' : path.extname(resolved).toLowerCase();\n const alpha = Boolean(options.alpha);\n switch (ext) {\n case '': {\n return { format: 'png', path: path.join(resolved, 'frame_%04d.png'), alpha };\n }\n case '.png': {\n if (/%0?\\d*d/.test(resolved) || (options.frames ?? 2) === 1) return { format: 'png', path: resolved, alpha };\n return { format: 'png', path: resolved.replace(/\\.png$/i, '_%04d.png'), alpha };\n }\n case '.gif':\n return { format: 'gif', path: resolved, alpha };\n case '.svg':\n return { format: 'svg', path: resolved, alpha };\n case '.mp4':\n case '.m4v': {\n if (alpha) throw new Error('MP4 (H.264) cannot hold alpha; write .webm, .mov, .gif or .png frames for a transparent render, or drop --alpha.');\n return {\n format: 'mp4',\n path: resolved,\n alpha,\n ffmpegArgs: [\n // yuv420p needs even sizes; sRGB pixels go to BT.709, tagged so players show the same colours.\n '-vf', 'pad=ceil(iw/2)*2:ceil(ih/2)*2,scale=out_color_matrix=bt709:out_range=tv,format=yuv420p',\n '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18', '-preset', 'medium',\n '-colorspace', 'bt709', '-color_primaries', 'bt709', '-color_trc', 'bt709', '-color_range', 'tv',\n '-movflags', '+faststart',\n ],\n };\n }\n case '.webm':\n return {\n format: 'webm',\n path: resolved,\n alpha,\n ffmpegArgs: alpha\n ? ['-c:v', 'libvpx-vp9', '-pix_fmt', 'yuva420p', '-b:v', '0', '-crf', '30', '-auto-alt-ref', '0', '-row-mt', '1']\n : ['-vf', 'scale=out_color_matrix=bt709:out_range=tv,format=yuv420p', '-c:v', 'libvpx-vp9', '-pix_fmt', 'yuv420p', '-b:v', '0', '-crf', '30', '-row-mt', '1',\n '-colorspace', 'bt709', '-color_primaries', 'bt709', '-color_trc', 'bt709'],\n };\n case '.mov':\n return {\n format: 'mov',\n path: resolved,\n alpha,\n ffmpegArgs: [\n '-c:v', 'prores_ks', '-profile:v', '4444', '-vendor', 'apl0',\n '-pix_fmt', alpha ? 'yuva444p10le' : 'yuv444p10le',\n ...(alpha ? ['-alpha_bits', '16'] : []),\n '-colorspace', 'bt709', '-color_primaries', 'bt709', '-color_trc', 'bt709',\n ],\n };\n default:\n throw new Error(`Flair cannot write \"${ext}\" files; write .mp4, .webm, .mov, .gif, .svg, or .png frames (a folder, or a pattern like out_%04d.png).`);\n }\n}\n\n/** A frame number put into a PNG sequence's printf pattern (`%04d`, `%d`). */\nexport function sequencePath(pattern: string, frame: number): string {\n return pattern.replace(/%(0?)(\\d*)d/, (_, zero: string, width: string) => String(frame).padStart(Number(width || 0), zero ? '0' : ' '));\n}\n\n// ==========================================\n// The pointer, scripted\n// ==========================================\n\n/** Pointer uniforms for one frame. */\nexport interface PointerFrame {\n mouse: [number, number, number, number];\n pointer: [number, number, number, number];\n /** `iClicks`, MAX_CLICKS × 4 numbers. */\n clicks: number[];\n /** `iTrail`, MAX_TRAIL × 4 numbers. */\n trail: number[];\n}\n\n/**\n * A pointer that shows off an interactive shader in a recording: it hovers\n * over the canvas from the start, wanders a figure-of-eight (so trails have\n * a path), and clicks every `interval` seconds, holding each press for a\n * moment. Returns the uniforms for each frame time, in order.\n */\nexport function scriptedPointer(width: number, height: number, times: readonly number[], options: { interval?: number; period?: number } = {}): PointerFrame[] {\n const interval = options.interval ?? 0.8;\n const period = Math.max(2, options.period ?? (times[times.length - 1] ?? 2));\n const at = (t: number): [number, number] => [\n width * (0.5 + 0.3 * Math.sin((2 * Math.PI * t) / period)),\n height * (0.5 + 0.22 * Math.sin((4 * Math.PI * t) / period)),\n ];\n const press = 0.25;\n const tracker = new PointerTracker();\n tracker.hover(true, 0);\n tracker.move(...at(0), 0);\n let clock = 0;\n let nextClick = 0.25;\n let release = Infinity;\n let mouse: [number, number, number, number] = [0, 0, 0, 0];\n const frames: PointerFrame[] = [];\n for (const time of times) {\n // Walk the script up to `time` in trail-sized steps, so the trail and clicks land where they would live.\n while (clock < time) {\n const next = Math.min(time, clock + TRAIL_SECONDS, nextClick, release);\n clock = next;\n const [x, y] = at(clock);\n tracker.move(x, y, clock);\n if (mouse[2] > 0) mouse = [x, y, mouse[2], mouse[3]];\n if (clock >= release) {\n tracker.up(clock);\n mouse = [mouse[0], mouse[1], -mouse[2], -Math.abs(mouse[3])];\n release = Infinity;\n }\n if (clock >= nextClick) {\n tracker.down(x, y, clock);\n mouse = [x, y, x, y];\n release = clock + press;\n nextClick = clock + interval;\n }\n }\n frames.push({\n mouse: [...mouse],\n pointer: tracker.pointer(time),\n clicks: [...tracker.clicksAt(time)],\n trail: [...tracker.trailAt(time)],\n });\n }\n return frames;\n}\n\n// ==========================================\n// The page\n// ==========================================\n\n/** A shader control as the page sets it. */\nexport interface PageControl {\n name: string;\n type: string;\n value: ControlValue;\n texture?: TextureSettings;\n}\n\n/** What the render page draws. */\nexport interface PageConfig {\n width: number;\n height: number;\n frames: number;\n /** Keep alpha in the frames (else they are composited over `background`). */\n alpha: boolean;\n /** CSS colour under transparent pixels when `alpha` is false. */\n background: string;\n /** How frames go back: PNG files, or raw RGBA (rows top down). */\n send: 'png' | 'rgba';\n /** A shader to run, or (when absent) SVG frame markup from `/state`. */\n shader?: {\n /** The full GLSL ES 3.00 fragment shader (see {@link fragmentSource}). */\n fragment: string;\n controls: PageControl[];\n /** Draws its frame opaque, as `ShaderRenderer` does without `alpha`: its own alpha is ignored. */\n opaque: boolean;\n };\n}\n\n/** JSON that is safe inside a `<script>`. */\nconst scriptJson = (value: unknown) => JSON.stringify(value).replace(/</g, '\\\\u003c');\n\n/**\n * The page headless Chrome runs: it compiles the shader with the same\n * wrapper as `ShaderRenderer` (`wrapSource`, so every built-in, `iPointer`,\n * `iClicks`, `iTrail`, `iAudio`, `iSpectrum`, `iBeat` and `iScroll` included, is\n * declared; the audio and scroll ones stay 0, so those effects play their idle\n * demos), sets each control by its\n * type (`int` and `bool` as integers), loads image controls the way\n * `loadImage` does (SVGs sized to their long edge first) before the first\n * frame, and flips them as the renderer does. Each frame asks `/state` for\n * its time, pointer, video frames or SVG markup, draws, and posts the\n * pixels to `/frame`.\n */\nexport function renderPageHtml(config: PageConfig): string {\n return `<!doctype html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<style>html,body{margin:0;background:transparent;overflow:hidden}</style>\n</head>\n<body>\n<script>\nconst CONFIG = ${scriptJson(config)};\nconst VERTEX = ${scriptJson(VERTEX_SOURCE)};\nconst W = CONFIG.width, H = CONFIG.height;\nconst report = (message) => fetch(\"/error\", { method: \"POST\", body: String(message) });\naddEventListener(\"error\", (e) => report(\"The render page failed: \" + e.message));\naddEventListener(\"unhandledrejection\", (e) => report((e.reason && e.reason.message) || String(e.reason)));\n\n// SVGs decode at their own (often icon) size: give them ${SVG_EDGE} px on the long edge, and a viewBox.\nfunction sizeSvg(markup, w0, h0) {\n const doc = new DOMParser().parseFromString(markup, \"image/svg+xml\");\n const svg = doc.documentElement;\n if (svg.localName !== \"svg\" || doc.getElementsByTagName(\"parsererror\").length) throw new Error(\"Not an SVG document.\");\n const len = (n) => { const m = (svg.getAttribute(n) || \"\").trim().match(/^([\\\\d.]+)(px)?$/); return m && +m[1] > 0 ? +m[1] : 0; };\n const box = (svg.getAttribute(\"viewBox\") || \"\").trim().split(/[\\\\s,]+/).map(Number);\n const hasBox = box.length === 4 && box[2] > 0 && box[3] > 0;\n const w = len(\"width\") || (hasBox ? box[2] : 300), h = len(\"height\") || (hasBox ? box[3] : 150);\n if (!hasBox) svg.setAttribute(\"viewBox\", \"0 0 \" + w + \" \" + h);\n const scale = w0 ? 1 : ${SVG_EDGE} / Math.max(w, h);\n svg.setAttribute(\"width\", w0 || Math.max(1, Math.round(w * scale)));\n svg.setAttribute(\"height\", h0 || Math.max(1, Math.round(h * scale)));\n return new XMLSerializer().serializeToString(svg);\n}\nconst isSvg = (url) => /^data:image\\\\/svg\\\\+xml[;,]/i.test(url) || /^(?!data:)[^?#]*\\\\.svg([?#]|$)/i.test(url);\nasync function decode(src, name) {\n const img = new Image();\n img.src = src;\n try { await img.decode(); } catch (e) { throw new Error(\"Could not load the image \" + name + \".\"); }\n return img;\n}\nasync function loadImage(url, name) {\n if (!isSvg(url)) return decode(url, name);\n const text = await (await fetch(url)).text();\n const blob = URL.createObjectURL(new Blob([sizeSvg(text)], { type: \"image/svg+xml\" }));\n try { return await decode(blob, name); } finally { URL.revokeObjectURL(blob); }\n}\n\nconst out = document.createElement(\"canvas\");\nout.width = W; out.height = H;\nconst ctx = out.getContext(\"2d\", { willReadFrequently: CONFIG.send === \"rgba\" });\nlet gl = null, glCanvas = null, u = null;\nconst textures = {};\n\nfunction setupShader() {\n glCanvas = document.createElement(\"canvas\");\n glCanvas.width = W; glCanvas.height = H;\n // As createContext: no antialiasing, depth or stencil; unpremultiplied alpha so pixels read back exactly.\n gl = glCanvas.getContext(\"webgl2\", { alpha: true, premultipliedAlpha: false, preserveDrawingBuffer: true, antialias: false, depth: false, stencil: false, powerPreference: \"high-performance\" });\n if (!gl) throw new Error(\"WebGL2 is not available in headless Chrome.\");\n const compile = (type, src) => {\n const s = gl.createShader(type);\n gl.shaderSource(s, src);\n gl.compileShader(s);\n if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(\"The shader does not compile: \" + gl.getShaderInfoLog(s));\n return s;\n };\n const program = gl.createProgram();\n gl.attachShader(program, compile(gl.VERTEX_SHADER, VERTEX));\n gl.attachShader(program, compile(gl.FRAGMENT_SHADER, CONFIG.shader.fragment));\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(\"The shader does not link: \" + gl.getProgramInfoLog(program));\n gl.useProgram(program);\n u = (n) => gl.getUniformLocation(program, n);\n gl.viewport(0, 0, W, H);\n if (CONFIG.shader.opaque) {\n // An opaque canvas, as ShaderRenderer has without alpha: alpha stays 1 whatever the shader writes.\n gl.clearColor(0, 0, 0, 1);\n gl.clear(gl.COLOR_BUFFER_BIT);\n gl.colorMask(true, true, true, false);\n }\n}\n\nfunction makeTexture(unit, texture) {\n const t = gl.createTexture();\n const settings = texture || { filter: \"mipmap\", wrap: \"repeat\" };\n gl.activeTexture(gl.TEXTURE0 + unit);\n gl.bindTexture(gl.TEXTURE_2D, t);\n // Black until (or unless) a picture arrives, as TextureCache does.\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, { mipmap: gl.LINEAR_MIPMAP_LINEAR, linear: gl.LINEAR, nearest: gl.NEAREST }[settings.filter]);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, settings.filter === \"nearest\" ? gl.NEAREST : gl.LINEAR);\n const wrap = { repeat: gl.REPEAT, clamp: gl.CLAMP_TO_EDGE, mirror: gl.MIRRORED_REPEAT }[settings.wrap];\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);\n return { unit, texture: t };\n}\n\nfunction upload(name, img) {\n const slot = textures[name];\n if (!slot) return;\n gl.activeTexture(gl.TEXTURE0 + slot.unit);\n gl.bindTexture(gl.TEXTURE_2D, slot.texture);\n // Rows bottom up, as the renderer's TextureCache uploads them.\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);\n gl.generateMipmap(gl.TEXTURE_2D);\n}\n\nasync function setControls() {\n let unit = 0;\n const loads = [];\n for (const c of CONFIG.shader.controls) {\n const loc = u(c.name);\n const v = c.value;\n if (c.type === \"sampler2D\") {\n textures[c.name] = makeTexture(unit, c.texture);\n if (loc) gl.uniform1i(loc, unit);\n unit++;\n if (v) loads.push(loadImage(v, \"for \" + c.name).then((img) => upload(c.name, img)));\n continue;\n }\n if (!loc) continue;\n if (c.type === \"float\") gl.uniform1f(loc, v);\n else if (c.type === \"int\") gl.uniform1i(loc, Math.round(v));\n else if (c.type === \"bool\") gl.uniform1i(loc, v ? 1 : 0);\n else if (Array.isArray(v)) gl[\"uniform\" + v.length + \"fv\"](loc, v);\n }\n await Promise.all(loads);\n}\n\nfunction drawShader(s) {\n gl.viewport(0, 0, W, H);\n const set = (n, f) => { const l = u(n); if (l) f(l); };\n set(\"iResolution\", (l) => gl.uniform3f(l, W, H, 1));\n set(\"iTime\", (l) => gl.uniform1f(l, s.time));\n set(\"iTimeDelta\", (l) => gl.uniform1f(l, s.delta));\n set(\"iFrame\", (l) => gl.uniform1i(l, s.frame));\n set(\"iMouse\", (l) => gl.uniform4fv(l, s.mouse));\n set(\"iPointer\", (l) => gl.uniform4fv(l, s.pointer));\n set(\"iClicks\", (l) => gl.uniform4fv(l, new Float32Array(s.clicks)));\n set(\"iTrail\", (l) => gl.uniform4fv(l, new Float32Array(s.trail)));\n // glslsandbox-style names, for complete programs.\n set(\"u_resolution\", (l) => gl.uniform2f(l, W, H));\n set(\"u_time\", (l) => gl.uniform1f(l, s.time));\n set(\"u_mouse\", (l) => gl.uniform2f(l, s.pointer[0], s.pointer[1]));\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n}\n\nasync function frameImage(s) {\n if (CONFIG.shader) {\n for (const [name, url] of Object.entries(s.video || {})) upload(name, await decode(url, \"of the video frame\"));\n drawShader(s);\n return glCanvas;\n }\n const markup = sizeSvg(s.svg, W, H);\n const url = URL.createObjectURL(new Blob([markup], { type: \"image/svg+xml\" }));\n try { return await decode(url, \"of the SVG frame\"); } finally { URL.revokeObjectURL(url); }\n}\n\nasync function capture(src) {\n // The WebGL canvas reads back exactly when nothing is composited under it.\n const direct = CONFIG.shader && (CONFIG.alpha || CONFIG.shader.opaque);\n if (direct) {\n if (CONFIG.send === \"png\") return new Promise((r) => glCanvas.toBlob(r, \"image/png\"));\n const px = new Uint8Array(W * H * 4);\n gl.readPixels(0, 0, W, H, gl.RGBA, gl.UNSIGNED_BYTE, px);\n const flipped = new Uint8Array(px.length), row = W * 4;\n for (let y = 0; y < H; y++) flipped.set(px.subarray((H - 1 - y) * row, (H - y) * row), y * row);\n return flipped;\n }\n ctx.clearRect(0, 0, W, H);\n if (!CONFIG.alpha) { ctx.fillStyle = CONFIG.background; ctx.fillRect(0, 0, W, H); }\n ctx.drawImage(src, 0, 0, W, H);\n if (CONFIG.send === \"png\") return new Promise((r) => out.toBlob(r, \"image/png\"));\n return ctx.getImageData(0, 0, W, H).data;\n}\n\n(async () => {\n if (CONFIG.shader) { setupShader(); await setControls(); }\n for (let i = 0; i < CONFIG.frames; i++) {\n const s = await (await fetch(\"/state?i=\" + i)).json();\n const body = await capture(await frameImage(s));\n const r = await fetch(\"/frame?i=\" + i, { method: \"POST\", body });\n if (!r.ok) return;\n }\n})().catch((e) => report((e && e.message) || String(e)));\n</script>\n</body>\n</html>\n`;\n}\n\n// ==========================================\n// The render loop\n// ==========================================\n\n/** One frame's state, as the page asks for it. */\nexport interface FrameState {\n time: number;\n delta: number;\n frame: number;\n mouse: [number, number, number, number];\n pointer: [number, number, number, number];\n clicks: number[];\n trail: number[];\n /** SVG markup to draw (vector frames). */\n svg?: string;\n /** Image control → local file to show this frame (video frames). */\n video?: Record<string, string>;\n}\n\n/** A render: the page to run, each frame's state, and where it goes. */\nexport interface RenderJob {\n page: PageConfig;\n state: (frame: number) => FrameState;\n plan: OutputPlan;\n fps: number;\n onProgress?: (frame: number, total: number) => void;\n}\n\n/** Frames written somewhere: a sink takes them in order and finishes. */\ninterface Sink {\n write(frame: number, data: Buffer): Promise<void>;\n finish(): Promise<void>;\n abort(): void;\n}\n\nfunction ffmpegSink(plan: OutputPlan, fps: number): Sink {\n const args = ['-y', '-hide_banner', '-loglevel', 'error', '-f', 'image2pipe', '-framerate', String(fps), '-c:v', 'png', '-i', '-', ...plan.ffmpegArgs!, '-r', String(fps), plan.path];\n const proc = spawn(ffmpegPath(), args, { stdio: ['pipe', 'ignore', 'pipe'] });\n let stderr = '';\n let failed: Error | null = null;\n proc.stderr!.on('data', (d) => (stderr += d.toString()));\n const done = new Promise<void>((resolve, reject) => {\n proc.on('error', (err: NodeJS.ErrnoException) => {\n failed = err.code === 'ENOENT' ? new Error(NO_FFMPEG) : new Error(`ffmpeg could not start: ${err.message}`);\n reject(failed);\n });\n proc.on('close', (code) => {\n if (code === 0) resolve();\n else reject((failed ??= new Error(`ffmpeg failed (exit ${code}): ${stderr.trim() || 'no message'}`)));\n });\n });\n done.catch(() => {});\n proc.stdin!.on('error', () => {});\n return {\n async write(_frame, data) {\n if (failed) throw failed;\n if (!proc.stdin!.write(data)) await new Promise((r) => proc.stdin!.once('drain', r));\n },\n async finish() {\n proc.stdin!.end();\n await done;\n },\n abort() {\n proc.kill('SIGKILL');\n },\n };\n}\n\nfunction pngSink(plan: OutputPlan): Sink {\n fs.mkdirSync(path.dirname(plan.path), { recursive: true });\n return {\n async write(frame, data) {\n await fs.promises.writeFile(plan.path.includes('%') ? sequencePath(plan.path, frame) : plan.path, data);\n },\n async finish() {},\n abort() {},\n };\n}\n\nasync function gifSink(plan: OutputPlan, width: number, height: number, fps: number): Promise<Sink> {\n // gifenc is CommonJS in Node: its functions may only be on the default export.\n const mod = (await import('gifenc')) as Partial<typeof import('gifenc')> & { default?: typeof import('gifenc') };\n const { GIFEncoder, quantize, applyPalette } = (typeof mod.GIFEncoder === 'function' ? mod : mod.default!) as typeof import('gifenc');\n const gif = GIFEncoder();\n const delay = 1000 / fps;\n return {\n async write(frame, data) {\n const rgba = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n const first = frame === 0 ? { repeat: 0 } : {};\n if (plan.alpha) {\n // One-bit alpha: each frame replaces the last (dispose 2), with a transparent palette entry.\n const palette = quantize(rgba, 256, { format: 'rgba4444', oneBitAlpha: true });\n const transparentIndex = palette.findIndex((c) => c[3] === 0);\n gif.writeFrame(applyPalette(rgba, palette, 'rgba4444'), width, height, {\n palette,\n delay,\n dispose: 2,\n ...(transparentIndex >= 0 ? { transparent: true, transparentIndex } : {}),\n ...first,\n });\n } else {\n const palette = quantize(rgba, 256);\n gif.writeFrame(applyPalette(rgba, palette), width, height, { palette, delay, ...first });\n }\n },\n async finish() {\n gif.finish();\n fs.mkdirSync(path.dirname(plan.path), { recursive: true });\n await fs.promises.writeFile(plan.path, gif.bytes());\n },\n abort() {},\n };\n}\n\n/** Chrome's flags: GPU WebGL where there is one (Metal on macOS), SwiftShader otherwise. */\nfunction chromeArgs(userDataDir: string, width: number, height: number, url: string): string[] {\n return [\n '--headless=new',\n `--user-data-dir=${userDataDir}`,\n `--window-size=${Math.max(64, width)},${Math.max(64, height)}`,\n '--use-gl=angle',\n ...(process.platform === 'darwin' ? ['--use-angle=metal'] : []),\n '--enable-webgl',\n '--enable-unsafe-swiftshader',\n '--ignore-gpu-blocklist',\n '--disable-gpu-sandbox',\n '--force-color-profile=srgb',\n '--no-first-run',\n '--no-default-browser-check',\n '--disable-background-networking',\n '--disable-background-timer-throttling',\n '--disable-renderer-backgrounding',\n '--disable-extensions',\n url,\n ];\n}\n\nconst STALL_MS = 60_000;\n\n/**\n * Runs a render: serves the page to headless Chrome on 127.0.0.1, hands it\n * each frame's state, and writes the frames it posts back. Resolves with the\n * output path (a PNG sequence's pattern).\n */\nexport async function runRender(job: RenderJob): Promise<string> {\n const { page, plan, fps, onProgress } = job;\n const total = page.frames;\n const chromeBin = findChrome();\n if (!chromeBin) {\n throw new Error('Chrome or Chromium is needed to render, and none was found. Install Google Chrome, run \"npx playwright install chromium\", or set CHROME_PATH.');\n }\n fs.mkdirSync(path.dirname(plan.path), { recursive: true });\n const sink = plan.format === 'gif' ? await gifSink(plan, page.width, page.height, fps) : plan.format === 'png' ? pngSink(plan) : ffmpegSink(plan, fps);\n const html = renderPageHtml(page);\n const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flair-chrome-'));\n\n return new Promise<string>((resolve, reject) => {\n let chrome: ChildProcess | null = null;\n let finished = false;\n let written = 0;\n let stall: NodeJS.Timeout | undefined;\n const sockets = new Set<import('node:net').Socket>();\n\n const cleanup = () => {\n clearTimeout(stall);\n try {\n chrome?.kill('SIGKILL');\n } catch {}\n for (const s of sockets) s.destroy();\n server.close();\n fs.rm(userDataDir, { recursive: true, force: true }, () => {});\n };\n const fail = (err: Error) => {\n if (finished) return;\n finished = true;\n sink.abort();\n cleanup();\n reject(err);\n };\n const succeed = () => {\n if (finished) return;\n finished = true;\n cleanup();\n resolve(plan.path);\n };\n const watchdog = () => {\n clearTimeout(stall);\n stall = setTimeout(() => fail(new Error(`The render stalled: no frame for ${STALL_MS / 1000} s after frame ${written} of ${total}. Is WebGL2 available to headless Chrome?`)), STALL_MS);\n };\n\n const server = http.createServer((req, res) => {\n const url = new URL(req.url || '/', 'http://127.0.0.1');\n if (url.pathname === '/' && req.method === 'GET') {\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(html);\n return;\n }\n if (url.pathname === '/state') {\n const i = Number(url.searchParams.get('i'));\n try {\n const s = job.state(i);\n const body = { ...s, ...(s.video ? { video: Object.fromEntries(Object.keys(s.video).map((k) => [k, `/video?i=${i}&name=${encodeURIComponent(k)}`])) } : {}) };\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n } catch (e) {\n res.writeHead(500);\n res.end();\n fail(e as Error);\n }\n return;\n }\n if (url.pathname === '/video') {\n const i = Number(url.searchParams.get('i'));\n const file = job.state(i).video?.[url.searchParams.get('name') ?? ''];\n if (!file) {\n res.writeHead(404);\n res.end();\n return;\n }\n res.writeHead(200, { 'Content-Type': 'image/png' });\n fs.createReadStream(file).pipe(res);\n return;\n }\n if (url.pathname === '/frame' && req.method === 'POST') {\n const i = Number(url.searchParams.get('i'));\n const chunks: Buffer[] = [];\n req.on('data', (c: Buffer) => chunks.push(c));\n req.on('end', () => {\n sink\n .write(i, Buffer.concat(chunks))\n .then(async () => {\n written++;\n watchdog();\n onProgress?.(written, total);\n res.writeHead(200);\n res.end('ok');\n if (written >= total) {\n await sink.finish();\n succeed();\n }\n })\n .catch((e: Error) => {\n res.writeHead(500);\n res.end();\n fail(e);\n });\n });\n return;\n }\n if (url.pathname === '/error' && req.method === 'POST') {\n let message = '';\n req.on('data', (c) => (message += c));\n req.on('end', () => {\n res.writeHead(200);\n res.end();\n fail(new Error(message || 'The render page failed.'));\n });\n return;\n }\n res.writeHead(404);\n res.end();\n });\n server.on('connection', (s) => {\n sockets.add(s);\n s.on('close', () => sockets.delete(s));\n });\n\n server.listen(0, '127.0.0.1', () => {\n const { port } = server.address() as import('node:net').AddressInfo;\n chrome = spawn(chromeBin, chromeArgs(userDataDir, page.width, page.height, `http://127.0.0.1:${port}/`), { stdio: 'ignore' });\n chrome.on('error', (err) => fail(new Error(`Could not launch Chrome at ${chromeBin}: ${err.message}`)));\n chrome.on('exit', (code) => {\n if (!finished && written < total) fail(new Error(`Chrome quit (exit ${code}) after ${written} of ${total} frames.`));\n });\n watchdog();\n });\n });\n}\n\n// ==========================================\n// Shader export\n// ==========================================\n\n/** Where the pointer is in a recording: nowhere (`none`), or the scripted pointer of {@link scriptedPointer} (`auto`). */\nexport type PointerMode = 'none' | 'auto';\n\nexport interface ExportOptions {\n /** GLSL fragment shader source (Shadertoy-style `mainImage`). */\n source: string;\n /**\n * Control values over the declared defaults. An image control takes a\n * URL, a `data:` URL, or a local file path (read into a `data:` URL).\n */\n values?: Record<string, ControlValue>;\n /** Destination: .mp4, .webm, .mov, .gif, or PNG frames (a folder or `out_%04d.png`). */\n outputPath: string;\n width?: number;\n height?: number;\n fps?: number;\n /** Seconds (default 3). */\n duration?: number;\n /** Keep the shader's alpha (WebM, MOV, GIF and PNG; MP4 is always opaque). */\n alpha?: boolean;\n /**\n * The shader draws over other content (`fragColor.a` is coverage): without\n * `alpha` it is composited over `background`. Otherwise its alpha is\n * ignored without `alpha`, as on an opaque canvas.\n */\n transparent?: boolean;\n /** CSS colour under transparent pixels when `alpha` is off (default black). */\n background?: string;\n /** The pointer: `none` (default) or `auto`, a scripted pointer that hovers, wanders and clicks. */\n pointer?: PointerMode;\n onProgress?: (frame: number, total: number) => void;\n}\n\nconst MIME: Record<string, string> = {\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.bmp': 'image/bmp',\n '.svg': 'image/svg+xml',\n};\n\n/**\n * An image for the page, as a `data:` URL: a local file is read, an\n * `http(s):` URL fetched (so CORS does not matter); `data:` URLs stay.\n */\nexport async function imageDataUrl(source: string, cwd = process.cwd()): Promise<string> {\n if (/^data:/i.test(source) || source === '') return source;\n if (/^https?:/i.test(source)) {\n const res = await fetch(source);\n if (!res.ok) throw new Error(`Could not download the image at ${source} (HTTP ${res.status}).`);\n const type = res.headers.get('content-type')?.split(';')[0] || MIME[path.extname(new URL(source).pathname).toLowerCase()] || 'application/octet-stream';\n return `data:${type};base64,${Buffer.from(await res.arrayBuffer()).toString('base64')}`;\n }\n const file = path.resolve(cwd, source.replace(/^file:\\/\\//, ''));\n if (!fs.existsSync(file)) throw new Error(`There is no image at ${file}.`);\n const type = MIME[path.extname(file).toLowerCase()];\n if (!type) throw new Error(`${path.basename(file)} is not an image Flair can show; give it a PNG, JPEG, GIF, WebP, AVIF, BMP or SVG.`);\n return `data:${type};base64,${fs.readFileSync(file).toString('base64')}`;\n}\n\n/**\n * The fragment shader the page compiles: a Shadertoy-style `mainImage`\n * source goes in `wrapSource`, as `ShaderRenderer` compiles it. A complete\n * program (one with its own `void main()`, glslsandbox-style, reading\n * `u_time` and `u_resolution`) is kept as written, with `#version 300 es`\n * added when it has none.\n */\nexport function fragmentSource(source: string): string {\n if (!/\\bvoid\\s+main\\s*\\(\\s*\\)/.test(source)) return wrapSource(source);\n return /^\\s*#version\\b/.test(source) ? source : `#version 300 es\\n#line 1\\n${source}`;\n}\n\n/** Shader controls with their values, images as `data:` URLs, ready for the page. */\nexport async function pageControls(source: string, values: Record<string, ControlValue> = {}): Promise<PageControl[]> {\n return Promise.all(\n parseControls(source).map(async (c) => {\n let value = controlValue(c, values);\n if (c.type === 'sampler2D' && typeof value === 'string') value = await imageDataUrl(value);\n return { name: c.name, type: c.type, value, ...(c.texture ? { texture: c.texture } : {}) };\n }),\n );\n}\n\n/** Frame times and pointer uniforms for `frames` frames at `fps`. */\nexport function frameStates(frames: number, fps: number, width: number, height: number, pointer: PointerMode): FrameState[] {\n const times = Array.from({ length: frames }, (_, i) => i / fps);\n const scripted = pointer === 'auto' ? scriptedPointer(width, height, times) : undefined;\n const none = { mouse: [0, 0, 0, 0] as [number, number, number, number], pointer: [0, 0, 0, 0] as [number, number, number, number], clicks: new Array(MAX_CLICKS * 4).fill(0), trail: new Array(MAX_TRAIL * 4).fill(0) };\n return times.map((time, i) => ({ time, delta: i === 0 ? 0 : 1 / fps, frame: i, ...(scripted?.[i] ?? none) }));\n}\n\n/**\n * Renders a shader animation frame by frame with WebGL2 in headless Chrome,\n * the way `ShaderRenderer` draws it (the same GLSL wrapper and built-ins,\n * images, int and bool controls), and writes MP4 (H.264), WebM (VP9, with\n * alpha when asked), MOV (ProRes 4444), GIF or PNG frames. MP4, WebM and\n * MOV need ffmpeg; GIF and PNG do not.\n */\nexport async function exportMotionGraphic(options: ExportOptions): Promise<string> {\n const { source, values = {}, outputPath, fps = 30, duration = 3, onProgress } = options;\n const width = Math.max(1, Math.round(options.width ?? 720));\n const height = Math.max(1, Math.round(options.height ?? 720));\n const frames = Math.max(1, Math.round(duration * fps));\n const plan = planOutput(outputPath, { alpha: options.alpha, frames });\n if (plan.format === 'svg') throw new Error('A shader cannot be written as SVG; write .mp4, .webm, .mov, .gif or .png frames.');\n const states = frameStates(frames, fps, width, height, options.pointer ?? 'none');\n return runRender({\n page: {\n width,\n height,\n frames,\n alpha: plan.alpha,\n background: options.background ?? '#000000',\n send: plan.format === 'gif' ? 'rgba' : 'png',\n shader: {\n fragment: fragmentSource(source),\n controls: await pageControls(source, values),\n opaque: !plan.alpha && !options.transparent,\n },\n },\n state: (i) => states[i]!,\n plan,\n fps,\n onProgress,\n });\n}\n"],"mappings":"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","names":["type"]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
SVG_PICTURE,
|
|
3
3
|
svgDuration,
|
|
4
4
|
svgFrame
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-DA4UUSDO.js";
|
|
6
6
|
|
|
7
7
|
// src/vector/static.ts
|
|
8
8
|
var DROPPED = /* @__PURE__ */ new Set(["script", "foreignobject", "iframe", "object", "embed"]);
|
|
@@ -153,4 +153,4 @@ export {
|
|
|
153
153
|
svgFrameMarkup,
|
|
154
154
|
svgFrameTree
|
|
155
155
|
};
|
|
156
|
-
//# sourceMappingURL=chunk-
|
|
156
|
+
//# sourceMappingURL=chunk-AUH3PEFW.js.map
|
|
@@ -535,6 +535,7 @@ function svgDuration(motion, steps = []) {
|
|
|
535
535
|
}
|
|
536
536
|
return Math.max(1, end);
|
|
537
537
|
}
|
|
538
|
+
var SCROLL_RANGE = [0.1, 0.6];
|
|
538
539
|
var SvgPlayer = class {
|
|
539
540
|
constructor(container, motion, options = {}) {
|
|
540
541
|
this.container = container;
|
|
@@ -545,6 +546,7 @@ var SvgPlayer = class {
|
|
|
545
546
|
this.loop = this.trigger ? false : options.loop ?? true;
|
|
546
547
|
this.speed = options.speed ?? 1;
|
|
547
548
|
this.hideWhenPaused = Boolean(options.hideWhenPaused);
|
|
549
|
+
this.scrollRange = options.scrollRange ?? SCROLL_RANGE;
|
|
548
550
|
this.length = options.duration ?? svgDuration(motion, this.steps) + (options.hold ?? 600);
|
|
549
551
|
this.view = svgView(motion, options);
|
|
550
552
|
container.appendChild(this.view.element);
|
|
@@ -572,6 +574,14 @@ var SvgPlayer = class {
|
|
|
572
574
|
});
|
|
573
575
|
this.intersectionObserver.observe(this.view.element);
|
|
574
576
|
}
|
|
577
|
+
if (this.trigger === "scroll") {
|
|
578
|
+
this.length = options.duration ?? svgDuration(motion);
|
|
579
|
+
this.triggerTarget = options.triggerTarget ?? container;
|
|
580
|
+
this.followingScroll = true;
|
|
581
|
+
this.listen(true);
|
|
582
|
+
this.followScroll();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
575
585
|
if (this.trigger) {
|
|
576
586
|
this.length = this.trigger === "click" ? Infinity : options.duration ?? svgDuration(motion);
|
|
577
587
|
this.triggerTarget = options.triggerTarget ?? container;
|
|
@@ -607,6 +617,10 @@ var SvgPlayer = class {
|
|
|
607
617
|
wasPlayingBeforeHidden = false;
|
|
608
618
|
hasEnteredViewport = false;
|
|
609
619
|
intersectionObserver;
|
|
620
|
+
scrollRange;
|
|
621
|
+
/** A scroll trigger's pending update (one per frame). */
|
|
622
|
+
scrollFrame = 0;
|
|
623
|
+
followingScroll = false;
|
|
610
624
|
/** The loop length in ms. */
|
|
611
625
|
get duration() {
|
|
612
626
|
return this.length;
|
|
@@ -641,6 +655,27 @@ var SvgPlayer = class {
|
|
|
641
655
|
this.time = this.loop ? (ms % this.length + this.length) % this.length : Math.min(Math.max(ms, 0), this.length);
|
|
642
656
|
this.view.draw(this.time, this.steps);
|
|
643
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Shows the picture at `progress` of the way through it, 0–1: how hosts
|
|
660
|
+
* scrub it themselves (a slider, a scroll position of their own). A scroll
|
|
661
|
+
* trigger stops following the page once a host calls this.
|
|
662
|
+
*/
|
|
663
|
+
setProgress(progress) {
|
|
664
|
+
if (this.followingScroll) {
|
|
665
|
+
this.followingScroll = false;
|
|
666
|
+
this.listen(false);
|
|
667
|
+
}
|
|
668
|
+
this.playing = false;
|
|
669
|
+
this.seek(Math.min(1, Math.max(0, progress)) * (Number.isFinite(this.length) ? this.length : svgDuration(this.motion, this.steps)));
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* A scroll trigger's playback position, 0–1, for the trigger target's
|
|
673
|
+
* `progress` through the viewport (see {@link SvgPlayerOptions.scrollRange}).
|
|
674
|
+
*/
|
|
675
|
+
scrollPosition(progress) {
|
|
676
|
+
const [from, to] = this.scrollRange;
|
|
677
|
+
return to > from ? Math.min(1, Math.max(0, (progress - from) / (to - from))) : progress >= to ? 1 : 0;
|
|
678
|
+
}
|
|
644
679
|
setSpeed(speed) {
|
|
645
680
|
this.speed = speed;
|
|
646
681
|
}
|
|
@@ -673,17 +708,36 @@ var SvgPlayer = class {
|
|
|
673
708
|
dispose() {
|
|
674
709
|
this.disposed = true;
|
|
675
710
|
this.playing = false;
|
|
676
|
-
if (this.trigger) this.listen(false);
|
|
711
|
+
if (this.trigger && (this.trigger !== "scroll" || this.followingScroll)) this.listen(false);
|
|
712
|
+
if (this.scrollFrame && typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(this.scrollFrame);
|
|
677
713
|
this.intersectionObserver?.disconnect();
|
|
678
714
|
this.view.element.remove();
|
|
679
715
|
}
|
|
680
716
|
onEnter = () => this.setActive(true);
|
|
681
717
|
onLeave = () => this.setActive(false);
|
|
682
718
|
onClick = () => this.advance();
|
|
719
|
+
/** Seeks to where the trigger target is in the viewport. */
|
|
720
|
+
followScroll = () => {
|
|
721
|
+
this.scrollFrame = 0;
|
|
722
|
+
if (!this.followingScroll || this.disposed) return;
|
|
723
|
+
const rect = this.triggerTarget.getBoundingClientRect();
|
|
724
|
+
const viewport = window.innerHeight || document.documentElement.clientHeight || 1;
|
|
725
|
+
const travel = viewport + rect.height;
|
|
726
|
+
const progress = travel > 0 ? Math.min(1, Math.max(0, (viewport - rect.top) / travel)) : 0;
|
|
727
|
+
this.seek(this.scrollPosition(progress) * this.length);
|
|
728
|
+
};
|
|
729
|
+
/** A scroll or resize: follow it on the next frame (or now, without frames). */
|
|
730
|
+
onScroll = () => {
|
|
731
|
+
if (typeof requestAnimationFrame === "undefined") return this.followScroll();
|
|
732
|
+
if (!this.scrollFrame) this.scrollFrame = requestAnimationFrame(this.followScroll);
|
|
733
|
+
};
|
|
683
734
|
listen(on) {
|
|
684
735
|
const target = this.triggerTarget;
|
|
685
736
|
const method = on ? "addEventListener" : "removeEventListener";
|
|
686
|
-
if (this.trigger === "
|
|
737
|
+
if (this.trigger === "scroll") {
|
|
738
|
+
window[method]("scroll", this.onScroll, { capture: true, passive: true });
|
|
739
|
+
window[method]("resize", this.onScroll, { passive: true });
|
|
740
|
+
} else if (this.trigger === "hover") {
|
|
687
741
|
target[method]("pointerenter", this.onEnter);
|
|
688
742
|
target[method]("pointerleave", this.onLeave);
|
|
689
743
|
} else if (this.trigger === "press") {
|
|
@@ -774,7 +828,8 @@ export {
|
|
|
774
828
|
parseSvg,
|
|
775
829
|
svgView,
|
|
776
830
|
svgDuration,
|
|
831
|
+
SCROLL_RANGE,
|
|
777
832
|
SvgPlayer,
|
|
778
833
|
animateCount
|
|
779
834
|
};
|
|
780
|
-
//# sourceMappingURL=chunk-
|
|
835
|
+
//# sourceMappingURL=chunk-DA4UUSDO.js.map
|