pixi-effects 0.21.0 → 0.22.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 (83) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +6 -6
  3. package/ai/SKILL.md +1 -1
  4. package/ai/chat-template.html +3 -3
  5. package/ai/reference/cheatsheet.md +2 -2
  6. package/ai/reference/pitfalls.md +6 -2
  7. package/ai/reference/recipes.md +30 -0
  8. package/ai/template.html +5 -5
  9. package/ai/tools/check.mjs +14 -3
  10. package/dist/{Base-wYJQ2g5Q.d.cts → Base-BzlPgd-D.d.cts} +1 -1
  11. package/dist/{Base-CzfLGoDr.d.ts → Base-gu-5X0Cv.d.ts} +1 -1
  12. package/dist/Composition-BK5SH6J7.cjs +16 -0
  13. package/dist/{Composition-2UFPSNVN.cjs.map → Composition-BK5SH6J7.cjs.map} +1 -1
  14. package/dist/Composition-Q5TVG3HM.js +7 -0
  15. package/dist/{Composition-BVI236ZR.js.map → Composition-Q5TVG3HM.js.map} +1 -1
  16. package/dist/Controller.cjs +2 -2
  17. package/dist/Controller.d.cts +2 -2
  18. package/dist/Controller.d.ts +2 -2
  19. package/dist/Controller.js +1 -1
  20. package/dist/{Movie-CgYwa5QG.d.ts → Movie--_TJvdJa.d.ts} +1 -1
  21. package/dist/{Movie-mtX9AkQ-.d.cts → Movie-DMxOoTdW.d.cts} +1 -1
  22. package/dist/Presenter.cjs +2 -2
  23. package/dist/Presenter.d.cts +2 -2
  24. package/dist/Presenter.d.ts +2 -2
  25. package/dist/Presenter.js +1 -1
  26. package/dist/blendModes-5E4G2ZZQ.cjs +94 -0
  27. package/dist/blendModes-5E4G2ZZQ.cjs.map +1 -0
  28. package/dist/blendModes-HCP5XTNP.js +90 -0
  29. package/dist/blendModes-HCP5XTNP.js.map +1 -0
  30. package/dist/chunk-BPTW7L25.cjs +275 -0
  31. package/dist/chunk-BPTW7L25.cjs.map +1 -0
  32. package/dist/{chunk-DDIXIWXK.js → chunk-DC4JPY6A.js} +29 -276
  33. package/dist/chunk-DC4JPY6A.js.map +1 -0
  34. package/dist/chunk-E7RONUZ7.js +268 -0
  35. package/dist/chunk-E7RONUZ7.js.map +1 -0
  36. package/dist/{chunk-BPW42X7C.cjs → chunk-IFZOI3JQ.cjs} +47 -300
  37. package/dist/chunk-IFZOI3JQ.cjs.map +1 -0
  38. package/dist/chunk-K4JVI5HK.cjs +143 -0
  39. package/dist/chunk-K4JVI5HK.cjs.map +1 -0
  40. package/dist/{chunk-6JJC37ZD.cjs → chunk-MHOO2CVU.cjs} +124 -74
  41. package/dist/chunk-MHOO2CVU.cjs.map +1 -0
  42. package/dist/{chunk-MNM56B3X.js → chunk-NHICCDTC.js} +13 -3
  43. package/dist/chunk-NHICCDTC.js.map +1 -0
  44. package/dist/{chunk-NGYGFGDS.js → chunk-UQDEDYVM.js} +82 -32
  45. package/dist/chunk-UQDEDYVM.js.map +1 -0
  46. package/dist/{chunk-WTRKDIKH.cjs → chunk-UZTKEYFP.cjs} +13 -3
  47. package/dist/chunk-UZTKEYFP.cjs.map +1 -0
  48. package/dist/chunk-YEPDQ7I3.js +132 -0
  49. package/dist/chunk-YEPDQ7I3.js.map +1 -0
  50. package/dist/index.cjs +70 -67
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +4 -4
  53. package/dist/index.d.ts +4 -4
  54. package/dist/index.js +9 -6
  55. package/dist/index.js.map +1 -1
  56. package/dist/loader.css +1 -1
  57. package/dist/{music-CFEC7HKL.js → music-FUN7N225.js} +3 -3
  58. package/dist/{music-CFEC7HKL.js.map → music-FUN7N225.js.map} +1 -1
  59. package/dist/{music-UAN7DXK4.cjs → music-GEAH2XCT.cjs} +7 -7
  60. package/dist/{music-UAN7DXK4.cjs.map → music-GEAH2XCT.cjs.map} +1 -1
  61. package/dist/{sfx-EX4PUBMV.cjs → sfx-2DYTYNYW.cjs} +8 -8
  62. package/dist/{sfx-EX4PUBMV.cjs.map → sfx-2DYTYNYW.cjs.map} +1 -1
  63. package/dist/{sfx-U3GM2MED.js → sfx-3IDAJ3UZ.js} +3 -3
  64. package/dist/{sfx-U3GM2MED.js.map → sfx-3IDAJ3UZ.js.map} +1 -1
  65. package/dist/three.cjs +7 -6
  66. package/dist/three.cjs.map +1 -1
  67. package/dist/three.d.cts +2 -2
  68. package/dist/three.d.ts +2 -2
  69. package/dist/three.js +3 -2
  70. package/dist/three.js.map +1 -1
  71. package/dist/{types-YmUaViL1.d.ts → types-DBbZVKGS.d.cts} +7 -4
  72. package/dist/{types-YmUaViL1.d.cts → types-DBbZVKGS.d.ts} +7 -4
  73. package/docs/dsl.md +43 -2
  74. package/llms-full.txt +82 -7
  75. package/package.json +1 -1
  76. package/dist/Composition-2UFPSNVN.cjs +0 -14
  77. package/dist/Composition-BVI236ZR.js +0 -5
  78. package/dist/chunk-6JJC37ZD.cjs.map +0 -1
  79. package/dist/chunk-BPW42X7C.cjs.map +0 -1
  80. package/dist/chunk-DDIXIWXK.js.map +0 -1
  81. package/dist/chunk-MNM56B3X.js.map +0 -1
  82. package/dist/chunk-NGYGFGDS.js.map +0 -1
  83. package/dist/chunk-WTRKDIKH.cjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.0
