pixi-effects 0.26.2 → 0.27.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 (68) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +8 -10
  3. package/ai/CHAT.md +1 -1
  4. package/ai/chat-template.html +4 -6
  5. package/ai/tools/render.mjs +16 -11
  6. package/dist/{Base-CpdDs4HT.d.ts → Base-CVezIEdo.d.ts} +2 -4
  7. package/dist/{Base-TbBhHPrG.d.cts → Base-DzC3i919.d.cts} +2 -4
  8. package/dist/Composition-4TYDRRRY.js +7 -0
  9. package/dist/{Composition-H4QA24MV.js.map → Composition-4TYDRRRY.js.map} +1 -1
  10. package/dist/Composition-QCOWQSDB.cjs +16 -0
  11. package/dist/{Composition-HM4YT5L7.cjs.map → Composition-QCOWQSDB.cjs.map} +1 -1
  12. package/dist/Controller.d.cts +2 -3
  13. package/dist/Controller.d.ts +2 -3
  14. package/dist/{Movie-i05Oh9mJ.d.cts → Movie-CQ0Nz62a.d.cts} +12 -6
  15. package/dist/{Movie-DVeDTdnc.d.ts → Movie-M9dKm9dI.d.ts} +12 -6
  16. package/dist/Presenter.d.cts +2 -3
  17. package/dist/Presenter.d.ts +2 -3
  18. package/dist/{blendModes-YV43K6E7.cjs → blendModes-HDOP6R7T.cjs} +5 -5
  19. package/dist/{blendModes-YV43K6E7.cjs.map → blendModes-HDOP6R7T.cjs.map} +1 -1
  20. package/dist/{blendModes-G3NOAU3T.js → blendModes-PAVTW3BO.js} +4 -4
  21. package/dist/{blendModes-G3NOAU3T.js.map → blendModes-PAVTW3BO.js.map} +1 -1
  22. package/dist/{chunk-TLYTYBWX.js → chunk-45QKLD6M.js} +9 -11
  23. package/dist/chunk-45QKLD6M.js.map +1 -0
  24. package/dist/{chunk-GBUM7X2S.js → chunk-CKTS6G6I.js} +7 -11
  25. package/dist/chunk-CKTS6G6I.js.map +1 -0
  26. package/dist/{chunk-555URQHQ.cjs → chunk-NDR2VQFV.cjs} +91 -95
  27. package/dist/chunk-NDR2VQFV.cjs.map +1 -0
  28. package/dist/{chunk-3DLYAPK7.cjs → chunk-P4GY3Z4G.cjs} +5 -5
  29. package/dist/{chunk-3DLYAPK7.cjs.map → chunk-P4GY3Z4G.cjs.map} +1 -1
  30. package/dist/{chunk-WPXDRSGN.cjs → chunk-RQMI7EJT.cjs} +1170 -280
  31. package/dist/chunk-RQMI7EJT.cjs.map +1 -0
  32. package/dist/{chunk-X3HB4DSR.js → chunk-TGJACVLD.js} +1150 -263
  33. package/dist/chunk-TGJACVLD.js.map +1 -0
  34. package/dist/{chunk-AXZUKNXF.js → chunk-TYWFKCPY.js} +4 -4
  35. package/dist/{chunk-AXZUKNXF.js.map → chunk-TYWFKCPY.js.map} +1 -1
  36. package/dist/{chunk-ECA2WNRX.cjs → chunk-ZMAD3KJU.cjs} +9 -11
  37. package/dist/chunk-ZMAD3KJU.cjs.map +1 -0
  38. package/dist/index.cjs +189 -116
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +16 -14
  41. package/dist/index.d.ts +16 -14
  42. package/dist/index.js +136 -45
  43. package/dist/index.js.map +1 -1
  44. package/dist/loader.css +1 -1
  45. package/dist/three.cjs +6 -6
  46. package/dist/three.d.cts +2 -3
  47. package/dist/three.d.ts +2 -3
  48. package/dist/three.js +2 -2
  49. package/dist/{types-DSGwN5w1.d.ts → types-DUVefWiB.d.cts} +128 -3
  50. package/dist/{types-DSGwN5w1.d.cts → types-DUVefWiB.d.ts} +128 -3
  51. package/docs/api.md +16 -6
  52. package/docs/dsl.md +14 -14
  53. package/llms-full.txt +38 -28
  54. package/llms.txt +1 -1
  55. package/package.json +3 -4
  56. package/skills/pixi-effects/SKILL.md +1 -1
  57. package/skills/pixi-effects/reference/cheatsheet.md +3 -3
  58. package/skills/pixi-effects/reference/pitfalls.md +3 -3
  59. package/skills/pixi-effects/reference/recipes.md +1 -1
  60. package/skills/pixi-effects/template.html +6 -8
  61. package/dist/Composition-H4QA24MV.js +0 -7
  62. package/dist/Composition-HM4YT5L7.cjs +0 -16
  63. package/dist/chunk-555URQHQ.cjs.map +0 -1
  64. package/dist/chunk-ECA2WNRX.cjs.map +0 -1
  65. package/dist/chunk-GBUM7X2S.js.map +0 -1
  66. package/dist/chunk-TLYTYBWX.js.map +0 -1
  67. package/dist/chunk-WPXDRSGN.cjs.map +0 -1
  68. package/dist/chunk-X3HB4DSR.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.27.0
4
+
5
+ **Changed (breaking)**
6
+
7
+ - **The library no longer uses GSAP.** The timeline is the library's own (`src/core/pure/`): a value at a time is computed from the script and the time alone, never from the way the playhead came to it, so scrubbing, looping, reverse play and export all give the same picture at the same time. `gsap` leaves the peer dependencies (`npm install pixi-effects pixi.js`), and every page loses the `gsap` and `gsap/PixiPlugin` lines of its import map (a page that still has them works; nothing imports them). Measured against the GSAP build on 92 pages by frame hash: 77 identical, 9 within 3/255, and the rest explained (four pages use `expo`, whose formula differs between GSAP 3.12 and 3.15; one page is not reproducible even with GSAP). The timeline itself is 1.3 to 11 times faster per frame and 25 KB (gzip) lighter for a page; a whole frame is dominated by drawing and changes by 1 to 15 %.
8
+ - **`expo` is GSAP 3.15's curve** (`2^(10(p-1))·p + p^6(1-p)`, which lands exactly on 1): up to 0.78 % of the range away from 3.12's, so a page made with 3.12 moves a little.
9
+ - **Where GSAP answered by the way the playhead came, the answer is now the script's**: overlapping tweens on one property (the one that started last wins), two items with the same start, a `set` where a tween ends (GSAP could lose it), a `from` after another segment (it ends where that segment ended).
10
+ - **`blur`, `blurX`, `blurY`, `blurPadding`, `colorize`, `colorizeAmount`, `colorMatrixFilter`, `lineColor`, `lineAlpha` are no longer keys** (they were GSAP PixiPlugin shorthands for a filter on the layer): the lint says so with the likely name; use a named filter and animate `filters.<name>.<prop>`. `scale`, `anchor`, `pivot`, `skew` (degrees), `rotation` (degrees), `position`, `tilePosition`, `tileScale`, `tint` and `autoAlpha` stay.
11
+ - `ease` names are the familiar ones (`power1…4`, `sine`, `expo`, `circ`, `back`, `elastic`, `bounce`, `steps(n)`, `spring…`, `cubic-bezier(…)`); a name nobody knows still warns and runs as `power1.out`. `delay` in a keyframe's vars moves its start; `onStart`, `onComplete`, `onRepeat` and `onReverseComplete` are said once and ignored.
12
+ - `movie.timeline` is the library's `Timeline` (`time(t)`, `duration()`, `segments()`), not a GSAP timeline.
13
+
14
+ **Added**
15
+
16
+ - `ease(name)` is exported: the function behind an ease name.
17
+ - Colour strings that Pixi can read (names, `hsl()`, `oklch()`) interpolate in `'rgb'` colour space as rgba.
18
+
19
+ ## 0.26.3
20
+
21
+ **Added**
22
+
23
+ - **Sound-only export.** `render({ format: 'wav' })` (16-bit PCM) and `render({ format: 'ogg' })` (Opus) write the movie's mixed sound without drawing a frame, with `range` cutting it; `pixi-effects render page.html -o mix.wav` does it from a script. A movie with no sound says so instead of writing an empty file.
24
+ - **Video codec and bitrate options.** `render({ video: { codec, bitrate, hardware, keyFrameInterval } })`: `codec` is `avc`, `hevc`, `vp9`, `av1` or `vp8` (checked against the container and against what this browser can encode: the usual one falls back to the next that works with a warning, a codec you name is used or the error names the one that would work, and `h264` gets a did-you-mean); `bitrate` is a quality name or bits a second (`8_000_000`, `'8M'`, `'800k'`); `hardware` picks the encoder; `keyFrameInterval` (seconds, default 2). The render command gains `--video-bitrate`.
25
+ - **Relative keyframe values.** `to`, `from` and `set` accept `'+=36'` / `'-=36'`, measured from where the layer stands, with an expression on the right (`'-=GW*0.1'`): `from: { x: '-=36' }` is the usual slide-in, `to: { y: '+=40' }` a nudge, and keyframes one after another stack in any order of seeking. Until now `'+=36'` was evaluated as an expression and became **0 with no warning**; in `initial`, a style or the start of a `from` + `to` it now warns and is read as the number. (GSAP supported relative values all along; the keyframe pipeline did not let them through.)
26
+
27
+ **Changed**
28
+
29
+ - **Video: the pauses of reverse play and of a loop are read ahead.** A looping layer reads the first frames of the file (and a pass behind them) before the end comes, so the wrap shows no pause (a 30 s loop of two heavy layers: 0 pauses, it had 3). Reverse play reads the next window in the background while the held frames last, and each window now just fits what is held: one heavy layer played backwards shows 29.3 distinct frames a second with 1 pause in 6 s (24.8 and 6 pauses without the read-ahead, 14.1 and 31 before 0.26.2); four heavy layers, each reading its own window, reach 18 a second. Frames are now let go by where the playhead is (the ones it has passed first), not by age. Measured with the on-screen frame-number method of 0.26.2, on an M1 Pro.
30
+
3
31
  ## 0.26.2