4
+
5
+ **Added**
6
+
7
+ - **Blend modes: 18, the CSS names.** `blendMode` takes `overlay`, `soft-light`, `hard-light`, `color-dodge`, `color-burn`, `darken`, `lighten`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity` and `linear-burn` besides `normal`, `add`, `screen` and `multiply`. They are our own blend filters, checked against the W3C formulas on WebGPU and WebGL (within 2/255: measured worst 0.5/255 at alpha 1 and 1.5/255 at alpha 0.5), in a separate chunk that a movie loads only when a layer uses one (nothing changes for the others), and they work on a layer with filters, on a `threeD` layer (after the depth-of-field blur) and on a composition (each child blends). A typo (`softlight`, `soft_light`) says what was meant; 10 or more on screen at once warns (about 2 ms each on WebGL in the author's measurement). The recipe `light-leak`, pitfalls 78–80, Playground example 17, and the gallery piece `light-leaks` (47 now: a dusk city told by five blend modes, made by a Claude Sonnet 5.5 subagent from the docs alone; its stumble notes added to the docs that `soft-light` / `overlay` need a mid-tone backdrop, that `color-dodge` over dark gives a saturated colour and that `blendMode` is not animatable).
8
+
9
+ **Changed**
10
+
11
+ - Blend modes: the same blend-mode mistake in many layers (particles make hundreds) is one warning with a count, not hundreds; an advanced mode while the blend filters are not registered warns on a layer with no filters too (Pixi's own message used to be the only one); if the `blendModes-*.js` chunk cannot be loaded the movie still plays (a warning, the advanced blends drawn normal) instead of failing to load; `linear-dodge` / `plus-lighter` (add), `luminance` (luminosity) and `exclude` (exclusion) are understood in the did-you-mean; the many-blends warning follows a negative `at` correctly and counts through nested compositions (a card with three blends in it, five cards on screen together: fifteen); the blend filters, groups and carriers a composition made are destroyed with it.
12
+
13
+ **Fixed**
14
+
15
+ - `blendMode` together with `maskInverted` rendered the layer black (the layer is drawn into a texture for the alpha mask, and its blend applied inside that texture against nothing; wrapping it in a group with a blend filter did not help either, because Pixi 8.22 skips a parent's filter when an alpha-masked layer is inside). A layer with both is now built as a group whose filter blends, with the mask shape drawn over the layer with the `erase` blend; it works with every mode on WebGPU and WebGL. A **text, image or video** layer as a (not inverted) mask still cannot carry a blend: it warns and draws the layer normal. A mask with filters of its own (a feathered hole) works too.
16
+ - Depth of field: a layer that was hidden (its lifespan over, `alpha` 0) kept the depth of the last frame it was drawn, so it was still counted as blurred: the "N layers are blurred" warning could fire for a movie that had only two layers on screen, and `inspect` returned a different `depthBlur` for the same frame depending on how you had got there. And `focus` could not name a layer called `hero card` or `3d-title` (only names shaped like a word): any name a sibling layer has now works.
17
+
18
+ **Changed**
19
+
20
+ - Depth of field warns about more mistakes: a negative or non-numeric `aperture` (it was silently off), two layers with the name `focus` points at (the first `threeD` one is used, as before, and now the check and the build agree on it), and `focus` / `aperture` written on a layer that is not a camera.
21
+ - `check.mjs --help` prints the usage (it was rejected as an unknown option), and `--at` prints each threeD layer's blur at every moment you name (`depth 2.40s — title 0 px, badge 10.12 px, …`; all of it is in `report.json` as `at[].depthBlur`).
22
+ - **Docs:** a dolly with no typed home distance (`offsetZ` on an automatic `z`, or the expression `H / 2 / tan(20 * PI / 180)`), how big a bokeh card must be to fill the frame at a depth (`(W / s) × (H / s)`), pitfall 77.
23
+
3
24
  ## 0.21.0
4
25
 
5
26
  **Added**
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # pixi-effects
2
2
 
3
- > **Status**: experimental — current release `0.21.0`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
3
+ > **Status**: experimental — current release `0.22.0`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
4
4
 
5
- **[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 46 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.
5
+ **[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 47 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
 
7
7
  ## Make a video by asking an AI
8
8
 
@@ -51,8 +51,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
51
51
  "gsap": "https://esm.sh/gsap@3.12.5",
52
52
  "gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
53
53
  "mediabunny": "https://esm.sh/mediabunny",
54
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/index.js",
55
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/Controller.js"
54
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/index.js",
55
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/Controller.js"
56
56
  }
57
57
  }
58
58
  </script>
@@ -63,7 +63,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
63
63
  </script>
64
64
  ```
65
65
 
66
- Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.21.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.
66
+ Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.22.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
67
 
68
68
  > **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
69
69
  >
@@ -79,7 +79,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
79
79
  "imports": {
80
80
  "...": "(everything from the importmap above)",
81
81
  "three": "https://esm.sh/three@0.178.0",
82
- "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/three.js"
82
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/three.js"
83
83
  }
84
84
  }
85
85
  </script>
package/ai/SKILL.md CHANGED
@@ -36,7 +36,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
36
36
  - **Group / parent layers = `{ type: 'null', name: 'rig' }`** (draws nothing, only moves) + `parent: 'rig'` on the layers that should follow it: their `x`/`y` are then measured from the null's origin, and the null's position / rotation / scale / alpha carry them (orbits, clock hands, grouped fades). Nulls chain. Only `null` layers can be parents; not for `threeD` layers.
37
37
  - **Time:** `speed` (slow motion, backward) and `time` (a curve of the layer's own seconds: freeze, rewind, ramp) on video, audio-file and composition layers; put what you want to rewind in a composition. Children of such a composition are in ITS seconds. `ease: 'cubic-bezier(.4, 0, .2, 1)'` is the CSS curve. See the recipe `rewind-and-replay` and pitfalls 69–73.
38
38
  - **Built in:** `animateText()` (per-letter / per-word / per-line entrances and exits in one call: `...animateText('Title', style, { x, y, align: 'center', at: 0.5, duration: 5, in: 'rise', out: 'fade', stagger: { each: 0.05 } })`), `splitText()` / `measureText()` (per-letter / per-word layers, exact positions, by hand), `shape: 'arc'` (progress rings, pie / donut slices: animate `endAngle`), `fillGradient` (linear/radial, alpha stops → vignettes), `{value}` counters, `repeat`/`yoyo`, `orbit()`, seeded randomness (`rand(seed)` / `noise(x, seed)` in expressions, `random(seed)` in JS, never `Math.random()`) `wiggle()` (a seeded shake as keyframes; on a camera put it on `offsetX` / `offsetY` so it rides on the move) `cameraPath()` (a camera flight through `[x, y, z]` points as keyframes; layers it passes: `hideBehindCamera: true`) depth of field (camera `focus` + `aperture`: `{ type: 'camera', initial: { focus: 'title' } }` blurs the other threeD layers by depth; a rack focus is a keyframe on `focus`) `music` (a tune with no file: `{ type: 'audio', music: { bpm, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4' }, { inst: 'bass', notes: 'c2:2 g1:2' }], drums: { from: 8, kick: 'x...x...' } } }`: notes and chords as text, counted in beats; instruments `keys pluck pad bass sub lead bell musicbox`; `musicEnvelope(music)` feeds `react()`), `audioEnvelope()` / `bpmEnvelope()` + `react()` (visuals that follow music: `const env = await audioEnvelope('music.mp3', { frameRate })` before `movie.init`, then `keyframes: react(env, { duration, props: { scale: { base: 1, amount: 0.4, band: 'bass', release: 0.2 } } })`; `beats: true` pulses on the beats; `bpmEnvelope(120, …)` when there is no file), `deck()` + `Presenter` (a talk as one movie: pages, steps, stops that pause playback, arrow keys / click / swipe to move on: `...deck({ transition: { kind: 'slide', duration: 0.6 }, pages: [{ name, duration, stops, sequences }] })`, `new Presenter(movie, { canvas })` from `pixi-effects/presenter`; G = page overview, P = presenter view with notes and a timer; `pixi-effects-render talk.html -o talk.pdf` = a PDF of the pages), `particles()` (bursts, snow, confetti, sparks as seeded layers: `...particles({ count: 90, at: 2, life: [1, 1.8], area: { x: 640, y: 330 }, speed: [120, 420], gravity: 240, fade: { out: 0.6 } })`), `followPath()` (a layer along an SVG path: `keyframes: followPath({ d, duration, orient: true })`), path morphing (`{ shape: 'path', d, morphTo, initial/keyframes: morph 0 → 1 }`) and `stagger()` (delays for a wave of layers: `stagger(letters, { each: 0.05, from: 'center' })`, or `stagger(n, …)` for numbers; GSAP-style `each` / `amount` / `from` / `grid` / `ease`). **Not in the DSL (hand-roll with a JS loop):** other per-frame curves (waves, spirals: sample them into short linear keyframes), shared masks. (Draw-on strokes `trimEnd`, text that changes over time `set: { text }` / `visibleChars`, shake `wiggle()` and particles are built in.)
39
- - **Blend modes:** `blendMode: 'add' | 'screen' | 'multiply'` on any layer (glows, light leaks).
39
+ - **Blend modes:** `blendMode` takes 18 CSS names (`add` `screen` `multiply` `overlay` `soft-light` `hard-light` `color-dodge` `color-burn` `darken` `lighten` `difference` `exclusion` `hue` `saturation` `color` `luminosity` `linear-burn`, hyphenated) on any layer: glows and light leaks (`add`, `color-dodge`), colour casts and contrast (`soft-light`, `overlay`), vignettes (`multiply`). It blends with everything below the layer; on a composition each child blends (as one picture: `threeD: true` or a filter); 10+ of the new modes on screen at once warn.
40
40
  - **One `Movie` per page.** A sound effect needs no `duration` (it lasts as long as its clip). Seeking is safe: counters, colours and growing shapes are right after any jump.
41
41
 
42
42
  ## Layout sanity (the cheap bugs a screenshot catches)
@@ -61,9 +61,9 @@
61
61
  "gsap": "https://cdn.jsdelivr.net/npm/gsap@3.12.5/+esm",
62
62
  "gsap/PixiPlugin": "https://cdn.jsdelivr.net/npm/gsap@3.12.5/PixiPlugin/+esm",
63
63
  "mediabunny": "https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm",
64
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/index.js",
65
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/Controller.js",
66
- "pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/Presenter.js"
64
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/index.js",
65
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/Controller.js",
66
+ "pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/Presenter.js"
67
67
  }
68
68
  }
69
69
  </script>