4
32
 
5
33
  **Fixed**
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pixi-effects
2
2
 
3
- > **Status**: experimental — current release `0.26.2`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
3
+ > **Status**: experimental — current release `0.27.0`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
4
4
 
5
5
  **[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 50 portfolio pieces written as plain data by AI models · **[Examples →](https://yjmtmtk.github.io/pixi-effects/examples/)** · 15 numbered examples, a music lab + an in-browser playground.
6
6
 
@@ -12,7 +12,7 @@ Give any AI (ChatGPT, Claude, Codex, Cursor, Claude Code …) one sentence:
12
12
 
13
13
  That is all it needs. **To register it as a skill** in your agent (Claude Code, Codex, Cursor, Gemini CLI, Copilot, OpenCode, …): `npx skills add yjmtmtk/pixi-effects` (update with `npx skills update`). In Claude Code you can also run `/plugin install pixi-effects --marketplace yjmtmtk/pixi-effects`. **If you are the AI** that was asked to do this, do not read the whole repository: if you can run commands (a shell, Node 22, Chrome), read [`AGENTS.md`](./AGENTS.md) and [`skills/pixi-effects/SKILL.md`](./skills/pixi-effects/SKILL.md); if you can only write text (a browser chat), fetch https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/CHAT.md and follow it. You will write the video as plain data in one HTML file, check it (the check tool writes a contact sheet to look at), and hand it over; the person opens the file and downloads the MP4 with the button in the player bar.
14
14
 
15
- Declarative composition and video rendering for the web. After Effects-style timelines on top of [PixiJS v8](https://pixijs.com/) and [GSAP](https://gsap.com/), with strict TypeScript types. Render to MP4 / WebM / MOV via [mediabunny](https://mediabunny.dev/).
15
+ Declarative composition and video rendering for the web. After Effects-style timelines on top of [PixiJS v8](https://pixijs.com/), with a small timeline of its own and strict TypeScript types. The ease names and the way `to` / `from` / `set` read are [GSAP](https://gsap.com/)'s, with thanks. Render to MP4 / WebM / MOV via [mediabunny](https://mediabunny.dev/).
16
16
 
17
17
  - **Declarative DSL** — describe your composition as a tree of typed sequences (text, image, video, audio, shapes, nested compositions). No imperative tween code.
18
18
  - **Sound effects without files** — `{ type: 'audio', sfx: 'swoosh', at: 2 }`: 11 synthesised presets (click, pop, swoosh, riser, hit, chime …) with `pitch` / `brightness` / `seed`, or your own `voices`; deterministic and in the exported file at the same moments as in the browser; `movie.inspectAudio()` checks them as numbers.
@@ -29,12 +29,12 @@ Declarative composition and video rendering for the web. After Effects-style tim
29
29
  - **Built-in player UI — or your own** — a drop-in HTML5-`<video>`-style overlay controller (play, scrub, mute, volume, fullscreen, export-to-file) whose colours are one option (`theme: { accent: '#ff4d6d' }`); or build a player yourself on the movie's `<video>`-named events (`play pause ended seeking seeked volumechange error`).
30
30
  - **MP4 / WebM / MOV export** — pick container and quality from the controller, call `movie.render()` from code, or render headless from the command line (`pixi-effects-render`).
31
31
  - **A timeline you can read and scrub** — `movie.timelineChart()` draws every layer as a bar on a time axis; `pixi-effects-view` opens the page with that timeline under it, zoomable, with a playhead you can drag.
32
- - **Tiny dependency surface** — only `mediabunny` (runtime) plus PixiJS and GSAP (peer deps). three.js is an *optional* peer, needed only if you import `pixi-effects/three`.
32
+ - **Tiny dependency surface** — only `mediabunny` (runtime) plus PixiJS (a peer dep). three.js is an *optional* peer, needed only if you import `pixi-effects/three`.
33
33
 
34
34
  ## Install
35
35
 
36
36
  ```bash
37
- npm install pixi-effects pixi.js gsap
37
+ npm install pixi-effects pixi.js
38
38
  npm install three # optional — only if you import `pixi-effects/three`
39
39
  npm install pixi-filters # optional — only if a layer uses a named filter other than blur / noise / alpha / colorMatrix
40
40
  ```
@@ -48,11 +48,9 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
48
48
  {
49
49
  "imports": {
50
50
  "pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
51
- "gsap": "https://esm.sh/gsap@3.12.5",
52
- "gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
53
51
  "mediabunny": "https://esm.sh/mediabunny",
54
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.2/dist/index.js",
55
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.2/dist/Controller.js"
52
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.27.0/dist/index.js",
53
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.27.0/dist/Controller.js"
56
54
  }
57
55
  }
58
56
  </script>
@@ -63,7 +61,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
63
61
  </script>
64
62
  ```
65
63
 
66
- Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.26.2/dist/index.js`) rather than a CDN-rebundled build such as `esm.sh/pixi-effects` or jsDelivr's `+esm`: the entries (`pixi-effects`, `…/controller`, `…/three`) share internal chunks, which only works when each file is served untouched.
64
+ Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.27.0/dist/index.js`) rather than a CDN-rebundled build such as `esm.sh/pixi-effects` or jsDelivr's `+esm`: the entries (`pixi-effects`, `…/controller`, `…/three`) share internal chunks, which only works when each file is served untouched.
67
65
 
68
66
  > **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
69
67
  >
@@ -79,7 +77,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
79
77
  "imports": {
80
78
  "...": "(everything from the importmap above)",
81
79
  "three": "https://esm.sh/three@0.178.0",
82
- "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.2/dist/three.js"
80
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.27.0/dist/three.js"
83
81
  }
84
82
  }
85
83
  </script>
package/ai/CHAT.md CHANGED
@@ -53,7 +53,7 @@ That is a complete, working video (the block between the two markers). Change th
53
53
  - **Order.** Layers are drawn in array order: later = on top. Put the background rect first.
54
54
  - **Units.** Pixels from the top-left; angles in **degrees**; colours `'#rrggbb'`. Any number may be an expression string: `'GW/2'`, `'GH - 80'`, `'min(GW, GH) * 0.4'`.
55
55
  - **Anchors.** `rect`, `circle`, `ellipse` are centred on `x, y`. **Text and image are top-left**: add `anchorX: 0.5, anchorY: 0.5` to centre them. A bar that grows from its base needs `anchorY: 1`.
56
- - **Animate with keyframes.** `{ at, duration, ease, to: {...} }` animates to a value, `from: {...}` from one, `from` + `to` both (a delayed fade-in needs no `initial.alpha`), `set: {...}` jumps. Eases are GSAP names: `'power2.out'`, `'back.out(1.7)'`, `'none'`.
56
+ - **Animate with keyframes.** `{ at, duration, ease, to: {...} }` animates to a value, `from: {...}` from one, `from` + `to` both (a delayed fade-in needs no `initial.alpha`), `set: {...}` jumps. Eases are the familiar names: `'power2.out'`, `'back.out(1.7)'`, `'none'`.
57
57
  - **Draw a line on:** `trimEnd: 0` in `initial`, then a keyframe `to: { trimEnd: 1 }` (every shape except `arc`).
58
58
  - **Shapes:** `shape: 'rect' | 'circle' | 'ellipse' | 'arc' | 'line' | 'polygon' | 'path'`. `rect`: `width height cornerRadius`; `circle`: `radius`; `line`: `from: [x, y], to: [x, y]` (canvas coordinates, no `x, y`); `path`: `d` (SVG path data). Style: `fillColor strokeColor strokeWidth fillAlpha`.
59
59
  - **Text:** `style` is a PixiJS text style (`fontSize fontFamily fontWeight fill letterSpacing align wordWrap wordWrapWidth lineHeight`). Use fonts that exist everywhere (`Georgia, serif`, `Arial, sans-serif`, `ui-monospace, monospace`); this page loads no web fonts. Estimate a text's width at about 0.6 × `fontSize` per character, keep important text 5 % away from the edges, and keep the bottom 60 px free (the player bar covers it in the preview, not in the file).
@@ -54,16 +54,14 @@
54
54
  window.__add = add;
55
55
  </script>
56
56
 
57
- <script type="importmap">
57
+ <script type="importmap">
58
58
  {
59
59
  "imports": {
60
60
  "pixi.js": "https://cdn.jsdelivr.net/npm/pixi.js@8.22.0/dist/pixi.min.mjs",
61
- "gsap": "https://cdn.jsdelivr.net/npm/gsap@3.12.5/+esm",
62
- "gsap/PixiPlugin": "https://cdn.jsdelivr.net/npm/gsap@3.12.5/PixiPlugin/+esm",
63
61
  "mediabunny": "https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm",
64
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.2/dist/index.js",
65
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.2/dist/Controller.js",
66
- "pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.2/dist/Presenter.js"
62
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.27.0/dist/index.js",
63
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.27.0/dist/Controller.js",
64
+ "pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.27.0/dist/Presenter.js"
67
65
  }
68
66
  }
69
67
  </script>
@@ -12,12 +12,12 @@
12
12
  * Exit code: 0 = the file was written, 1 = the page or the render failed (no file), 2 = bad usage.
13
13
  *
14
14
  * Options: -o, --out FILE (default ./<page name>.<format>; the container follows the extension: mp4 webm mov mkv) ·
15
- * --format mp4|webm|mov|mkv|pdf (a .pdf is the deck as pages: one picture per page of a movie with `stops`; --all-stops makes a page of every stop) · --quality very-low|low|medium|high|very-high (video and audio bitrate, default high) ·
15
+ * --format mp4|webm|mov|mkv|wav|ogg|pdf (wav / ogg: only the movie's sound, no picture; a .pdf is the deck as pages: one picture per page of a movie with `stops`; --all-stops makes a page of every stop) · --quality very-low|low|medium|high|very-high (video and audio bitrate, default high) ·
16
16
  * --range A:B (only seconds A to B; 2: runs to the end, :5 starts at the beginning) · --scene NAME (the span of a top-level layer: a movie's scene) · --scale S (output size, 0 < S <= 1: smaller picture, smaller file) ·
17
17
  * --draft (for looking: half size, low quality, no motion blur; a --quality you give still wins) ·
18
18
  * --motion-blur SAMPLES (2-64: each frame is drawn that many times over the shutter and averaged; overrides the page's own motionBlur) ·
19
19
  * --shutter FRACTION (with --motion-blur: how long the shutter is open, 0-1 of a frame, default 0.5) ·
20
- * --video-codec C · --audio-codec C · --query "a=1&b=2" (added to the page URL) · --fail-on-warn (exit 1 when the page
20
+ * --video-codec C (avc hevc vp9 av1 vp8) · --video-bitrate B (a quality or bits a second: 8M, 800k) · --audio-codec C · --query "a=1&b=2" (added to the page URL) · --fail-on-warn (exit 1 when the page
21
21
  * logged a warning; the file is still written) · --quiet (no progress line) · --timeout SECONDS (default 900) ·
22
22
  * --root DIR (static server root; default: the nearest folder above the page with dist/) · --chrome PATH
23
23
  */
@@ -27,14 +27,15 @@ import path from 'node:path';
27
27
  import { pathToFileURL } from 'node:url';
28
28
  import { findChrome, findRoot, serve, launchChrome, shown, sleep } from './check.mjs';
29
29
 
30
- const FORMATS = ['mp4', 'webm', 'mov', 'mkv', 'pdf'];
30
+ const FORMATS = ['mp4', 'webm', 'mov', 'mkv', 'wav', 'ogg', 'pdf'];
31
+ const SOUND_ONLY = ['wav', 'ogg'];
31
32
  const QUALITIES = ['very-low', 'low', 'medium', 'high', 'very-high'];
32
33
 
33
34
  /** 'clip.WEBM' → 'webm'; no extension → null; any other extension is an error. */
34
35
  export function formatFromPath(file) {
35
36
  const ext = path.extname(file).slice(1).toLowerCase();
36
37
  if (!ext) return null;
37
- if (!FORMATS.includes(ext)) throw new Error(`cannot tell the container from ".${ext}": use an output ending in mp4, webm, mov, mkv or pdf (or --format)`);
38
+ if (!FORMATS.includes(ext)) throw new Error(`cannot tell the container from ".${ext}": use an output ending in mp4, webm, mov, mkv, wav, ogg or pdf (or --format)`);
38
39
  return ext;
39
40
  }
40
41
 
@@ -43,18 +44,21 @@ export function defaultOutput(page, format) {
43
44
  }
44
45
 
45
46
  export function parseRenderArgs(argv) {
46
- const o = { range: null, scene: null, scale: null, draft: false, qualityGiven: false, page: null, out: null, format: null, quality: 'high', videoCodec: null, audioCodec: null, allStops: false, motionBlur: null, shutter: null, query: null, failOnWarn: false, quiet: false, timeout: 900, root: null, chrome: null };
47
+ const o = { range: null, scene: null, scale: null, draft: false, qualityGiven: false, page: null, out: null, format: null, quality: 'high', videoCodec: null, videoBitrate: null, audioCodec: null, allStops: false, motionBlur: null, shutter: null, query: null, failOnWarn: false, quiet: false, timeout: 900, root: null, chrome: null };
47
48
  const need = (i, name) => { if (i + 1 >= argv.length) throw new Error(`${name} needs a value`); return argv[i + 1]; };
48
49
  for (let i = 0; i < argv.length; i++) {
49
50
  const a = argv[i];
50
51
  if (a === '-o' || a === '--out') o.out = need(i++, a);
51
52
  else if (a === '--format') {
52
53
  o.format = need(i++, a).toLowerCase();
53
- if (!FORMATS.includes(o.format)) throw new Error(`--format must be mp4, webm, mov, mkv or pdf (got "${o.format}")`);
54
+ if (!FORMATS.includes(o.format)) throw new Error(`--format must be mp4, webm, mov, mkv, wav, ogg or pdf (got "${o.format}")`);
54
55
  } else if (a === '--quality') {
55
56
  o.qualityGiven = true;
56
57
  o.quality = need(i++, a);
57
58
  if (!QUALITIES.includes(o.quality)) throw new Error(`--quality must be very-low, low, medium, high or very-high (got "${o.quality}")`);
59
+ } else if (a === '--video-bitrate') {
60
+ o.videoBitrate = need(i++, a);
61
+ if (!/^(very-low|low|medium|high|very-high|\d+(\.\d+)?\s*[kKmM]?)$/.test(o.videoBitrate)) throw new Error(`--video-bitrate must be a quality (very-low, low, medium, high, very-high) or bits a second (8000000, 8M, 800k), got "${o.videoBitrate}"`);
58
62
  } else if (a === '--video-codec') o.videoCodec = need(i++, a);
59
63
  else if (a === '--audio-codec') o.audioCodec = need(i++, a);
60
64
  else if (a === '--motion-blur') {
@@ -88,6 +92,7 @@ export function parseRenderArgs(argv) {
88
92
  if (o.allStops && o.format !== 'pdf' && !(o.out && formatFromPath(o.out) === 'pdf')) throw new Error('--all-stops goes with a PDF (-o deck.pdf)');
89
93
  o.format ??= (o.out ? formatFromPath(o.out) : null) ?? 'mp4';
90
94
  if (o.range && o.scene !== null) throw new Error('--range and --scene cannot be used together: a scene is a range');
95
+ if (SOUND_ONLY.includes(o.format) && (o.scale !== null || o.draft || o.motionBlur !== null || o.videoCodec || o.videoBitrate)) throw new Error(`--scale, --draft, --motion-blur, --video-codec and --video-bitrate are for a picture: ${o.format} writes only the sound`);
91
96
  if (o.format === 'pdf' && (o.range || o.scene !== null || o.scale !== null || o.draft)) throw new Error('--range, --scene, --scale and --draft are not for a PDF (a PDF is the pages of a deck)');
92
97
  return o;
93
98
  }
@@ -96,16 +101,16 @@ const UPLOAD = '/__pixi_effects_out';
96
101
  const kb = n => (n >= 1048576 ? `${(n / 1048576).toFixed(2)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`);
97
102
 
98
103
  /** The page's side: render, and PUT the Blob to our server (so a long video never goes through base64). */
99
- const renderScript = (format, quality, videoCodec, audioCodec, motionBlur, shutter, allStops, part = {}) => `(() => {
104
+ const renderScript = (format, quality, videoCodec, audioCodec, motionBlur, shutter, allStops, part = {}, videoBitrate = null) => `(() => {
100
105
  window.__r = { state: 'running', progress: 0 };
101
106
  movie.on('progress', e => { window.__r.progress = e.progress; });
102
107
  (async () => {
103
108
  try {
104
- const video = { ${[part.draft && !part.qualityGiven ? null : `bitrate: ${JSON.stringify(quality)}`, videoCodec ? `codec: ${JSON.stringify(videoCodec)}` : null].filter(Boolean).join(', ')} };
109
+ const video = { ${[videoBitrate ? `bitrate: ${JSON.stringify(/^\d+(\.\d+)?$/.test(videoBitrate) ? Number(videoBitrate) : videoBitrate)}` : part.draft && !part.qualityGiven ? null : `bitrate: ${JSON.stringify(quality)}`, videoCodec ? `codec: ${JSON.stringify(videoCodec)}` : null].filter(Boolean).join(', ')} };
105
110
  const audio = { bitrate: ${JSON.stringify(quality)}${audioCodec ? `, codec: ${JSON.stringify(audioCodec)}` : ''} };
106
111
  const blob = ${JSON.stringify(format)} === 'pdf'
107
112
  ? await movie.exportPDF({ which: ${allStops ? "'stops'" : "'pages'"}, title: document.title })
108
- : await movie.render({ format: ${JSON.stringify(format)}, video, audio${part.range ? `, range: [${part.range[0] ?? 0}, ${part.range[1] ?? 'movie.duration'}]` : ''}${part.scene ? `, range: ${JSON.stringify(part.scene)}` : ''}${part.scale ? `, scale: ${part.scale}` : ''}${part.draft ? ', draft: true' : ''}${motionBlur ? `, motionBlur: { samples: ${motionBlur}${shutter ? `, shutter: ${shutter}` : ''} }` : ''} });
113
+ : await movie.render({ format: ${JSON.stringify(format)}, ${SOUND_ONLY.includes(format) ? '' : 'video, '}audio${part.range ? `, range: [${part.range[0] ?? 0}, ${part.range[1] ?? 'movie.duration'}]` : ''}${part.scene ? `, range: ${JSON.stringify(part.scene)}` : ''}${part.scale ? `, scale: ${part.scale}` : ''}${part.draft ? ', draft: true' : ''}${motionBlur ? `, motionBlur: { samples: ${motionBlur}${shutter ? `, shutter: ${shutter}` : ''} }` : ''} });
109
114
  const res = await fetch(${JSON.stringify(UPLOAD)}, { method: 'PUT', body: blob });
110
115
  if (!res.ok) throw new Error('could not hand the file to the command (' + res.status + ')');
111
116
  window.__r = { state: 'done', bytes: blob.size, type: blob.type };
@@ -171,7 +176,7 @@ export async function runRender(opts, log = console.log, progress = () => {}) {
171
176
  }
172
177
  Object.assign(result, { duration: info.duration, frames: info.totalFrames, width: info.width, height: info.height, frameRate: info.frameRate });
173
178
 
174
- await cdp.eval(renderScript(opts.format, opts.quality, opts.videoCodec, opts.audioCodec, opts.motionBlur, opts.shutter, opts.allStops, { range: opts.range, scene: opts.scene, scale: opts.scale, draft: opts.draft, qualityGiven: opts.qualityGiven }));
179
+ await cdp.eval(renderScript(opts.format, opts.quality, opts.videoCodec, opts.audioCodec, opts.motionBlur, opts.shutter, opts.allStops, { range: opts.range, scene: opts.scene, scale: opts.scale, draft: opts.draft, qualityGiven: opts.qualityGiven }, opts.videoBitrate));
175
180
  let state;
176
181
  for (;;) {
177
182
  left();
@@ -221,7 +226,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process
221
226
  console.error(`pixi-effects render failed: ${r.problems.join('; ') || 'no file written'}`);
222
227
  process.exit(1);
223
228
  }
224
- console.log(`${shown(r.file)} ${kb(r.bytes)} · ${r.duration} s · ${r.width}×${r.height} @ ${r.frameRate} fps · ${opts.format} · rendered in ${r.seconds} s`);
229
+ console.log(`${shown(r.file)} ${kb(r.bytes)} · ${r.duration} s · ${SOUND_ONLY.includes(opts.format) ? 'sound only' : `${r.width}×${r.height} @ ${r.frameRate} fps`} · ${opts.format} · rendered in ${r.seconds} s`);
225
230
  process.exit(opts.failOnWarn && r.logs.length ? 1 : 0);
226
231
  } catch (e) {
227
232
  console.error(`pixi-effects render failed: ${e.message}`);
@@ -1,9 +1,8 @@
1
1
  import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
2
- import { S as SequenceSpec, C as CompositionShape, a6 as AudioDescriptor } from './types-DSGwN5w1.js';
3
- import { gsap } from 'gsap';
2
+ import { S as SequenceSpec, C as CompositionShape, a6 as Timeline, a7 as AudioDescriptor } from './types-DUVefWiB.js';
4
3
 
5
4
  /**
6
- * Resolves a routed keyframe path to a concrete GSAP tween target.
5
+ * Resolves a routed keyframe path to a concrete tween target.
7
6
  * `path` is everything after the registered prefix + dot (for
8
7
  * `three.cube.rotation.y` under prefix `three`, path = `cube.rotation.y`).
9
8
  * Return null to skip the key (the router is expected to have warned).
@@ -45,7 +44,6 @@ interface SpaceHost {
45
44
  }): void;
46
45
  }
47
46
 
48
- type Timeline = ReturnType<typeof gsap.timeline>;
49
47
  declare abstract class Sequence {
50
48
  spec: SequenceSpec;
51
49
  parent: CompositionShape | null;
@@ -1,9 +1,8 @@
1
1
  import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
2
- import { S as SequenceSpec, C as CompositionShape, a6 as AudioDescriptor } from './types-DSGwN5w1.cjs';
3
- import { gsap } from 'gsap';
2
+ import { S as SequenceSpec, C as CompositionShape, a6 as Timeline, a7 as AudioDescriptor } from './types-DUVefWiB.cjs';
4
3
 
5
4
  /**
6
- * Resolves a routed keyframe path to a concrete GSAP tween target.
5
+ * Resolves a routed keyframe path to a concrete tween target.
7
6
  * `path` is everything after the registered prefix + dot (for
8
7
  * `three.cube.rotation.y` under prefix `three`, path = `cube.rotation.y`).
9
8
  * Return null to skip the key (the router is expected to have warned).
@@ -45,7 +44,6 @@ interface SpaceHost {
45
44
  }): void;
46
45
  }
47
46
 
48
- type Timeline = ReturnType<typeof gsap.timeline>;
49
47
  declare abstract class Sequence {
50
48
  spec: SequenceSpec;
51
49
  parent: CompositionShape | null;
@@ -0,0 +1,7 @@
1
+ export { CompositionSequence } from './chunk-CKTS6G6I.js';
2
+ import './chunk-TGJACVLD.js';
3
+ import './chunk-TYWFKCPY.js';
4
+ import './chunk-45QKLD6M.js';
5
+ import './chunk-BXOCEZ4U.js';
6
+ //# sourceMappingURL=Composition-4TYDRRRY.js.map
7
+ //# sourceMappingURL=Composition-4TYDRRRY.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-H4QA24MV.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-4TYDRRRY.js"}
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ var chunkNDR2VQFV_cjs = require('./chunk-NDR2VQFV.cjs');
4
+ require('./chunk-RQMI7EJT.cjs');
5
+ require('./chunk-P4GY3Z4G.cjs');
6
+ require('./chunk-ZMAD3KJU.cjs');
7
+ require('./chunk-TWZNOUM7.cjs');
8
+
9
+
10
+
11
+ Object.defineProperty(exports, "CompositionSequence", {
12
+ enumerable: true,
13
+ get: function () { return chunkNDR2VQFV_cjs.CompositionSequence; }
14
+ });
15
+ //# sourceMappingURL=Composition-QCOWQSDB.cjs.map
16
+ //# sourceMappingURL=Composition-QCOWQSDB.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-HM4YT5L7.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-QCOWQSDB.cjs"}
@@ -1,7 +1,6 @@
1
- import { j as Movie } from './Movie-i05Oh9mJ.cjs';
1
+ import { j as Movie } from './Movie-CQ0Nz62a.cjs';
2
2
  import 'pixi.js';
3
- import 'gsap';
4
- import './types-DSGwN5w1.cjs';
3
+ import './types-DUVefWiB.cjs';
5
4
 
6
5
  /** The bar's default stylesheet. Its colours and sizes are CSS custom properties (`--mc-accent`, `--mc-fg`, `--mc-track`, `--mc-bar-bg`, `--mc-track-height`, `--mc-font`): set them in your page CSS, or with the `theme` option. */
7
6
  declare const CONTROLLER_CSS = "\n.movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }\n.movie-controller {\n position: absolute;\n pointer-events: none;\n font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n opacity: 1;\n transition: opacity 200ms ease;\n}\n.movie-controller[data-state=\"hidden\"] { opacity: 0; }\n.movie-controller > * { pointer-events: auto; }\n\n.mc-progress {\n position: relative;\n width: 100%;\n height: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n outline: none;\n}\n.mc-progress:focus-visible {\n outline: 2px solid var(--mc-accent, #007AFF);\n outline-offset: 2px;\n}\n.mc-progress::before {\n content: \"\"; position: absolute; left: 12px; right: 12px;\n height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));\n transition: height 120ms ease;\n}\n.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }\n.mc-stop-tick { position: absolute; top: 50%; width: 7px; height: 7px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5); }\n.mc-stop-page { width: 11px; height: 11px; box-shadow: 0 0 0 2.5px var(--mc-accent, #007AFF), 0 0 0 4px rgba(0,0,0,0.35); }\n.mc-progress-fill {\n position: absolute; left: 12px; top: 50%;\n height: var(--mc-track-height, 3px);\n width: calc((100% - 24px) * var(--mc-fill, 0));\n background: var(--mc-accent, #007AFF);\n transform: translateY(-50%);\n transition: height 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-fill,\n.mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-progress-thumb {\n position: absolute; top: 50%;\n left: calc(12px + (100% - 24px) * var(--mc-fill, 0));\n width: 12px; height: 12px; border-radius: 50%;\n background: var(--mc-accent, #007AFF);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-thumb,\n.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }\n\n.mc-bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px 8px 12px;\n background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));\n color: var(--mc-fg, #fff);\n line-height: 1;\n}\n.mc-btn {\n background: none; border: 0; padding: 0; margin: 0;\n width: 28px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;\n transition: opacity 120ms ease;\n}\n.mc-btn:hover { opacity: 1; }\n.mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.mc-time {\n font-size: 12px; font-family: Menlo, Monaco, monospace;\n color: var(--mc-fg, #fff); opacity: 0.85;\n min-width: 90px;\n}\n.mc-spacer { flex: 1; }\n\n.mc-volume { display: flex; align-items: center; }\n.mc-vol-slider {\n position: relative;\n width: 0;\n height: 28px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-left: 0;\n outline: none;\n transition: width 200ms ease, margin-left 200ms ease;\n overflow: hidden;\n}\n.mc-volume:hover .mc-vol-slider,\n.mc-volume:focus-within .mc-vol-slider,\n.mc-vol-slider.mc-scrubbing {\n width: 70px;\n margin-left: 6px;\n}\n.mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }\n.mc-vol-slider::before {\n content: \"\"; position: absolute; left: 4px; right: 4px;\n height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;\n}\n.mc-vol-fill {\n position: absolute; left: 4px; top: 50%;\n height: 3px;\n width: calc((100% - 8px) * var(--mc-volume, 1));\n background: var(--mc-fg, #fff);\n transform: translateY(-50%);\n border-radius: 2px;\n pointer-events: none;\n}\n.mc-vol-thumb {\n position: absolute; top: 50%;\n left: calc(4px + (100% - 8px) * var(--mc-volume, 1));\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--mc-fg, #fff);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n[data-mc-wrap]:fullscreen {\n width: 100vw; height: 100vh;\n display: flex; align-items: center; justify-content: center;\n background: #000;\n}\n[data-mc-wrap]:fullscreen > canvas {\n width: 100% !important;\n height: 100% !important;\n max-width: none !important;\n max-height: none !important;\n object-fit: contain;\n border-radius: 0;\n}\n\n.mc-settings-popover {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 12px;\n background: rgba(20, 20, 20, 0.96);\n border-radius: 8px;\n padding: 10px 12px;\n min-width: 180px;\n color: #fff;\n font-size: 12px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.mc-settings-popover[data-open=\"false\"] { display: none; }\n.mc-settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.mc-settings-row > span { color: rgba(255,255,255,0.85); }\n.mc-settings-row > select {\n font: inherit;\n background: rgba(255,255,255,0.1);\n color: #fff;\n border: 1px solid rgba(255,255,255,0.18);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n}\n.mc-export-confirm {\n margin-top: 4px;\n width: 100%;\n background: var(--mc-accent, #007AFF);\n color: #fff;\n border: 0;\n border-radius: 4px;\n padding: 6px 10px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: background 120ms ease;\n}\n.mc-export-confirm:hover { background: #0066d6; }\n.mc-export-confirm:disabled { background: rgba(255,255,255,0.18); cursor: not-allowed; }\n\n.mc-export-overlay {\n position: absolute; inset: 0;\n background: rgba(0,0,0,0.7);\n display: flex; justify-content: center; align-items: center;\n z-index: 2;\n}\n.mc-export-panel {\n background: rgba(20,20,20,0.96); color: #fff;\n border-radius: 8px; padding: 16px 20px;\n text-align: center; min-width: 200px; max-width: 80%;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.mc-export-title { font-size: 13px; margin-bottom: 10px; opacity: 0.9; }\n.mc-export-track {\n width: 100%; height: 6px;\n background: rgba(255,255,255,0.15); border-radius: 3px;\n margin-bottom: 8px; overflow: hidden;\n}\n.mc-export-fill {\n height: 100%; width: 0%;\n background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));\n transition: width 0.3s ease; border-radius: 3px;\n}\n.mc-export-text {\n color: rgba(255,255,255,0.7); font-size: 11px;\n font-family: Menlo, Monaco, monospace;\n}\n";
@@ -1,7 +1,6 @@
1
- import { j as Movie } from './Movie-DVeDTdnc.js';
1
+ import { j as Movie } from './Movie-M9dKm9dI.js';
2
2
  import 'pixi.js';
3
- import 'gsap';
4
- import './types-DSGwN5w1.js';
3
+ import './types-DUVefWiB.js';
5
4
 
6
5
  /** The bar's default stylesheet. Its colours and sizes are CSS custom properties (`--mc-accent`, `--mc-fg`, `--mc-track`, `--mc-bar-bg`, `--mc-track-height`, `--mc-font`): set them in your page CSS, or with the `theme` option. */
7
6
  declare const CONTROLLER_CSS = "\n.movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }\n.movie-controller {\n position: absolute;\n pointer-events: none;\n font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n opacity: 1;\n transition: opacity 200ms ease;\n}\n.movie-controller[data-state=\"hidden\"] { opacity: 0; }\n.movie-controller > * { pointer-events: auto; }\n\n.mc-progress {\n position: relative;\n width: 100%;\n height: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n outline: none;\n}\n.mc-progress:focus-visible {\n outline: 2px solid var(--mc-accent, #007AFF);\n outline-offset: 2px;\n}\n.mc-progress::before {\n content: \"\"; position: absolute; left: 12px; right: 12px;\n height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));\n transition: height 120ms ease;\n}\n.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }\n.mc-stop-tick { position: absolute; top: 50%; width: 7px; height: 7px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5); }\n.mc-stop-page { width: 11px; height: 11px; box-shadow: 0 0 0 2.5px var(--mc-accent, #007AFF), 0 0 0 4px rgba(0,0,0,0.35); }\n.mc-progress-fill {\n position: absolute; left: 12px; top: 50%;\n height: var(--mc-track-height, 3px);\n width: calc((100% - 24px) * var(--mc-fill, 0));\n background: var(--mc-accent, #007AFF);\n transform: translateY(-50%);\n transition: height 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-fill,\n.mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-progress-thumb {\n position: absolute; top: 50%;\n left: calc(12px + (100% - 24px) * var(--mc-fill, 0));\n width: 12px; height: 12px; border-radius: 50%;\n background: var(--mc-accent, #007AFF);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-thumb,\n.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }\n\n.mc-bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px 8px 12px;\n background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));\n color: var(--mc-fg, #fff);\n line-height: 1;\n}\n.mc-btn {\n background: none; border: 0; padding: 0; margin: 0;\n width: 28px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;\n transition: opacity 120ms ease;\n}\n.mc-btn:hover { opacity: 1; }\n.mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.mc-time {\n font-size: 12px; font-family: Menlo, Monaco, monospace;\n color: var(--mc-fg, #fff); opacity: 0.85;\n min-width: 90px;\n}\n.mc-spacer { flex: 1; }\n\n.mc-volume { display: flex; align-items: center; }\n.mc-vol-slider {\n position: relative;\n width: 0;\n height: 28px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-left: 0;\n outline: none;\n transition: width 200ms ease, margin-left 200ms ease;\n overflow: hidden;\n}\n.mc-volume:hover .mc-vol-slider,\n.mc-volume:focus-within .mc-vol-slider,\n.mc-vol-slider.mc-scrubbing {\n width: 70px;\n margin-left: 6px;\n}\n.mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }\n.mc-vol-slider::before {\n content: \"\"; position: absolute; left: 4px; right: 4px;\n height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;\n}\n.mc-vol-fill {\n position: absolute; left: 4px; top: 50%;\n height: 3px;\n width: calc((100% - 8px) * var(--mc-volume, 1));\n background: var(--mc-fg, #fff);\n transform: translateY(-50%);\n border-radius: 2px;\n pointer-events: none;\n}\n.mc-vol-thumb {\n position: absolute; top: 50%;\n left: calc(4px + (100% - 8px) * var(--mc-volume, 1));\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--mc-fg, #fff);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n[data-mc-wrap]:fullscreen {\n width: 100vw; height: 100vh;\n display: flex; align-items: center; justify-content: center;\n background: #000;\n}\n[data-mc-wrap]:fullscreen > canvas {\n width: 100% !important;\n height: 100% !important;\n max-width: none !important;\n max-height: none !important;\n object-fit: contain;\n border-radius: 0;\n}\n\n.mc-settings-popover {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 12px;\n background: rgba(20, 20, 20, 0.96);\n border-radius: 8px;\n padding: 10px 12px;\n min-width: 180px;\n color: #fff;\n font-size: 12px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.mc-settings-popover[data-open=\"false\"] { display: none; }\n.mc-settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.mc-settings-row > span { color: rgba(255,255,255,0.85); }\n.mc-settings-row > select {\n font: inherit;\n background: rgba(255,255,255,0.1);\n color: #fff;\n border: 1px solid rgba(255,255,255,0.18);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n}\n.mc-export-confirm {\n margin-top: 4px;\n width: 100%;\n background: var(--mc-accent, #007AFF);\n color: #fff;\n border: 0;\n border-radius: 4px;\n padding: 6px 10px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: background 120ms ease;\n}\n.mc-export-confirm:hover { background: #0066d6; }\n.mc-export-confirm:disabled { background: rgba(255,255,255,0.18); cursor: not-allowed; }\n\n.mc-export-overlay {\n position: absolute; inset: 0;\n background: rgba(0,0,0,0.7);\n display: flex; justify-content: center; align-items: center;\n z-index: 2;\n}\n.mc-export-panel {\n background: rgba(20,20,20,0.96); color: #fff;\n border-radius: 8px; padding: 16px 20px;\n text-align: center; min-width: 200px; max-width: 80%;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.mc-export-title { font-size: 13px; margin-bottom: 10px; opacity: 0.9; }\n.mc-export-track {\n width: 100%; height: 6px;\n background: rgba(255,255,255,0.15); border-radius: 3px;\n margin-bottom: 8px; overflow: hidden;\n}\n.mc-export-fill {\n height: 100%; width: 0%;\n background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));\n transition: width 0.3s ease; border-radius: 3px;\n}\n.mc-export-text {\n color: rgba(255,255,255,0.7); font-size: 11px;\n font-family: Menlo, Monaco, monospace;\n}\n";
@@ -1,6 +1,5 @@
1
1
  import { Application } from 'pixi.js';
2
- import { gsap } from 'gsap';
3
- import { _ as Stop, e as AssetSpec, c as CompositionSpec } from './types-DSGwN5w1.cjs';
2
+ import { a6 as Timeline, _ as Stop, e as AssetSpec, c as CompositionSpec } from './types-DUVefWiB.cjs';
4
3
 
5
4
  interface AudioInspectOptions {
6
5
  /** Width of each loudness window in seconds. Default 0.1. */
@@ -289,14 +288,21 @@ interface MovieOptions {
289
288
  interface RenderOptions {
290
289
  /** Motion blur for this export (see `MovieOptions.motionBlur`); overrides the movie's own setting, `false` turns it off. */
291
290
  motionBlur?: MotionBlurSpec;
292
- format?: 'mp4' | 'mov' | 'webm' | 'mkv';
291
+ /** `mp4`, `mov`, `webm`, `mkv`: the picture with its sound. `wav`, `ogg`: only the sound of the movie (an error when it has none). */
292
+ format?: 'mp4' | 'mov' | 'webm' | 'mkv' | 'wav' | 'ogg';
293
293
  video?: {
294
+ /** `'avc'` (H.264), `'hevc'`, `'vp9'`, `'av1'`, `'vp8'`: default the usual one for the format, the next that works if this browser cannot encode it; a name you give is never swapped. */
294
295
  codec?: string;
295
- bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high';
296
+ /** A quality name, or bits a second: `8_000_000` or `'8M'`, `'800k'`. */
297
+ bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high' | number | string;
298
+ /** Where to encode: `'prefer-hardware'`, `'prefer-software'` or `'no-preference'` (default). */
299
+ hardware?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
300
+ /** Seconds between keyframes (default 2): shorter seeks faster and makes a bigger file. */
301
+ keyFrameInterval?: number;
296
302
  };
297
303
  audio?: {
298
304
  codec?: string;
299
- bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high';
305
+ bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high' | number | string;
300
306
  };
301
307
  /**
302
308
  * Only part of the movie: `[from, to]` in seconds, or the name of a top-level layer (its start to its end). The file starts at 0
@@ -403,7 +409,7 @@ declare class Movie {
403
409
  private _events;
404
410
  private _initState;
405
411
  app: Application | null;
406
- timeline: ReturnType<typeof gsap.timeline> | null;
412
+ timeline: Timeline | null;
407
413
  audioBuffer: AudioBuffer | null;
408
414
  audioSource: AudioBufferSourceNode | null;
409
415
  gainNode: GainNode | null;
@@ -1,6 +1,5 @@
1
1
  import { Application } from 'pixi.js';
2
- import { gsap } from 'gsap';
3
- import { _ as Stop, e as AssetSpec, c as CompositionSpec } from './types-DSGwN5w1.js';
2
+ import { a6 as Timeline, _ as Stop, e as AssetSpec, c as CompositionSpec } from './types-DUVefWiB.js';
4
3
 
5
4
  interface AudioInspectOptions {
6
5
  /** Width of each loudness window in seconds. Default 0.1. */
@@ -289,14 +288,21 @@ interface MovieOptions {
289
288
  interface RenderOptions {
290
289
  /** Motion blur for this export (see `MovieOptions.motionBlur`); overrides the movie's own setting, `false` turns it off. */
291
290
  motionBlur?: MotionBlurSpec;
292
- format?: 'mp4' | 'mov' | 'webm' | 'mkv';
291
+ /** `mp4`, `mov`, `webm`, `mkv`: the picture with its sound. `wav`, `ogg`: only the sound of the movie (an error when it has none). */
292
+ format?: 'mp4' | 'mov' | 'webm' | 'mkv' | 'wav' | 'ogg';
293
293
  video?: {
294
+ /** `'avc'` (H.264), `'hevc'`, `'vp9'`, `'av1'`, `'vp8'`: default the usual one for the format, the next that works if this browser cannot encode it; a name you give is never swapped. */
294
295
  codec?: string;
295
- bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high';
296
+ /** A quality name, or bits a second: `8_000_000` or `'8M'`, `'800k'`. */
297
+ bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high' | number | string;
298
+ /** Where to encode: `'prefer-hardware'`, `'prefer-software'` or `'no-preference'` (default). */
299
+ hardware?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
300
+ /** Seconds between keyframes (default 2): shorter seeks faster and makes a bigger file. */
301
+ keyFrameInterval?: number;
296
302
  };
297
303
  audio?: {
298
304
  codec?: string;
299
- bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high';
305
+ bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high' | number | string;
300
306
  };
301
307
  /**
302
308
  * Only part of the movie: `[from, to]` in seconds, or the name of a top-level layer (its start to its end). The file starts at 0
@@ -403,7 +409,7 @@ declare class Movie {
403
409
  private _events;
404
410
  private _initState;
405
411
  app: Application | null;
406
- timeline: ReturnType<typeof gsap.timeline> | null;
412
+ timeline: Timeline | null;
407
413
  audioBuffer: AudioBuffer | null;
408
414
  audioSource: AudioBufferSourceNode | null;
409
415
  gainNode: GainNode | null;
@@ -1,7 +1,6 @@
1
- import { j as Movie } from './Movie-i05Oh9mJ.cjs';
1
+ import { j as Movie } from './Movie-CQ0Nz62a.cjs';
2
2
  import 'pixi.js';
3
- import 'gsap';
4
- import './types-DSGwN5w1.cjs';
3
+ import './types-DUVefWiB.cjs';
5
4
 
6
5
  interface PresenterOptions {
7
6
  canvas: HTMLCanvasElement;
@@ -1,7 +1,6 @@
1
- import { j as Movie } from './Movie-DVeDTdnc.js';
1
+ import { j as Movie } from './Movie-M9dKm9dI.js';
2
2
  import 'pixi.js';
3
- import 'gsap';
4
- import './types-DSGwN5w1.js';
3
+ import './types-DUVefWiB.js';
5
4
 
6
5
  interface PresenterOptions {
7
6
  canvas: HTMLCanvasElement;
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunk3DLYAPK7_cjs = require('./chunk-3DLYAPK7.cjs');
4
- require('./chunk-ECA2WNRX.cjs');
3
+ var chunkP4GY3Z4G_cjs = require('./chunk-P4GY3Z4G.cjs');
4
+ require('./chunk-ZMAD3KJU.cjs');
5
5
  require('./chunk-TWZNOUM7.cjs');
6
6
  var pixi_js = require('pixi.js');
7
7
 
@@ -68,7 +68,7 @@ function make(name, gl, gpu) {
68
68
  var OWN_MODES = [...Object.keys(SEPARABLE), ...Object.keys(NONSEP)];
69
69
  var done = false;
70
70
  function registerBlendModes() {
71
- chunk3DLYAPK7_cjs.setBlendFilterFactory(createBlendFilter);
71
+ chunkP4GY3Z4G_cjs.setBlendFilterFactory(createBlendFilter);
72
72
  if (done) return;
73
73
  done = true;
74
74
  for (const [name, s] of Object.entries(SEPARABLE)) {
@@ -90,5 +90,5 @@ fn blendRGB(cb: vec3<f32>, cs: vec3<f32>) -> vec3<f32> { ${s.gpu} }`);
90
90
  exports.OWN_MODES = OWN_MODES;
91
91
  exports.createBlendFilter = createBlendFilter;
92
92
  exports.registerBlendModes = registerBlendModes;
93
- //# sourceMappingURL=blendModes-YV43K6E7.cjs.map
94
- //# sourceMappingURL=blendModes-YV43K6E7.cjs.map
93
+ //# sourceMappingURL=blendModes-HDOP6R7T.cjs.map
94
+ //# sourceMappingURL=blendModes-HDOP6R7T.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/filters/blendModes.ts"],"names":["BlendModeFilter","ExtensionType","extensions","setBlendFilterFactory"],"mappings":";;;;;;;AAaA,IAAM,SAAA,GAAiC;AAAA,EACrC,OAAA,EAAe,EAAE,EAAA,EAAI,oEAAA,EAAsE,KAAK,0EAAA,EAA2E;AAAA,EAC3K,YAAA,EAAe,EAAE,EAAA,EAAI,oEAAA,EAAsE,KAAK,0EAAA,EAA2E;AAAA,EAC3K,YAAA,EAAe;AAAA,IACb,EAAA,EAAI,oKAAA;AAAA,IACJ,GAAA,EAAK;AAAA,GACP;AAAA,EACA,aAAA,EAAe,EAAE,EAAA,EAAI,4EAAA,EAA8E,KAAK,sFAAA,EAAuF;AAAA,EAC/L,YAAA,EAAe,EAAE,EAAA,EAAI,kFAAA,EAAoF,KAAK,4FAAA,EAA6F;AAAA,EAC3M,MAAA,EAAe,EAAE,EAAA,EAAI,mBAAA,EAAqB,KAAK,mBAAA,EAAoB;AAAA,EACnE,OAAA,EAAe,EAAE,EAAA,EAAI,mBAAA,EAAqB,KAAK,mBAAA,EAAoB;AAAA,EACnE,UAAA,EAAe,EAAE,EAAA,EAAI,oBAAA,EAAsB,KAAK,oBAAA,EAAqB;AAAA,EACrE,SAAA,EAAe,EAAE,EAAA,EAAI,6BAAA,EAA+B,KAAK,6BAAA,EAA8B;AAAA,EACvF,aAAA,EAAe,EAAE,EAAA,EAAI,+BAAA,EAAiC,KAAK,+BAAA;AAC7D,CAAA;AAEA,IAAM,SAAA,GAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+JAAA,CAAA;AAOlB,IAAM,UAAA,GAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wMAAA,CAAA;AAOnB,IAAM,MAAA,GAAsD;AAAA,EAC1D,GAAA,EAAY,EAAE,EAAA,EAAI,8CAAA,EAAgD,KAAK,8CAAA,EAA+C;AAAA,EACtH,UAAA,EAAY,EAAE,EAAA,EAAI,8CAAA,EAAgD,KAAK,8CAAA,EAA+C;AAAA,EACtH,KAAA,EAAY,EAAE,EAAA,EAAI,6BAAA,EAA+B,KAAK,6BAAA,EAA8B;AAAA,EACpF,UAAA,EAAY,EAAE,EAAA,EAAI,6BAAA,EAA+B,KAAK,6BAAA;AACxD,CAAA;AAEA,IAAM,OAAA,GAAU;AAAA;AAAA;AAAA;AAAA,kDAAA,CAAA;AAKhB,IAAM,QAAA,GAAW;AAAA;AAAA;AAAA;AAAA,8DAAA,CAAA;AAMjB,IAAM,OAAA,uBAAc,GAAA,EAAuC;AAGpD,SAAS,kBAAkB,IAAA,EAAsC;AACtE,EAAA,MAAM,CAAA,GAAI,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAA;AAC1B,EAAA,OAAO,CAAA,GAAK,IAAI,CAAA,EAAE,GAAmC,IAAA;AACvD;AAEA,SAAS,IAAA,CAAK,IAAA,EAAc,EAAA,EAAY,GAAA,EAAmB;AACzD,EAAA,MAAM,CAAA,GAAI,cAAcA,uBAAA,CAAgB;AAAA,IACtC,OAAO,SAAA,GAAY,EAAE,IAAA,EAAM,IAAA,EAAMC,sBAAc,SAAA,EAAU;AAAA,IACzD,WAAA,GAAc;AAAE,MAAA,KAAA,CAAM,EAAE,EAAA,EAAI,EAAE,SAAA,EAAW,IAAI,IAAA,EAAM,OAAA,EAAQ,EAAG,GAAA,EAAK,EAAE,SAAA,EAAW,GAAA,EAAK,IAAA,EAAM,QAAA,IAAY,CAAA;AAAA,IAAG;AAAA,GAC5G;AACA,EAAA,OAAA,CAAQ,GAAA,CAAI,MAAM,CAAC,CAAA;AACnB,EAAAC,kBAAA,CAAW,IAAI,CAAU,CAAA;AAC3B;AAEO,IAAM,SAAA,GAA+B,CAAC,GAAG,MAAA,CAAO,IAAA,CAAK,SAAS,CAAA,EAAG,GAAG,MAAA,CAAO,IAAA,CAAK,MAAM,CAAC;AAE9F,IAAI,IAAA,GAAO,KAAA;AAEJ,SAAS,kBAAA,GAA2B;AACzC,EAAAC,uCAAA,CAAsB,iBAAiB,CAAA;AACvC,EAAA,IAAI,IAAA,EAAM;AACV,EAAA,IAAA,GAAO,IAAA;AACP,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,SAAS,CAAA,EAAG;AACjD,IAAA,IAAA;AAAA,MAAK,IAAA;AAAA,MACH,CAAA,4BAAA,EAA+B,EAAE,EAAE,CAAA;AAAA,6FAAA,CAAA;AAAA,MACnC,CAAA,8BAAA,EAAiC,EAAE,GAAG,CAAA;AAAA,yHAAA;AAAA,KAA+H;AAAA,EACzK;AACA,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG;AAC9C,IAAA,IAAA,CAAK,IAAA,EAAM,GAAG,SAAS;AAAA,kCAAA,EAAuC,CAAA,CAAE,EAAE,CAAA,EAAA,CAAA,EAAM,CAAA,EAAG,UAAU;AAAA,yDAAA,EAA8D,CAAA,CAAE,GAAG,CAAA,EAAA,CAAI,CAAA;AAAA,EAC9J;AACF","file":"blendModes-YV43K6E7.cjs","sourcesContent":["import { BlendModeFilter, ExtensionType, extensions } from 'pixi.js';\nimport { setBlendFilterFactory, type BlendFilterLike } from '../core/blend';\n\n/**\n * The W3C compositing blend modes as our own `BlendModeFilter`s, registered on the SAME pixi instance the movie uses (this file imports\n * 'pixi.js' like the rest of the library; `import 'pixi.js/advanced-blend-modes'` would register on its own copy of `extensions` when\n * the page maps pixi.js to an esm.sh bundle, and then nothing is registered and the layer silently draws as `normal`). It also avoids\n * what pixi 8.22's own versions get wrong: the source colour is premultiplied (a half-transparent layer was blended with its darkened\n * colour and then faded again), the backdrop's alpha is ignored, soft-light is inverted on WebGPU (`select` arguments swapped),\n * linear-light and saturation are wrong. Loaded by `Movie.init` only when a layer uses one of these (see `usesAdvancedBlend`).\n */\ntype Sep = { gl: string; gpu: string }; // a separable blend: the body of B(b, s) for one channel\n\nconst SEPARABLE: Record<string, Sep> = {\n overlay: { gl: 'return b <= 0.5 ? 2.0 * b * s : 1.0 - 2.0 * (1.0 - b) * (1.0 - s);', gpu: 'return select(1.0 - 2.0 * (1.0 - b) * (1.0 - s), 2.0 * b * s, b <= 0.5);' },\n 'hard-light': { gl: 'return s <= 0.5 ? 2.0 * b * s : 1.0 - 2.0 * (1.0 - b) * (1.0 - s);', gpu: 'return select(1.0 - 2.0 * (1.0 - b) * (1.0 - s), 2.0 * b * s, s <= 0.5);' },\n 'soft-light': {\n gl: 'if (s <= 0.5) return b - (1.0 - 2.0 * s) * b * (1.0 - b); float d = b <= 0.25 ? ((16.0 * b - 12.0) * b + 4.0) * b : sqrt(b); return b + (2.0 * s - 1.0) * (d - b);',\n gpu: 'let d = select(sqrt(b), ((16.0 * b - 12.0) * b + 4.0) * b, b <= 0.25); return select(b + (2.0 * s - 1.0) * (d - b), b - (1.0 - 2.0 * s) * b * (1.0 - b), s <= 0.5);',\n },\n 'color-dodge': { gl: 'if (b <= 0.0) return 0.0; return s >= 1.0 ? 1.0 : min(1.0, b / (1.0 - s));', gpu: 'if (b <= 0.0) { return 0.0; } return select(min(1.0, b / (1.0 - s)), 1.0, s >= 1.0);' },\n 'color-burn': { gl: 'if (b >= 1.0) return 1.0; return s <= 0.0 ? 0.0 : 1.0 - min(1.0, (1.0 - b) / s);', gpu: 'if (b >= 1.0) { return 1.0; } return select(1.0 - min(1.0, (1.0 - b) / s), 0.0, s <= 0.0);' },\n darken: { gl: 'return min(b, s);', gpu: 'return min(b, s);' },\n lighten: { gl: 'return max(b, s);', gpu: 'return max(b, s);' },\n difference: { gl: 'return abs(b - s);', gpu: 'return abs(b - s);' },\n exclusion: { gl: 'return b + s - 2.0 * b * s;', gpu: 'return b + s - 2.0 * b * s;' },\n 'linear-burn': { gl: 'return max(0.0, b + s - 1.0);', gpu: 'return max(0.0, b + s - 1.0);' },\n};\n\nconst GL_NONSEP = `\nfloat lum(vec3 c) { return 0.3 * c.r + 0.59 * c.g + 0.11 * c.b; }\nvec3 clipColor(vec3 c) { float l = lum(c); float n = min(c.r, min(c.g, c.b)); float x = max(c.r, max(c.g, c.b));\n if (n < 0.0) c = l + (c - l) * l / (l - n); if (x > 1.0) c = l + (c - l) * (1.0 - l) / (x - l); return c; }\nvec3 setLum(vec3 c, float l) { return clipColor(c + (l - lum(c))); }\nfloat sat(vec3 c) { return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b)); }\nvec3 setSat(vec3 c, float s) { float mx = max(c.r, max(c.g, c.b)); float mn = min(c.r, min(c.g, c.b)); return mx > mn ? (c - mn) * s / (mx - mn) : vec3(0.0); }`;\nconst GPU_NONSEP = `\nfn lum(c: vec3<f32>) -> f32 { return 0.3 * c.r + 0.59 * c.g + 0.11 * c.b; }\nfn clipColor(c0: vec3<f32>) -> vec3<f32> { var c = c0; let l = lum(c); let n = min(c.r, min(c.g, c.b)); let x = max(c.r, max(c.g, c.b));\n if (n < 0.0) { c = l + (c - l) * l / (l - n); } if (x > 1.0) { c = l + (c - l) * (1.0 - l) / (x - l); } return c; }\nfn setLum(c: vec3<f32>, l: f32) -> vec3<f32> { return clipColor(c + (l - lum(c))); }\nfn sat(c: vec3<f32>) -> f32 { return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b)); }\nfn setSat(c: vec3<f32>, s: f32) -> vec3<f32> { let mx = max(c.r, max(c.g, c.b)); let mn = min(c.r, min(c.g, c.b)); if (mx > mn) { return (c - vec3<f32>(mn)) * s / (mx - mn); } return vec3<f32>(0.0); }`;\nconst NONSEP: Record<string, { gl: string; gpu: string }> = {\n hue: { gl: 'return setLum(setSat(cs, sat(cb)), lum(cb));', gpu: 'return setLum(setSat(cs, sat(cb)), lum(cb));' },\n saturation: { gl: 'return setLum(setSat(cb, sat(cs)), lum(cb));', gpu: 'return setLum(setSat(cb, sat(cs)), lum(cb));' },\n color: { gl: 'return setLum(cs, lum(cb));', gpu: 'return setLum(cs, lum(cb));' },\n luminosity: { gl: 'return setLum(cb, lum(cs));', gpu: 'return setLum(cb, lum(cs));' },\n};\n\nconst GL_MAIN = `\n vec3 cs = front.a > 0.0 ? front.rgb / front.a : vec3(0.0);\n vec3 cb = back.a > 0.0 ? back.rgb / back.a : vec3(0.0);\n vec3 mixed = front.rgb * (1.0 - back.a) + front.a * back.a * blendRGB(cb, cs) + (1.0 - front.a) * back.rgb;\n finalColor = vec4(mixed, blendedAlpha) * uBlend;`;\nconst GPU_MAIN = `\n let cs = select(vec3<f32>(0.0), front.rgb / front.a, front.a > 0.0);\n let cb = select(vec3<f32>(0.0), back.rgb / back.a, back.a > 0.0);\n let mixed = front.rgb * (1.0 - back.a) + front.a * back.a * blendRGB(cb, cs) + (1.0 - front.a) * back.rgb;\n out = vec4<f32>(mixed, blendedAlpha) * blendUniforms.uBlend;`;\n\nconst CLASSES = new Map<string, new () => BlendModeFilter>();\n\n/** A fresh blend filter for a layer that has filters of its own (it goes last in the chain and reads the backdrop). */\nexport function createBlendFilter(name: string): BlendFilterLike | null {\n const C = CLASSES.get(name);\n return C ? (new C() as unknown as BlendFilterLike) : null;\n}\n\nfunction make(name: string, gl: string, gpu: string): void {\n const C = class extends BlendModeFilter {\n static extension = { name, type: ExtensionType.BlendMode };\n constructor() { super({ gl: { functions: gl, main: GL_MAIN }, gpu: { functions: gpu, main: GPU_MAIN } }); }\n };\n CLASSES.set(name, C);\n extensions.add(C as never);\n}\n\nexport const OWN_MODES: readonly string[] = [...Object.keys(SEPARABLE), ...Object.keys(NONSEP)];\n\nlet done = false;\n/** Register the blends (once per page), and give `core/blend.ts` the maker for the filter chain case. */\nexport function registerBlendModes(): void {\n setBlendFilterFactory(createBlendFilter); // every call: the registration below is once per page, the maker is cheap to hand over again\n if (done) return;\n done = true;\n for (const [name, s] of Object.entries(SEPARABLE)) {\n make(name,\n `float B(float b, float s) { ${s.gl} }\\nvec3 blendRGB(vec3 cb, vec3 cs) { return vec3(B(cb.r, cs.r), B(cb.g, cs.g), B(cb.b, cs.b)); }`,\n `fn B(b: f32, s: f32) -> f32 { ${s.gpu} }\\nfn blendRGB(cb: vec3<f32>, cs: vec3<f32>) -> vec3<f32> { return vec3<f32>(B(cb.r, cs.r), B(cb.g, cs.g), B(cb.b, cs.b)); }`);\n }\n for (const [name, s] of Object.entries(NONSEP)) {\n make(name, `${GL_NONSEP}\\nvec3 blendRGB(vec3 cb, vec3 cs) { ${s.gl} }`, `${GPU_NONSEP}\\nfn blendRGB(cb: vec3<f32>, cs: vec3<f32>) -> vec3<f32> { ${s.gpu} }`);\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/filters/blendModes.ts"],"names":["BlendModeFilter","ExtensionType","extensions","setBlendFilterFactory"],"mappings":"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","file":"blendModes-HDOP6R7T.cjs","sourcesContent":["import { BlendModeFilter, ExtensionType, extensions } from 'pixi.js';\nimport { setBlendFilterFactory, type BlendFilterLike } from '../core/blend';\n\n/**\n * The W3C compositing blend modes as our own `BlendModeFilter`s, registered on the SAME pixi instance the movie uses (this file imports\n * 'pixi.js' like the rest of the library; `import 'pixi.js/advanced-blend-modes'` would register on its own copy of `extensions` when\n * the page maps pixi.js to an esm.sh bundle, and then nothing is registered and the layer silently draws as `normal`). It also avoids\n * what pixi 8.22's own versions get wrong: the source colour is premultiplied (a half-transparent layer was blended with its darkened\n * colour and then faded again), the backdrop's alpha is ignored, soft-light is inverted on WebGPU (`select` arguments swapped),\n * linear-light and saturation are wrong. Loaded by `Movie.init` only when a layer uses one of these (see `usesAdvancedBlend`).\n */\ntype Sep = { gl: string; gpu: string }; // a separable blend: the body of B(b, s) for one channel\n\nconst SEPARABLE: Record<string, Sep> = {\n overlay: { gl: 'return b <= 0.5 ? 2.0 * b * s : 1.0 - 2.0 * (1.0 - b) * (1.0 - s);', gpu: 'return select(1.0 - 2.0 * (1.0 - b) * (1.0 - s), 2.0 * b * s, b <= 0.5);' },\n 'hard-light': { gl: 'return s <= 0.5 ? 2.0 * b * s : 1.0 - 2.0 * (1.0 - b) * (1.0 - s);', gpu: 'return select(1.0 - 2.0 * (1.0 - b) * (1.0 - s), 2.0 * b * s, s <= 0.5);' },\n 'soft-light': {\n gl: 'if (s <= 0.5) return b - (1.0 - 2.0 * s) * b * (1.0 - b); float d = b <= 0.25 ? ((16.0 * b - 12.0) * b + 4.0) * b : sqrt(b); return b + (2.0 * s - 1.0) * (d - b);',\n gpu: 'let d = select(sqrt(b), ((16.0 * b - 12.0) * b + 4.0) * b, b <= 0.25); return select(b + (2.0 * s - 1.0) * (d - b), b - (1.0 - 2.0 * s) * b * (1.0 - b), s <= 0.5);',\n },\n 'color-dodge': { gl: 'if (b <= 0.0) return 0.0; return s >= 1.0 ? 1.0 : min(1.0, b / (1.0 - s));', gpu: 'if (b <= 0.0) { return 0.0; } return select(min(1.0, b / (1.0 - s)), 1.0, s >= 1.0);' },\n 'color-burn': { gl: 'if (b >= 1.0) return 1.0; return s <= 0.0 ? 0.0 : 1.0 - min(1.0, (1.0 - b) / s);', gpu: 'if (b >= 1.0) { return 1.0; } return select(1.0 - min(1.0, (1.0 - b) / s), 0.0, s <= 0.0);' },\n darken: { gl: 'return min(b, s);', gpu: 'return min(b, s);' },\n lighten: { gl: 'return max(b, s);', gpu: 'return max(b, s);' },\n difference: { gl: 'return abs(b - s);', gpu: 'return abs(b - s);' },\n exclusion: { gl: 'return b + s - 2.0 * b * s;', gpu: 'return b + s - 2.0 * b * s;' },\n 'linear-burn': { gl: 'return max(0.0, b + s - 1.0);', gpu: 'return max(0.0, b + s - 1.0);' },\n};\n\nconst GL_NONSEP = `\nfloat lum(vec3 c) { return 0.3 * c.r + 0.59 * c.g + 0.11 * c.b; }\nvec3 clipColor(vec3 c) { float l = lum(c); float n = min(c.r, min(c.g, c.b)); float x = max(c.r, max(c.g, c.b));\n if (n < 0.0) c = l + (c - l) * l / (l - n); if (x > 1.0) c = l + (c - l) * (1.0 - l) / (x - l); return c; }\nvec3 setLum(vec3 c, float l) { return clipColor(c + (l - lum(c))); }\nfloat sat(vec3 c) { return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b)); }\nvec3 setSat(vec3 c, float s) { float mx = max(c.r, max(c.g, c.b)); float mn = min(c.r, min(c.g, c.b)); return mx > mn ? (c - mn) * s / (mx - mn) : vec3(0.0); }`;\nconst GPU_NONSEP = `\nfn lum(c: vec3<f32>) -> f32 { return 0.3 * c.r + 0.59 * c.g + 0.11 * c.b; }\nfn clipColor(c0: vec3<f32>) -> vec3<f32> { var c = c0; let l = lum(c); let n = min(c.r, min(c.g, c.b)); let x = max(c.r, max(c.g, c.b));\n if (n < 0.0) { c = l + (c - l) * l / (l - n); } if (x > 1.0) { c = l + (c - l) * (1.0 - l) / (x - l); } return c; }\nfn setLum(c: vec3<f32>, l: f32) -> vec3<f32> { return clipColor(c + (l - lum(c))); }\nfn sat(c: vec3<f32>) -> f32 { return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b)); }\nfn setSat(c: vec3<f32>, s: f32) -> vec3<f32> { let mx = max(c.r, max(c.g, c.b)); let mn = min(c.r, min(c.g, c.b)); if (mx > mn) { return (c - vec3<f32>(mn)) * s / (mx - mn); } return vec3<f32>(0.0); }`;\nconst NONSEP: Record<string, { gl: string; gpu: string }> = {\n hue: { gl: 'return setLum(setSat(cs, sat(cb)), lum(cb));', gpu: 'return setLum(setSat(cs, sat(cb)), lum(cb));' },\n saturation: { gl: 'return setLum(setSat(cb, sat(cs)), lum(cb));', gpu: 'return setLum(setSat(cb, sat(cs)), lum(cb));' },\n color: { gl: 'return setLum(cs, lum(cb));', gpu: 'return setLum(cs, lum(cb));' },\n luminosity: { gl: 'return setLum(cb, lum(cs));', gpu: 'return setLum(cb, lum(cs));' },\n};\n\nconst GL_MAIN = `\n vec3 cs = front.a > 0.0 ? front.rgb / front.a : vec3(0.0);\n vec3 cb = back.a > 0.0 ? back.rgb / back.a : vec3(0.0);\n vec3 mixed = front.rgb * (1.0 - back.a) + front.a * back.a * blendRGB(cb, cs) + (1.0 - front.a) * back.rgb;\n finalColor = vec4(mixed, blendedAlpha) * uBlend;`;\nconst GPU_MAIN = `\n let cs = select(vec3<f32>(0.0), front.rgb / front.a, front.a > 0.0);\n let cb = select(vec3<f32>(0.0), back.rgb / back.a, back.a > 0.0);\n let mixed = front.rgb * (1.0 - back.a) + front.a * back.a * blendRGB(cb, cs) + (1.0 - front.a) * back.rgb;\n out = vec4<f32>(mixed, blendedAlpha) * blendUniforms.uBlend;`;\n\nconst CLASSES = new Map<string, new () => BlendModeFilter>();\n\n/** A fresh blend filter for a layer that has filters of its own (it goes last in the chain and reads the backdrop). */\nexport function createBlendFilter(name: string): BlendFilterLike | null {\n const C = CLASSES.get(name);\n return C ? (new C() as unknown as BlendFilterLike) : null;\n}\n\nfunction make(name: string, gl: string, gpu: string): void {\n const C = class extends BlendModeFilter {\n static extension = { name, type: ExtensionType.BlendMode };\n constructor() { super({ gl: { functions: gl, main: GL_MAIN }, gpu: { functions: gpu, main: GPU_MAIN } }); }\n };\n CLASSES.set(name, C);\n extensions.add(C as never);\n}\n\nexport const OWN_MODES: readonly string[] = [...Object.keys(SEPARABLE), ...Object.keys(NONSEP)];\n\nlet done = false;\n/** Register the blends (once per page), and give `core/blend.ts` the maker for the filter chain case. */\nexport function registerBlendModes(): void {\n setBlendFilterFactory(createBlendFilter); // every call: the registration below is once per page, the maker is cheap to hand over again\n if (done) return;\n done = true;\n for (const [name, s] of Object.entries(SEPARABLE)) {\n make(name,\n `float B(float b, float s) { ${s.gl} }\\nvec3 blendRGB(vec3 cb, vec3 cs) { return vec3(B(cb.r, cs.r), B(cb.g, cs.g), B(cb.b, cs.b)); }`,\n `fn B(b: f32, s: f32) -> f32 { ${s.gpu} }\\nfn blendRGB(cb: vec3<f32>, cs: vec3<f32>) -> vec3<f32> { return vec3<f32>(B(cb.r, cs.r), B(cb.g, cs.g), B(cb.b, cs.b)); }`);\n }\n for (const [name, s] of Object.entries(NONSEP)) {\n make(name, `${GL_NONSEP}\\nvec3 blendRGB(vec3 cb, vec3 cs) { ${s.gl} }`, `${GPU_NONSEP}\\nfn blendRGB(cb: vec3<f32>, cs: vec3<f32>) -> vec3<f32> { ${s.gpu} }`);\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { setBlendFilterFactory } from './chunk-AXZUKNXF.js';
2
- import './chunk-TLYTYBWX.js';
1
+ import { setBlendFilterFactory } from './chunk-TYWFKCPY.js';
2
+ import './chunk-45QKLD6M.js';
3
3
  import './chunk-BXOCEZ4U.js';
4
4
  import { BlendModeFilter, ExtensionType, extensions } from 'pixi.js';
5
5
 
@@ -86,5 +86,5 @@ fn blendRGB(cb: vec3<f32>, cs: vec3<f32>) -> vec3<f32> { ${s.gpu} }`);
86
86
  }
87
87
 
88
88
  export { OWN_MODES, createBlendFilter, registerBlendModes };
89
- //# sourceMappingURL=blendModes-G3NOAU3T.js.map
90
- //# sourceMappingURL=blendModes-G3NOAU3T.js.map
89
+ //# sourceMappingURL=blendModes-PAVTW3BO.js.map
90
+ //# sourceMappingURL=blendModes-PAVTW3BO.js.map