@@ -40,7 +40,7 @@ filters: [ { type: 'chromaKey', keyColor, threshold, smoothing, spill } | { type
40
40
  mask: <a layer spec>, maskInverted, // a mask with no `at` of its own starts and ends with the layer it masks (its keyframes count from that layer's start); an explicit mask `at` is composition time
41
41
  filterArea: { x, y, width, height } // in the layer's OWN coordinates; lets blur/glow draw past the layer's bounds
42
42
  threeD: true // opt into 2.5D (see below)
43
- blendMode: 'normal' | 'add' | 'screen' | 'multiply' // add / screen brighten (glows, light leaks: overlapping halos add up); on a composition its children inherit the mode
43
+ blendMode: 'normal' | 'add' | 'screen' | 'multiply' | 'overlay' | 'soft-light' | 'hard-light' | 'color-dodge' | 'color-burn' | 'darken' | 'lighten' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity' | 'linear-burn' // CSS names (hyphenated); blends with EVERYTHING BELOW the layer; add / screen brighten (glows), multiply darkens, soft-light / overlay for colour casts and contrast, color-dodge for light sources; on a composition the children each blend (to blend it as one picture: threeD: true or a filter); the 14 new ones cost ~2 ms a layer on WebGL (10+ on screen warns)
44
44
  ```
45
45
 
46
46
  Animatable props (`initial` / keyframes): `x y alpha rotation scale scaleX scaleY pivotX pivotY anchorX anchorY skewX skewY tint width height visible autoAlpha`, plus for shapes their style/geometry, for text `fill`, for audio `volume`, for 3D `z rotationX rotationY`, filter params as `'filters.<name>.<param>'`, three objects as `'three.<obj>.<path>'`. **These go in `initial` / keyframes, never at the top level of a shape or text layer** (top-level `x`, `rotation`, `strokeColor`, `fillAlpha` each warn "is not a … key"): the top level of a shape holds its geometry (`width height radius d startAngle endAngle cornerRadius anchorX anchorY`), `trimStart` / `trimEnd`, `strokeCap` / `strokeJoin`, `fillGradient`.
@@ -79,7 +79,7 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
79
79
 
80
80
  **null + `parent`** — `{ type: 'null', name: 'rig', initial: { x, y }, keyframes: [...] }` draws nothing; layers with `parent: 'rig'` are drawn inside it and follow its `x y rotation scale skew alpha pivotX pivotY`. Their `x / y` are measured **from the null's origin** (null at (640,360), child `x: 200` → drawn at 840), so rotating the null makes the child circle it. Nulls chain (`parent` on a null). Children are drawn at the null's place in the stack, together. Only `null` layers can be parents; not for `threeD` layers; a mask follows its layer.
81
81
 
82
- **camera** (2.5D) — props in `initial`/keyframes: `x y z lookAtX lookAtY lookAtZ fov focus aperture` (defaults: centred, looking at the z = 0 plane, `fov` 40, `z` auto = `(H/2)/tan(fov/2)` ≈ 989 at 720p). **Depth of field:** `focus` = the sharp plane (a `threeD` layer's name = its first z, or a z number), `aperture` = the lens diameter in px (30 default, 0 off); writing either blurs the other `threeD` layers by their distance from the focus (a rack focus is a keyframe on `focus`; recipe `rack-focus`; `inspect` reports `depthBlur`). Never on the camera object itself. An orbit is `orbit()`; by hand: `x = cx + R·sin θ`, `z = R·cos θ`, `R = (H/2)/tan(fov/2)`, `lookAt` = the centre at `z = 0`.
82
+ **camera** (2.5D) — props in `initial`/keyframes: `x y z lookAtX lookAtY lookAtZ fov focus aperture` (defaults: centred, looking at the z = 0 plane, `fov` 40, `z` auto = `(H/2)/tan(fov/2)` ≈ 989 at 720p). **Depth of field:** `focus` = the sharp plane (a `threeD` layer's name = its first z, or a z number), `aperture` = the lens diameter in px (30 default, 0 off); writing either blurs the other `threeD` layers by their distance from the focus (a rack focus is a keyframe on `focus`; recipe `rack-focus`; `inspect` reports `depthBlur`, and `check --at 2.4` prints each layer's blur). A dolly with no typed distance: leave `z` automatic and animate `offsetZ`. Never on the camera object itself. An orbit is `orbit()`; by hand: `x = cx + R·sin θ`, `z = R·cos θ`, `R = (H/2)/tan(fov/2)`, `lookAt` = the centre at `z = 0`.
83
83
 
84
84
  **three** (optional entry `pixi-effects/three`) — `three({ type:'three', width, height, setup(ctx) { …; return { objects: { knot } } }, update?, dispose? })`; drive with `'three.knot.rotation.y'` keyframes. Call `registerThree()` before `init`.
85
85
 
@@ -73,7 +73,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
73
73
  44. **Zero-pad a counter with `format: { pad: 2 }`** (`18:42:{value}` shows `18:42:05`). A `{value}` placeholder that nothing sets or animates prints 0 and now warns: `{value}` is reserved, there is no way to print it literally. *(fixed)*
74
74
  45. **The same mistake in a loop warns once**: the warning "keyframe starts after the layer ends" is summarised after three layers. The usual cause is an off-by-one (`f <= frames` instead of `f < frames`) that puts the last sample exactly at the layer's end. *(fixed)*
75
75
  46. **Still not in the DSL (hand-roll):** shared masks (one mask for several layers), `zIndex` (stack order is list order). *(Earlier round-two notes listed draw-on strokes, changing text, jitter and particle emitters here: they are built in now: `trimEnd`, `set: { text }` / `visibleChars`, `wiggle()`, `particles()`.)*
76
- 46b. **Blend modes exist now** (`blendMode: 'add' | 'screen' | 'multiply'` on any layer, threeD layers and compositions included; a composition's children inherit it). Additive white cores no longer read grey on a coloured background. It also works on a layer with `filters` (a blurred glow with `add` / `multiply`): the mode is applied by the last filter, because a container's blend inside a filter pass multiplied against nothing and rendered black. With `add`, use dim saturated colours: overlaps sum, and near-white sums to pure white. *(fixed)*
76
+ 46b. **Blend modes exist now** (`blendMode`: `add` `screen` `multiply` and, since 0.22, 15 more CSS names (pitfalls 78–80), on any layer, threeD layers and compositions included; a composition's children inherit it). Additive white cores no longer read grey on a coloured background. It also works on a layer with `filters` (a blurred glow with `add` / `multiply`): the mode is applied by the last filter, because a container's blend inside a filter pass multiplied against nothing and rendered black. With `add`, use dim saturated colours: overlaps sum, and near-white sums to pure white. *(fixed)*
77
77
  47. **Browser tooling: prefer `node ai/tools/check.mjs page.html`** — it runs its own private Chrome, so none of the following can bite. If you drive a browser by hand instead: the `agent-browser` daemon is shared, so a `contactSheet` can come back from another session: after saving an image, look at it and confirm it is YOUR piece. In zsh a variable holding `agent-browser --session x` is not split into words (use a small wrapper script). `inspect` now clips overlaps to the visible area, ignores the two scenes of a running transition, treats text inside a moving (panned) composition as moving, and does not report text scaled to 0 as "no size"; it still does not compare text with picture content or shapes.
78
78
  48. **A composition's `alpha` is applied to each child separately, not to the flattened group** (measured: two overlapping opaque circles in a composition at `alpha: 0.5` show the lower one through the upper one in the overlap). Fading a group that has overlapping children therefore shows the overlap; fade the children themselves, or keep overlapping parts out of the group. Expressions have no way to read a sibling's size: measure text with `canvas.measureText` (the font string Pixi uses) and compute pill widths in JS. *(docs)*
79
79
  49. **sfx timing and level.** An sfx layer's `duration` is the sound's length (it stretches, it does not trim). Align the loud point: a `riser` peaks at its end, a `swoosh` 0.16 s in. Many sounds at the same instant add up: above 0.9 the mix is limited and a warning names the layers to turn down. Preset names, knobs and voice fields are closed lists — a guess (`whoosh`, `ding`, `impact`, `sawtooth`, `lp`, `decay`) warns with the right word. *(fixed: warns)*
@@ -101,6 +101,10 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
101
101
  71. **Sound follows `speed` like a tape: the pitch moves with it.** An audio file or a video's sound at `speed: 2` is an octave up; backward plays reversed. Not for `sfx` / `music` layers (warns): change an sfx with `pitch` (semitones), or put the sfx **inside a remapped composition** and it follows that clock (slower and lower, or backward).
102
102
  72. **A video at `speed: 2` with no `duration` is half as long; with a `duration` longer than the file it holds the last frame** (add `loop: true` to repeat). A negative `time` counts back from the end of the file (`initial: { time: -2 }` = two seconds before the end).
103
103
  73. **Know the shape of the curve before you time a beat to it.** `cubic-bezier(.2, .8, .2, 1)` does half of its change in the first 13 % of the time and 99 % by 74 % (not slow motion: over 6 s the first 2 s of content pass in 0.6 s); `.2, .8, .5, 1` has a shorter tail. GSAP's `power2` is a cubic (`power1` = t², `power3` = t⁴): `power2.in` has done 12.5 % at half time and 42 % at three quarters, so a rewind with it dismantles most of the title in the last third.
104
- 74. **`focus` as a layer name means that layer's FIRST `z`.** The camera does not follow a layer whose `z` moves (it warns): to follow it, animate `focus` itself with numbers. A name no layer has, or a layer that is not `threeD`, warns and the `z = 0` plane is used (did-you-mean for typos).
104
+ 74. **`focus` as a layer name means that layer's FIRST `z`, and the name must be unique.** (Two layers with one name: the first `threeD` one is used and it warns.) The camera does not follow a layer whose `z` moves (it warns): to follow it, animate `focus` itself with numbers. A name no layer has, or a layer that is not `threeD`, warns and the `z = 0` plane is used (did-you-mean for typos).
105
105
  75. **Depth of field blurs `threeD` layers only, and the layer's own depth decides it.** A 2D layer never blurs; a `threeD` layer at the focus depth stays sharp; the depth is measured along the camera's view direction, so with an orbiting or tilted camera a layer off to the side of the look-at point is a little deeper than its `z` says and blurs a little even at the focus `z`; one blur for the whole layer, so a tilted floor is one blur (split it into layers).
106
106
  76. **`aperture` is the lens diameter in px, not a 0–1 strength.** 30 is the default (clearly shallow), 0 turns it off, 60 is strong, 100 is extreme (small bright objects speckle); the blur never exceeds 32 px. 20 or more blurred layers at once warns (one filter pass each, more on WebGPU).
107
+ 77. **A dolly does not need the camera's home distance typed in:** keep `z` automatic and animate `offsetZ` (negative = closer). Writing `z` yourself means typing `H / 2 / tan(fov / 2)` (about 989 at 720p, fov 40; as an expression `H / 2 / tan(20 * PI / 180)`). A bokeh field is a few big cards, not dozens of layers: a card at depth `z` fills the frame at size `(W / s) × (H / s)`, `s = camZ / (camZ − z)`.
108
+ 78. **A blend mode mixes the layer with everything BELOW it, in the order of the array.** The backdrop goes first, the blended layer after it; a layer placed before its backdrop blends with nothing. Names are CSS names with the hyphen: `soft-light`, not `softLight` (the warning says which you meant).
109
+ 79. **On a plain `composition` a `blendMode` makes each child blend by itself, not the composition as one picture.** To blend the whole group give the composition `threeD: true` (z 0, default camera: nothing else changes) or a filter. A text, image or video layer as the (not inverted) `mask` cannot carry a blend (it warns, draws normal); a shape mask and `maskInverted` work.
110
+ 80. **`soft-light` / `overlay` do almost nothing over near-black or near-white; `color-dodge` over dark gives a saturated colour, not white; `blendMode` cannot be keyframed** (use a second short-lived layer). **The 14 new modes cost about 2 ms a layer on WebGL; 10 or more on screen at once warns.** A glow, a haze or a colour cast is ONE layer with a gradient (`fillGradient`), not dozens of circles; keep `add` / `screen` / `multiply` for big piles of particles.
@@ -775,6 +775,36 @@ return [
775
775
 
776
776
  ---
777
777
 
778
+ ## Light leaks and colour casts (blend modes)
779
+
780
+ `blendMode` mixes a layer with everything **below** it. A colour cast is one `soft-light` gradient over the whole picture, a light leak is a `color-dodge` glow that drifts across it, a vignette is a `multiply` radial gradient that goes from clear to dark. Each is ONE layer (a gradient), not a pile of shapes: the blend modes beyond `add` / `screen` / `multiply` cost a full-frame pass each.
781
+
782
+ ```js
783
+ // @recipe light-leak
784
+ const skyline = Array.from({ length: 9 }, (_, i) => ({
785
+ type: 'shape', shape: 'rect', name: 'block-' + i, width: 90 + (i % 3) * 30, height: 160 + ((i * 53) % 190), anchorX: 0, anchorY: 1,
786
+ initial: { x: 40 + i * 140, y: 650, fillColor: '#0d1220' },
787
+ }));
788
+ return { duration: 6, sequences: [
789
+ { type: 'shape', shape: 'rect', width: 'GW', height: 'GH', anchorX: 0, anchorY: 0, initial: { x: 0, y: 0 },
790
+ fillGradient: { stops: [[0, '#2a3b66'], [0.55, '#8a5a78'], [1, '#e8a060']] } },
791
+ ...skyline,
792
+ // a colour cast: orange at the top, blue at the bottom, over everything above
793
+ { type: 'shape', shape: 'rect', name: 'cast', width: 'GW', height: 'GH', anchorX: 0, anchorY: 0, blendMode: 'soft-light', initial: { x: 0, y: 0, alpha: 0 },
794
+ fillGradient: { stops: [[0, '#ff9a3c'], [1, '#3c5bff']] },
795
+ keyframes: [{ at: 0.5, to: { alpha: 0.9 }, duration: 2, ease: 'sine.inOut' }] },
796
+ // a light leak: a strong source burning out toward white as it drifts in from the right
797
+ { type: 'shape', shape: 'circle', name: 'leak', radius: 380, blendMode: 'color-dodge', initial: { x: 1500, y: 140 },
798
+ fillGradient: { type: 'radial', stops: [[0, 'rgba(255,120,30,0.9)'], [1, 'rgba(255,120,30,0)']] },
799
+ keyframes: [{ at: 1, to: { x: 900 }, duration: 3.5, ease: 'power2.out' }] },
800
+ // a vignette: clear in the middle, dark at the corners
801
+ { type: 'shape', shape: 'rect', name: 'vignette', width: 'GW', height: 'GH', anchorX: 0, anchorY: 0, blendMode: 'multiply', initial: { x: 0, y: 0 },
802
+ fillGradient: { type: 'radial', radius: 0.8, stops: [[0.5, 'rgba(255,255,255,1)'], [1, 'rgba(60,60,80,1)']] } },
803
+ ] };
804
+ ```
805
+
806
+ ---
807
+
778
808
  ## A hand-held shake and a flickering light (`wiggle`, seeded)
779
809
 
780
810
  `wiggle()` returns keyframes (spread it into `keyframes`): each property drifts between seeded random targets and ends back at `around`. Use `freq` ~3 for a slow float, 8+ for a shake, `ease: 'none'` for a jittery flicker. Per-layer variety: give each its own `seed`.
package/ai/template.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>pixi-effects video</title>
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/loader.css" /> <!-- the loader: HTML + CSS only, so it is on screen at once and keeps moving while the page builds the video -->
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/loader.css" /> <!-- the loader: HTML + CSS only, so it is on screen at once and keeps moving while the page builds the video -->
8
8
  <style>
9
9
  body { background: #0d1220; margin: 0; padding: 16px; }
10
10
  .stage { position: relative; width: min(960px, 100%); } /* the player and the loader both sit inside this box */
@@ -39,10 +39,10 @@
39
39
  "gsap": "https://esm.sh/gsap@3.12.5",
40
40
  "gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
41
41
  "mediabunny": "https://esm.sh/mediabunny",
42
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/index.js",
43
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/Controller.js",
44
- "pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/Presenter.js",
45
- "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.21.0/dist/three.js",
42
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/index.js",
43
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/Controller.js",
44
+ "pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/Presenter.js",
45
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/three.js",
46
46
  "pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
47
47
  "three": "https://esm.sh/three@0.178.0"
48
48
  }
@@ -39,12 +39,15 @@ export function findChrome(env = process.env, exists = fs.existsSync, platform =
39
39
  return candidates.find(c => c && exists(c)) ?? null;
40
40
  }
41
41
 
42
+ export const USAGE = 'usage: node ai/tools/check.mjs <page.html> [--out DIR] [--frames N] [--formats mp4,webm] [--no-export] [--draft] [--at 3.5,title@end] [--onion 1:3] [--query a=1&b=2] [--timeout S] [--root DIR] [--chrome PATH] [--strict] [--help]\n(the header of ai/tools/check.mjs explains each option)';
43
+
42
44
  export function parseArgs(argv) {
43
- const o = { page: null, out: null, frames: 12, formats: ['mp4'], export: true, timeout: 240, root: null, chrome: null, strict: false, at: null, draft: false, onion: null, query: null };
45
+ const o = { page: null, out: null, frames: 12, formats: ['mp4'], export: true, timeout: 240, root: null, chrome: null, strict: false, at: null, draft: false, onion: null, query: null, help: false };
44
46
  const need = (i, name) => { if (i + 1 >= argv.length) throw new Error(`${name} needs a value`); return argv[i + 1]; };
45
47
  for (let i = 0; i < argv.length; i++) {
46
48
  const a = argv[i];
47
- if (a === '--out') o.out = need(i++, a);
49
+ if (a === '--help' || a === '-h') o.help = true;
50
+ else if (a === '--out') o.out = need(i++, a);
48
51
  else if (a === '--frames') o.frames = Math.max(1, Math.round(Number(need(i++, a))) || 12);
49
52
  else if (a === '--formats') o.formats = need(i++, a).split(',').map(s => s.trim()).filter(Boolean);
50
53
  else if (a === '--no-export') o.export = false;
@@ -336,6 +339,9 @@ export async function runCheck(opts, log = console.log) {
336
339
  for (const w of wanted) {
337
340
  const png = await cdp.eval(`movie.snapshot(${w.frame}, { as: 'dataURL' })`);
338
341
  fs.writeFileSync(path.join(framesDir, `${w.label}.png`), Buffer.from(png.slice(png.indexOf(',') + 1), 'base64'));
342
+ // depth of field: the blur (px) each threeD layer has at this moment, so a focus pull can be read as numbers (0 = sharp)
343
+ const rows = await cdp.eval(`movie.inspect(${w.frame}).then(r => r.layers.filter(l => l.depthBlur !== undefined).map(l => [l.path, l.depthBlur]))`).catch(() => []);
344
+ if (rows.some(([, px]) => px > 0)) w.depthBlur = Object.fromEntries(rows);
339
345
  }
340
346
  const atSheet = await cdp.eval(`movie.contactSheet({ frames: ${JSON.stringify(wanted.map(w => w.frame))}, as: 'dataURL' })`);
341
347
  const atFile = path.join(outDir, 'at.png');
@@ -469,6 +475,10 @@ function finish(report, outDir, log) {
469
475
  if (rv.length > 4) L.push(` … ${rv.length - 4} more (report.json)`);
470
476
  }
471
477
  }
478
+ for (const w of (report.at ?? []).filter(x => x.depthBlur)) {
479
+ const shown = Object.entries(w.depthBlur).slice(0, 8).map(([k, px]) => `${k} ${px} px`).join(', ');
480
+ L.push(` depth ${w.label} — ${shown}${Object.keys(w.depthBlur).length > 8 ? ', …' : ''} (blur of each threeD layer at that moment; 0 = sharp)`);
481
+ }
472
482
  if (report.stops) {
473
483
  const s = report.stops;
474
484
  L.push(` stops ${s.count} stop(s) on ${s.pages} page(s) — stops.png shows each one in order; the picture at a stop is what the audience sees while it waits`);
@@ -501,8 +511,9 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
501
511
  try {
502
512
  if (typeof WebSocket === 'undefined') throw new Error('Node >= 22 is needed (built-in WebSocket)');
503
513
  const opts = parseArgs(process.argv.slice(2));
514
+ if (opts.help) { console.log(USAGE); process.exit(0); }
504
515
  if (!opts.page) {
505
- console.error('usage: node ai/tools/check.mjs <page.html> [--out DIR] [--frames N] [--formats mp4,webm] [--no-export] [--draft] [--at 3.5,title@end] [--onion 1:3] [--query a=1&b=2] [--timeout S] [--root DIR] [--chrome PATH]');
516
+ console.error(USAGE);
506
517
  process.exit(2);
507
518
  }
508
519
  const report = await runCheck(opts);
@@ -1,5 +1,5 @@
1
1
  import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
2
- import { S as SequenceSpec, C as CompositionShape, a0 as AudioDescriptor } from './types-YmUaViL1.cjs';
2
+ import { S as SequenceSpec, C as CompositionShape, a1 as AudioDescriptor } from './types-DBbZVKGS.cjs';
3
3
  import { gsap } from 'gsap';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
2
- import { S as SequenceSpec, C as CompositionShape, a0 as AudioDescriptor } from './types-YmUaViL1.js';
2
+ import { S as SequenceSpec, C as CompositionShape, a1 as AudioDescriptor } from './types-DBbZVKGS.js';
3
3
  import { gsap } from 'gsap';
4
4
 
5
5
  /**
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ var chunkMHOO2CVU_cjs = require('./chunk-MHOO2CVU.cjs');
4
+ require('./chunk-IFZOI3JQ.cjs');
5
+ require('./chunk-K4JVI5HK.cjs');
6
+ require('./chunk-BPTW7L25.cjs');
7
+ require('./chunk-UZTKEYFP.cjs');
8
+
9
+
10
+
11
+ Object.defineProperty(exports, "CompositionSequence", {
12
+ enumerable: true,
13
+ get: function () { return chunkMHOO2CVU_cjs.CompositionSequence; }
14
+ });
15
+ //# sourceMappingURL=Composition-BK5SH6J7.cjs.map
16
+ //# sourceMappingURL=Composition-BK5SH6J7.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-2UFPSNVN.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-BK5SH6J7.cjs"}
@@ -0,0 +1,7 @@
1
+ export { CompositionSequence } from './chunk-UQDEDYVM.js';
2
+ import './chunk-DC4JPY6A.js';
3
+ import './chunk-YEPDQ7I3.js';
4
+ import './chunk-E7RONUZ7.js';
5
+ import './chunk-NHICCDTC.js';
6
+ //# sourceMappingURL=Composition-Q5TVG3HM.js.map
7
+ //# sourceMappingURL=Composition-Q5TVG3HM.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-BVI236ZR.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-Q5TVG3HM.js"}
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkWTRKDIKH_cjs = require('./chunk-WTRKDIKH.cjs');
3
+ var chunkUZTKEYFP_cjs = require('./chunk-UZTKEYFP.cjs');
4
4
 
5
5
  // src/Controller.ts
6
6
  var ICONS = {
@@ -456,7 +456,7 @@ var Controller = class _Controller {
456
456
  }
457
457
  /** Change the look of the bar (see {@link ControllerTheme}): only the keys you give change, `null` restores the default. */
458
458
  setTheme(theme) {
459
- chunkWTRKDIKH_cjs.warnUnknownOptions("Controller theme", theme, Object.keys(THEME_PROPERTIES));
459
+ chunkUZTKEYFP_cjs.warnUnknownOptions("Controller theme", theme, Object.keys(THEME_PROPERTIES));
460
460
  for (const key of Object.keys(THEME_PROPERTIES)) {
461
461
  if (!(key in theme)) continue;
462
462
  const value = theme[key];
@@ -1,7 +1,7 @@
1
- import { h as Movie } from './Movie-mtX9AkQ-.cjs';
1
+ import { h as Movie } from './Movie-DMxOoTdW.cjs';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
- import './types-YmUaViL1.cjs';
4
+ import './types-DBbZVKGS.cjs';
5
5
 
6
6
  /** 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
7
  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,7 @@
1
- import { h as Movie } from './Movie-CgYwa5QG.js';
1
+ import { h as Movie } from './Movie--_TJvdJa.js';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
- import './types-YmUaViL1.js';
4
+ import './types-DBbZVKGS.js';
5
5
 
6
6
  /** 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
7
  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,4 +1,4 @@
1
- import { warnUnknownOptions } from './chunk-MNM56B3X.js';
1
+ import { warnUnknownOptions } from './chunk-NHICCDTC.js';
2
2
 
3
3
  // src/Controller.ts
4
4
  var ICONS = {
@@ -1,6 +1,6 @@
1
1
  import { Application } from 'pixi.js';
2
2
  import { gsap } from 'gsap';
3
- import { U as Stop, e as AssetSpec, c as CompositionSpec } from './types-YmUaViL1.js';
3
+ import { V as Stop, e as AssetSpec, c as CompositionSpec } from './types-DBbZVKGS.js';
4
4
 
5
5
  interface AudioInspectOptions {
6
6
  /** Width of each loudness window in seconds. Default 0.1. */
@@ -1,6 +1,6 @@
1
1
  import { Application } from 'pixi.js';
2
2
  import { gsap } from 'gsap';
3
- import { U as Stop, e as AssetSpec, c as CompositionSpec } from './types-YmUaViL1.cjs';
3
+ import { V as Stop, e as AssetSpec, c as CompositionSpec } from './types-DBbZVKGS.cjs';
4
4
 
5
5
  interface AudioInspectOptions {
6
6
  /** Width of each loudness window in seconds. Default 0.1. */
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkWTRKDIKH_cjs = require('./chunk-WTRKDIKH.cjs');
3
+ var chunkUZTKEYFP_cjs = require('./chunk-UZTKEYFP.cjs');
4
4
 
5
5
  // src/Presenter.ts
6
6
  var OPTION_KEYS = ["canvas", "keyboard", "clickToAdvance", "swipe", "indicator", "autoAdvance", "loop", "accent", "onExit"];
@@ -147,7 +147,7 @@ var Presenter = class {
147
147
  };
148
148
  constructor(movie, options) {
149
149
  if (!options || !options.canvas) throw new Error("Presenter requires options.canvas (HTMLCanvasElement).");
150
- chunkWTRKDIKH_cjs.warnUnknownOptions("Presenter", options, OPTION_KEYS);
150
+ chunkUZTKEYFP_cjs.warnUnknownOptions("Presenter", options, OPTION_KEYS);
151
151
  this.movie = movie;
152
152
  this.options = {
153
153
  canvas: options.canvas,
@@ -1,7 +1,7 @@
1
- import { h as Movie } from './Movie-mtX9AkQ-.cjs';
1
+ import { h as Movie } from './Movie-DMxOoTdW.cjs';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
- import './types-YmUaViL1.cjs';
4
+ import './types-DBbZVKGS.cjs';
5
5
 
6
6
  interface PresenterOptions {
7
7
  canvas: HTMLCanvasElement;
@@ -1,7 +1,7 @@
1
- import { h as Movie } from './Movie-CgYwa5QG.js';
1
+ import { h as Movie } from './Movie--_TJvdJa.js';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
- import './types-YmUaViL1.js';
4
+ import './types-DBbZVKGS.js';
5
5
 
6
6
  interface PresenterOptions {
7
7
  canvas: HTMLCanvasElement;
package/dist/Presenter.js CHANGED
@@ -1,4 +1,4 @@
1
- import { warnUnknownOptions } from './chunk-MNM56B3X.js';
1
+ import { warnUnknownOptions } from './chunk-NHICCDTC.js';
2
2
 
3
3
  // src/Presenter.ts
4
4
  var OPTION_KEYS = ["canvas", "keyboard", "clickToAdvance", "swipe", "indicator", "autoAdvance", "loop", "accent", "onExit"];
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ var chunkK4JVI5HK_cjs = require('./chunk-K4JVI5HK.cjs');
4
+ require('./chunk-BPTW7L25.cjs');
5
+ require('./chunk-UZTKEYFP.cjs');
6
+ var pixi_js = require('pixi.js');
7
+
8
+ var SEPARABLE = {
9
+ 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);" },
10
+ "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);" },
11
+ "soft-light": {
12
+ 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);",
13
+ 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);"
14
+ },
15
+ "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);" },
16
+ "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);" },
17
+ darken: { gl: "return min(b, s);", gpu: "return min(b, s);" },
18
+ lighten: { gl: "return max(b, s);", gpu: "return max(b, s);" },
19
+ difference: { gl: "return abs(b - s);", gpu: "return abs(b - s);" },
20
+ exclusion: { gl: "return b + s - 2.0 * b * s;", gpu: "return b + s - 2.0 * b * s;" },
21
+ "linear-burn": { gl: "return max(0.0, b + s - 1.0);", gpu: "return max(0.0, b + s - 1.0);" }
22
+ };
23
+ var GL_NONSEP = `
24
+ float lum(vec3 c) { return 0.3 * c.r + 0.59 * c.g + 0.11 * c.b; }
25
+ vec3 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));
26
+ 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; }
27
+ vec3 setLum(vec3 c, float l) { return clipColor(c + (l - lum(c))); }
28
+ float sat(vec3 c) { return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b)); }
29
+ vec3 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); }`;
30
+ var GPU_NONSEP = `
31
+ fn lum(c: vec3<f32>) -> f32 { return 0.3 * c.r + 0.59 * c.g + 0.11 * c.b; }
32
+ fn 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));
33
+ 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; }
34
+ fn setLum(c: vec3<f32>, l: f32) -> vec3<f32> { return clipColor(c + (l - lum(c))); }
35
+ fn sat(c: vec3<f32>) -> f32 { return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b)); }
36
+ fn 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); }`;
37
+ var NONSEP = {
38
+ hue: { gl: "return setLum(setSat(cs, sat(cb)), lum(cb));", gpu: "return setLum(setSat(cs, sat(cb)), lum(cb));" },
39
+ saturation: { gl: "return setLum(setSat(cb, sat(cs)), lum(cb));", gpu: "return setLum(setSat(cb, sat(cs)), lum(cb));" },
40
+ color: { gl: "return setLum(cs, lum(cb));", gpu: "return setLum(cs, lum(cb));" },
41
+ luminosity: { gl: "return setLum(cb, lum(cs));", gpu: "return setLum(cb, lum(cs));" }
42
+ };
43
+ var GL_MAIN = `
44
+ vec3 cs = front.a > 0.0 ? front.rgb / front.a : vec3(0.0);
45
+ vec3 cb = back.a > 0.0 ? back.rgb / back.a : vec3(0.0);
46
+ vec3 mixed = front.rgb * (1.0 - back.a) + front.a * back.a * blendRGB(cb, cs) + (1.0 - front.a) * back.rgb;
47
+ finalColor = vec4(mixed, blendedAlpha) * uBlend;`;
48
+ var GPU_MAIN = `
49
+ let cs = select(vec3<f32>(0.0), front.rgb / front.a, front.a > 0.0);
50
+ let cb = select(vec3<f32>(0.0), back.rgb / back.a, back.a > 0.0);
51
+ let mixed = front.rgb * (1.0 - back.a) + front.a * back.a * blendRGB(cb, cs) + (1.0 - front.a) * back.rgb;
52
+ out = vec4<f32>(mixed, blendedAlpha) * blendUniforms.uBlend;`;
53
+ var CLASSES = /* @__PURE__ */ new Map();
54
+ function createBlendFilter(name) {
55
+ const C = CLASSES.get(name);
56
+ return C ? new C() : null;
57
+ }
58
+ function make(name, gl, gpu) {
59
+ const C = class extends pixi_js.BlendModeFilter {
60
+ static extension = { name, type: pixi_js.ExtensionType.BlendMode };
61
+ constructor() {
62
+ super({ gl: { functions: gl, main: GL_MAIN }, gpu: { functions: gpu, main: GPU_MAIN } });
63
+ }
64
+ };
65
+ CLASSES.set(name, C);
66
+ pixi_js.extensions.add(C);
67
+ }
68
+ var OWN_MODES = [...Object.keys(SEPARABLE), ...Object.keys(NONSEP)];
69
+ var done = false;
70
+ function registerBlendModes() {
71
+ chunkK4JVI5HK_cjs.setBlendFilterFactory(createBlendFilter);
72
+ if (done) return;
73
+ done = true;
74
+ for (const [name, s] of Object.entries(SEPARABLE)) {
75
+ make(
76
+ name,
77
+ `float B(float b, float s) { ${s.gl} }
78
+ vec3 blendRGB(vec3 cb, vec3 cs) { return vec3(B(cb.r, cs.r), B(cb.g, cs.g), B(cb.b, cs.b)); }`,
79
+ `fn B(b: f32, s: f32) -> f32 { ${s.gpu} }
80
+ fn 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)); }`
81
+ );
82
+ }
83
+ for (const [name, s] of Object.entries(NONSEP)) {
84
+ make(name, `${GL_NONSEP}
85
+ vec3 blendRGB(vec3 cb, vec3 cs) { ${s.gl} }`, `${GPU_NONSEP}
86
+ fn blendRGB(cb: vec3<f32>, cs: vec3<f32>) -> vec3<f32> { ${s.gpu} }`);
87
+ }
88
+ }
89
+
90
+ exports.OWN_MODES = OWN_MODES;
91
+ exports.createBlendFilter = createBlendFilter;
92
+ exports.registerBlendModes = registerBlendModes;
93
+ //# sourceMappingURL=blendModes-5E4G2ZZQ.cjs.map
94
+ //# sourceMappingURL=blendModes-5E4G2ZZQ.cjs.map
@@ -0,0 +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-5E4G2ZZQ.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"]}