pixi-effects 0.22.0 → 0.26.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 (92) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +17 -15
  3. package/ai/CHAT.md +2 -2
  4. package/ai/chat-template.html +3 -3
  5. package/ai/tools/check.mjs +10 -3
  6. package/ai/tools/pixi-effects.mjs +48 -0
  7. package/ai/tools/render.mjs +3 -3
  8. package/ai/tools/view.mjs +3 -3
  9. package/dist/{Base-gu-5X0Cv.d.ts → Base-BgnRXbtJ.d.ts} +1 -1
  10. package/dist/{Base-BzlPgd-D.d.cts → Base-DHVKqA46.d.cts} +1 -1
  11. package/dist/Composition-2GJSCHPP.cjs +16 -0
  12. package/dist/{Composition-BK5SH6J7.cjs.map → Composition-2GJSCHPP.cjs.map} +1 -1
  13. package/dist/Composition-C4FSWJZF.js +7 -0
  14. package/dist/{Composition-Q5TVG3HM.js.map → Composition-C4FSWJZF.js.map} +1 -1
  15. package/dist/Controller.cjs +2 -2
  16. package/dist/Controller.d.cts +2 -2
  17. package/dist/Controller.d.ts +2 -2
  18. package/dist/Controller.js +1 -1
  19. package/dist/{Movie--_TJvdJa.d.ts → Movie-DP5p1r4E.d.ts} +5 -1
  20. package/dist/{Movie-DMxOoTdW.d.cts → Movie-DXReF4-W.d.cts} +5 -1
  21. package/dist/Presenter.cjs +2 -2
  22. package/dist/Presenter.d.cts +2 -2
  23. package/dist/Presenter.d.ts +2 -2
  24. package/dist/Presenter.js +1 -1
  25. package/dist/{blendModes-HCP5XTNP.js → blendModes-G3NOAU3T.js} +5 -5
  26. package/dist/{blendModes-HCP5XTNP.js.map → blendModes-G3NOAU3T.js.map} +1 -1
  27. package/dist/{blendModes-5E4G2ZZQ.cjs → blendModes-YV43K6E7.cjs} +6 -6
  28. package/dist/{blendModes-5E4G2ZZQ.cjs.map → blendModes-YV43K6E7.cjs.map} +1 -1
  29. package/dist/{chunk-K4JVI5HK.cjs → chunk-3DLYAPK7.cjs} +7 -9
  30. package/dist/{chunk-K4JVI5HK.cjs.map → chunk-3DLYAPK7.cjs.map} +1 -1
  31. package/dist/{chunk-YEPDQ7I3.js → chunk-AXZUKNXF.js} +6 -6
  32. package/dist/{chunk-YEPDQ7I3.js.map → chunk-AXZUKNXF.js.map} +1 -1
  33. package/dist/{chunk-NHICCDTC.js → chunk-BXOCEZ4U.js} +12 -3
  34. package/dist/chunk-BXOCEZ4U.js.map +1 -0
  35. package/dist/{chunk-IFZOI3JQ.cjs → chunk-DP37TTMO.cjs} +842 -40
  36. package/dist/chunk-DP37TTMO.cjs.map +1 -0
  37. package/dist/{chunk-BPTW7L25.cjs → chunk-ECA2WNRX.cjs} +23 -9
  38. package/dist/chunk-ECA2WNRX.cjs.map +1 -0
  39. package/dist/{chunk-DC4JPY6A.js → chunk-PRZ4U6JN.js} +814 -22
  40. package/dist/chunk-PRZ4U6JN.js.map +1 -0
  41. package/dist/chunk-SY27HZC5.cjs +1727 -0
  42. package/dist/chunk-SY27HZC5.cjs.map +1 -0
  43. package/dist/{chunk-E7RONUZ7.js → chunk-TLYTYBWX.js} +20 -6
  44. package/dist/chunk-TLYTYBWX.js.map +1 -0
  45. package/dist/{chunk-UZTKEYFP.cjs → chunk-TWZNOUM7.cjs} +12 -3
  46. package/dist/chunk-TWZNOUM7.cjs.map +1 -0
  47. package/dist/chunk-XS6GSJCN.js +1724 -0
  48. package/dist/chunk-XS6GSJCN.js.map +1 -0
  49. package/dist/index.cjs +234 -77
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +5 -5
  52. package/dist/index.d.ts +5 -5
  53. package/dist/index.js +173 -16
  54. package/dist/index.js.map +1 -1
  55. package/dist/loader.css +1 -1
  56. package/dist/{music-FUN7N225.js → music-AF4VTFAF.js} +3 -3
  57. package/dist/{music-FUN7N225.js.map → music-AF4VTFAF.js.map} +1 -1
  58. package/dist/{music-GEAH2XCT.cjs → music-KB7YZLHP.cjs} +7 -7
  59. package/dist/{music-GEAH2XCT.cjs.map → music-KB7YZLHP.cjs.map} +1 -1
  60. package/dist/{sfx-2DYTYNYW.cjs → sfx-2O6UQEUP.cjs} +8 -8
  61. package/dist/{sfx-2DYTYNYW.cjs.map → sfx-2O6UQEUP.cjs.map} +1 -1
  62. package/dist/{sfx-3IDAJ3UZ.js → sfx-VUKARHDX.js} +3 -3
  63. package/dist/{sfx-3IDAJ3UZ.js.map → sfx-VUKARHDX.js.map} +1 -1
  64. package/dist/three.cjs +7 -7
  65. package/dist/three.d.cts +2 -2
  66. package/dist/three.d.ts +2 -2
  67. package/dist/three.js +3 -3
  68. package/dist/{types-DBbZVKGS.d.cts → types-CoT0ypcx.d.cts} +60 -4
  69. package/dist/{types-DBbZVKGS.d.ts → types-CoT0ypcx.d.ts} +60 -4
  70. package/docs/api.md +2 -2
  71. package/docs/dsl.md +140 -7
  72. package/llms-full.txt +328 -39
  73. package/llms.txt +6 -5
  74. package/package.json +3 -3
  75. package/{ai → skills/pixi-effects}/SKILL.md +16 -12
  76. package/{ai → skills/pixi-effects}/reference/cheatsheet.md +10 -7
  77. package/{ai → skills/pixi-effects}/reference/pitfalls.md +22 -4
  78. package/{ai → skills/pixi-effects}/reference/recipes.md +133 -2
  79. package/{ai/tools → skills/pixi-effects/scripts}/save-image.py +1 -1
  80. package/{ai → skills/pixi-effects}/template.html +6 -6
  81. package/dist/Composition-BK5SH6J7.cjs +0 -16
  82. package/dist/Composition-Q5TVG3HM.js +0 -7
  83. package/dist/chunk-BPTW7L25.cjs.map +0 -1
  84. package/dist/chunk-DC4JPY6A.js.map +0 -1
  85. package/dist/chunk-E7RONUZ7.js.map +0 -1
  86. package/dist/chunk-IFZOI3JQ.cjs.map +0 -1
  87. package/dist/chunk-MHOO2CVU.cjs +0 -840
  88. package/dist/chunk-MHOO2CVU.cjs.map +0 -1
  89. package/dist/chunk-NHICCDTC.js.map +0 -1
  90. package/dist/chunk-UQDEDYVM.js +0 -838
  91. package/dist/chunk-UQDEDYVM.js.map +0 -1
  92. package/dist/chunk-UZTKEYFP.cjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.0
4
+
5
+ **Added**
6
+
7
+ - **Agent skill.** `npx skills add yjmtmtk/pixi-effects` registers pixi-effects as a skill in Claude Code, Codex, Cursor, Gemini CLI, Copilot, OpenCode and about 70 more agents; its description starts from the task (a video, motion graphics, an animated title, a lower third, a promo, a chart), so an agent can reach it without being told the library's name. Claude Code also installs it as a plugin (`/plugin install pixi-effects --marketplace yjmtmtk/pixi-effects`).
8
+ - **`pixi-effects` bin** (named like the package): `npx pixi-effects check my-video.html`, `npx pixi-effects render … -o x.mp4`, `npx pixi-effects view …` run the same three tools with nothing installed, and a mistyped command says which one you meant. The three `pixi-effects-check|render|view` bins stay; `npx pixi-effects-check` on its own never worked (npm looks for a package of that name), the docs now say `npx pixi-effects check`. **Fixed:** the three tools printed nothing and exited 0 when npm started them through its symlink (`./node_modules/.bin/pixi-effects-check`); they now run.
9
+ - **`movie.audioBlocked`**: true while the browser keeps the movie's sound silent until a tap (iOS Safari); a player can then ask for the tap. The landing page does: the reel and the gallery pieces start with one tap inside the frame when the sound is locked.
10
+
11
+ **Changed**
12
+
13
+ - The skill moved from `ai/` to `skills/pixi-effects/` (`SKILL.md`, `template.html`, `reference/`, `scripts/save-image.py`); `ai/` keeps the chat guide and the tools. Old links to `ai/SKILL.md`, `ai/reference/*` and `ai/template.html` no longer exist. The skill's commands are `npx pixi-effects-check|render|view`.
14
+
15
+ ## 0.25.0
16
+
17
+ One release holds three milestones that were built one after the other: **composition** (named mattes, the luma wipe), **light** (lights, shadows, fog) and **freedom of drawing** (the shader layer, the `warp` filter). Wipe, iris and dissolve transitions now also work on WebGL (they drew nothing there before).
18
+
19
+ **Added**
20
+
21
+ - **Light, shadows and fog for 2.5D.** A `{ type: 'light', kind: 'ambient' | 'point' | 'spot' | 'parallel' }` layer (a carrier like the camera: its numbers go in `initial` / `keyframes`) lights every `threeD` layer of its composition, per pixel, in the layer's own mesh shader (no extra pass; GLSL and WGSL): Lambert shading with `falloff` (`none`, `smooth`, `inverseSquare`), a spot's cone and feather, a sun. `lit: false` opts a layer out. Shadows need `castsShadows` on the light AND on the layer: hard by default, `shadowDiffusion` for a penumbra that widens with distance, up to 4 casters per receiver, the strongest wins. The camera takes `fogNear` / `fogFar` / `fogColor` / `fogAmount` for depth fog. A composition with no light and no fog builds nothing and draws the same picture at the same cost: frames of all ten `threeD` pages of the repository are pixel-identical before and after. Checked against an independent JS reference (every pixel within 2/255) on WebGPU and WebGL for every kind, the falloffs, a card turned on two axes, a card seen from its back, fog, shadows (hard, soft, two and four casters, a turned caster), and together with depth of field, blend modes, the layer's own filters, cards, nested and time-remapped compositions and colour keyframes. Cost at 1080p on one machine: lights add 1 to 2.6 ms; soft shadows are the expensive part (3 soft lights over 20 layers: about 20 ms). Warnings, said once at build with a fix: no ambient light, a half-set `castsShadows`, too many lights or casters (which are dropped), 3 or more lights with soft shadows, misplaced keys and aliases (`target` → `lookAtX/Y/Z`, `angle` → `coneAngle`, `castShadows` → `castsShadows`, `intensity: 80`), `fogFar` not beyond `fogNear`. `movie.inspect` gives each lit layer a `light` number and `check --at` prints them most lit first.
22
+ - **Shader layer.** `{ type: 'shader', fragment, uniforms, width, height, resolution, transparent }` draws a Shadertoy `mainImage` (GLSL ES 3.00) as a layer: `iResolution`, `iTime` (the layer's own seconds; a time-remapped composition's clock inside it) and `iFrame` are declared for you, and so is every `uniforms` entry (a number is a `float`, 2 to 4 numbers a vector, a hex colour a `vec3`); keyframes move them as `'uniforms.speed'` and `'uniforms.tint.0'`. It is a layer like any other: masks, named mattes (it can be a matte too), filters, blend modes, `threeD` and lights work on it. Every shader layer draws with ONE shared WebGL2 context (a context per layer would hit the browser's limit of about 16) and copies the result into a canvas of its own; a lost context is made again on the next frame. A compile error is reported in the author's own line numbers (`line 2: 'x' : undeclared identifier`) and the layer is a magenta checkerboard, never a silent blank. Static warnings with a fix: no `mainImage`, `gl_FragColor`, `texture2D`, `varying`, `#version`, a declared built-in, `iMouse` / `iChannel` / `iDate` (not provided), a `while` loop or a `for` with a bound that is not a constant, a bad `uniforms` value or name, `resolution`, `transparent`. Checked on WebGPU and WebGL: pixels against the formula, every seek order identical, error lines, transparency (premultiplied), layers of different sizes on one context, a rebuilt movie, a lost context, masks, mattes, filters, blend, `threeD`, lights, export. Cost in a prototype at 1080p on one machine: 20 small heavy shaders +11 to +17 ms a frame, one full-size shader about +10 ms. No `iChannel` textures, mouse or feedback in this version, and a shader cannot read what is behind it (it is a layer, not a filter).
23
+ - **`warp` filter.** `{ type: 'warp', kind: 'wave' | 'haze', strength, scale, speed, angle, seed }` bends a layer's picture: a travelling wave (water, a flag) or drifting noise (heat haze). Its own GLSL and WGSL, a pure function of the frame's time through the same `TimeFilter` hook as `grain` (the sub-frames of a motion-blurred frame share one time), padded by `strength` so a shifted edge is not cut, options animated by the filter's name. Checked stripe by stripe against a JS reference on both backends (a worked-by-hand wave and constants from an independent implementation of the integer hash for `haze`). Aliases `distort`, `displace`, `ripple`, `wavy`, `heatHaze` point to it.
24
+ - **Docs and examples:** `docs/dsl.md` (`shader`, the filter table and a distortion paragraph), the cheatsheet, SKILL, recipes `shader-plasma` and `warp-water`, pitfalls 89–92, the guide "Shaders and warps", Playground example 20, and a gallery piece (50 now): `shader-garden`, four formulas and no pictures (a flowing field under a title bent like water, contour lines of a noise terrain seen only through letters, ripples through a disc), made by a Claude Sonnet 5.5 subagent from the docs alone (its notes are in `examples/gallery/_notes/`; the two gaps it found are fixed: a mask cuts what the shader drew and does not move the pattern, and a small shader layer sits by its top-left corner).
25
+ - **Docs and examples:** `docs/dsl.md` (Lights, Shadows, Fog), the cheatsheet, SKILL, recipes `spot-room` and `fog-depth`, pitfalls 85–88, the 2.5D guide, Playground example 19, and a gallery piece (49 now): `spotlight-room`, a dark room where one tight lamp picks out three pictures and they throw shadows on the wall, with slow camera and depth fog (made by a Claude Sonnet 5.5 subagent from the docs alone; its stumble notes are in `examples/gallery/_notes/`, and the gaps it found are fixed: the size of a spot's pool and the card that hides it, a hard shadow reading as a slab, how to build a floor, the `check` light line cut at 8 layers).
26
+
27
+ - **Named mattes.** A layer can be named and used as the `mask` of any number of layers in the same composition: `mask: 'band'` (the matte reads its opacity), `mask: { layer: 'band', channel: 'luma', invert: true }` (its brightness, Rec. 709; the opposite) and a list `mask: ['panel', { layer: 'hole', invert: true }]` (an intersection; subtracting a matte is inverting it). The matte layer itself is not drawn: it is drawn once per frame into a texture (so three layers sharing a moving matte cost one pass for the matte) and a `MatteFilter` cuts each layer that names it. `blendMode` works with every matte. Warnings, said once at build with a fix: a name nobody has (did-you-mean), the same name twice, a matte that is not alive for as long as its users (they would vanish), a `threeD` matte or a masked `threeD` layer, a matte with a mask of its own, `maskInverted` written with a named matte, a `channel` that is not `alpha` / `luma` (`luminance` → `luma`), a masked layer with a `parent`, and more than 8 mattes in one composition (each is a texture the size of the composition: about 8 MiB at 1080p). `inspect` skips matte layers.
28
+ - **Luma wipe.** The transition `{ kind: 'luma', from, to, at, duration, map, softness, flip }` follows a brightness map: dark parts change first. `map` is `'linear'`, `'diagonal'`, `'radial'` (computed on the GPU from the position on screen, a circle whatever the aspect ratio: no image, no canvas) or the name of a grayscale image asset; `softness` is the soft band (0.1), `flip` reverses it. An unknown map name warns once with the closest built-in. A layer a transition wraps keeps its named matte.
29
+ - **A gallery piece for mattes and the luma wipe** (48 now): `matte-reel`, one moving matte shared by gradient "photographs", hairlines and captions, an inverted hole, a headline that blends in through a luma ramp, a multiply band, and a luma wipe (with a venetian-blind map drawn on a canvas) to an end card (made by a Claude Sonnet 5.5 subagent from the docs alone; its stumble notes are in `examples/gallery/_notes/`, and the two gaps it found are fixed in the docs: outside the matte layer's shape the matte is 0 for `luma` too, and a whole composition can be the masked layer).
30
+ - **Docs and examples:** `docs/dsl.md` (Named mattes and luma, `luma` transition, blend rule 10), the cheatsheet, SKILL, recipes `shared-matte` and `luma-wipe`, pitfalls 81–84, the transitions and images guides, and Playground example 18.
31
+
32
+ **Changed**
33
+
34
+ - **Inverted masks (`maskInverted`) and text, image and video masks now go through the matte filter** (the same one as a named matte); a plain shape mask that is not inverted stays the stencil (the cheapest, hard-edged). The `AlphaMask` code, the `erase` wrapper group of 0.22 and the warning "a text mask with a blend loses the blend" are gone: **`blendMode` now works with every kind of mask**. Measured before and after on the 16 gallery and example pages that use masks (frames at 0, 25, 50, 75 and 99 %): 14 are pixel-identical (largest difference 0 to 1 of 255), `ma` differs in 10 edge pixels (largest 11 of 255), and the `05-composition-mask` example now draws its `maskInverted` hole correctly inside a nested composition (it was cut off at the bottom, and missing in the last frames).
35
+
36
+ **Fixed**
37
+
38
+ - **Wipe, iris and dissolve transitions did nothing on WebGL**: their fragment shader declared the frame uniforms without `highp`, so it did not link ("Precisions of uniform 'uOutputFrame' differ between VERTEX and FRAGMENT shaders") and the transition drew nothing (WebGPU, the default, was fine). The transitions are now tested on both backends.
39
+
3
40
  ## 0.22.0
4
41
 
5
42
  **Added**
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # pixi-effects
2
2
 
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.
3
+ > **Status**: experimental — current release `0.26.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/)** · 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.
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
 
7
7
  ## Make a video by asking an AI
8
8
 
@@ -10,7 +10,7 @@ Give any AI (ChatGPT, Claude, Codex, Cursor, Claude Code …) one sentence:
10
10
 
11
11
  > Use https://github.com/yjmtmtk/pixi-effects to make a video: a 10-second title for a bakery, warm colours, a soft chime at the end.
12
12
 
13
- That is all it needs. **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 [`ai/SKILL.md`](./ai/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.
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
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/).
16
16
 
@@ -19,7 +19,7 @@ Declarative composition and video rendering for the web. After Effects-style tim
19
19
  - **Expression language** — sprinkle `'GW * 0.5'` or `'min(W, H) / 2'` anywhere a number goes. Resolved at runtime against a sequence-relative scope.
20
20
  - **Filters by name** — `filters: [{ type: 'glow', outerStrength: 3 }]`, no import: `blur noise alpha colorMatrix` (pixi.js) and the 38 filters of [`pixi-filters`](https://github.com/pixijs/filters) (`dropShadow crt rgbSplit oldFilm glitch pixelate twist …`), animated with `'filters.<name>.<option>'`. Any other PixiJS `Filter` goes in as `{ type: 'custom' }`.
21
21
  - **Draw-on strokes and changing text** — `trimEnd` 0 → 1 draws a line, border or SVG path on; a keyframe `set: { text }` swaps a string and `visibleChars` is a typewriter.
22
- - **Masks, transitions, chroma key** — inline masks; chroma key; seven scene transitions (`crossfade`, `wipe`, `iris`, `slide`, `dip`, `zoom`, `dissolve`).
22
+ - **Masks, transitions, chroma key** — inline masks; chroma key; eight scene transitions (`crossfade`, `wipe`, `iris`, `slide`, `dip`, `zoom`, `dissolve`, `luma`).
23
23
  - **Presets** — `kenBurns` for stills, `withFade` for fade-in/out; `wiggle`, `stagger`, `animateText`, `followPath`, `particles` and `react` return keyframes or layers (seeded, so every render is the same).
24
24
  - **Presentations** — `stops` in the composition pause the movie where you say; `Presenter` (`pixi-effects/presenter`) moves on with arrow keys, a click or a swipe, with a page overview (G), a presenter view with notes and a timer (P), and `deck()` to lay out pages and steps. Export a deck as PDF (`movie.exportPDF()`, `pixi-effects-render talk.html -o talk.pdf`). See the [Presenting guide](https://yjmtmtk.github.io/pixi-effects/guide/presenting.html).
25
25
  - **Music written as text** — `{ type: 'audio', music: { bpm: 96, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4' }], drums: { kick: 'x...x...' } } }`: a tune with no audio file, played by a small built-in synthesiser (keys, pluck, pad, bass, lead, bell, music box, drums; chords by name, swing, fades, a ritardando). Deterministic, in the exported file at the same moment; an AI writes it as easily as a title.
@@ -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.22.0/dist/index.js",
55
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.22.0/dist/Controller.js"
54
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.0/dist/index.js",
55
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.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.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.
66
+ Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.26.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.22.0/dist/three.js"
82
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.0/dist/three.js"
83
83
  }
84
84
  }
85
85
  </script>
@@ -166,20 +166,20 @@ sequences: [
166
166
  ]
167
167
  ```
168
168
 
169
- `+z` is toward the viewer, rotations are degrees (CSS signs), and with `z = 0` and the default camera a `threeD` layer looks identical to a 2D one. See [DSL reference § 3D layers & camera](./docs/dsl.md#3d-layers--camera) and [`examples/11-depth.html`](./examples/11-depth.html).
169
+ `+z` is toward the viewer, rotations are degrees (CSS signs), and with `z = 0` and the default camera a `threeD` layer looks identical to a 2D one. Add `{ type: 'light', kind: 'spot', … }` layers for shading and shadows, and `fogNear` / `fogFar` on the camera for depth fog (nothing changes in a composition with neither). See [DSL reference § 3D layers & camera](./docs/dsl.md#3d-layers--camera) and [`examples/11-depth.html`](./examples/11-depth.html).
170
170
 
171
171
  ## For AI agents
172
172
 
173
173
  This library is designed to be written by AI: a video is plain data, and every mistake we found while having AI sessions build test animations is either warned about at runtime or written down.
174
174
 
175
175
  - [`llms.txt`](./llms.txt) / [`llms-full.txt`](./llms-full.txt) — index and full text for LLM tooling (generated from the files below).
176
- - [`ai/SKILL.md`](./ai/SKILL.md) — a [skill](https://docs.claude.com/en/docs/claude-code/skills) (workflow, rules, verification loop). Copy the `ai/` folder to `~/.claude/skills/pixi-effects/` (or your project's `.claude/skills/`) to have Claude load it automatically when you ask for a video.
177
- - [`ai/reference/cheatsheet.md`](./ai/reference/cheatsheet.md), [`recipes.md`](./ai/reference/recipes.md) (tested), [`pitfalls.md`](./ai/reference/pitfalls.md), and a starter [`ai/template.html`](./ai/template.html).
176
+ - [`skills/pixi-effects/SKILL.md`](./skills/pixi-effects/SKILL.md) — a [skill](https://docs.claude.com/en/docs/claude-code/skills) (workflow, rules, verification loop). Copy the `ai/` folder to `~/.claude/skills/pixi-effects/` (or your project's `.claude/skills/`) to have Claude load it automatically when you ask for a video.
177
+ - [`skills/pixi-effects/reference/cheatsheet.md`](./skills/pixi-effects/reference/cheatsheet.md), [`recipes.md`](./skills/pixi-effects/reference/recipes.md) (tested), [`pitfalls.md`](./skills/pixi-effects/reference/pitfalls.md), and a starter [`skills/pixi-effects/template.html`](./skills/pixi-effects/template.html).
178
178
 
179
- - **Look at a page with its timeline, and scrub one with the other:** `npx pixi-effects-view my-video.html` (or `node ai/tools/view.mjs …`) opens your browser on a viewer: the page on top, the timeline of every layer under it with a playhead that follows the movie. Click or drag the timeline to seek, click a layer's name to jump to where it starts, Space plays, ← / → step a frame, + / − (or Ctrl/⌘ + wheel) zoom, the names stay in place. For the human who wants to see what the AI made.
180
- - **Render a page to a video file, headless:** `npx pixi-effects-render my-video.html -o my-video.mp4` (or `node ai/tools/render.mjs …`; Node ≥ 22 and Chrome installed, no dependencies). The container follows the extension (`mp4 webm mov mkv`); `--quality very-low…very-high`, `--query lang=ja` (added to the page URL), `--fail-on-warn` (exit 1 when the page logged a warning), `--quiet`. It waits for `window.__ready`, runs `movie.render()` and streams the file to disk; exit 0 = written, 1 = the page or the render failed (no file). For scripts, CI and batches; `pixi-effects-check` below is the review.
181
- - **A talk as a PDF:** `npx pixi-effects-render my-talk.html -o my-talk.pdf` (one page per page of a `deck()` / `stops` movie; `--all-stops` makes a page of every step). The review tool writes `stops.png` for such a page, one picture per stop.
182
- - **One-command review for the AI that wrote the page:** `npx pixi-effects-check my-video.html` (or `node ai/tools/check.mjs my-video.html`; Node ≥ 22 and Chrome installed, no dependencies). It opens the page in its own headless Chrome and reports warnings, layout (`movie.inspect` over the whole timeline), the soundtrack (`movie.inspectAudio`) and a real export decoded again, and writes a contact sheet PNG and a `timeline.html` (every layer as a bar on a time axis) to look at. Exit code 0 / 1.
179
+ - **Look at a page with its timeline, and scrub one with the other:** `npx pixi-effects view my-video.html` (or `node ai/tools/view.mjs …`) opens your browser on a viewer: the page on top, the timeline of every layer under it with a playhead that follows the movie. Click or drag the timeline to seek, click a layer's name to jump to where it starts, Space plays, ← / → step a frame, + / − (or Ctrl/⌘ + wheel) zoom, the names stay in place. For the human who wants to see what the AI made.
180
+ - **Render a page to a video file, headless:** `npx pixi-effects render my-video.html -o my-video.mp4` (or `node ai/tools/render.mjs …`; Node ≥ 22 and Chrome installed, no dependencies). The container follows the extension (`mp4 webm mov mkv`); `--quality very-low…very-high`, `--query lang=ja` (added to the page URL), `--fail-on-warn` (exit 1 when the page logged a warning), `--quiet`. It waits for `window.__ready`, runs `movie.render()` and streams the file to disk; exit 0 = written, 1 = the page or the render failed (no file). For scripts, CI and batches; `pixi-effects-check` below is the review.
181
+ - **A talk as a PDF:** `npx pixi-effects render my-talk.html -o my-talk.pdf` (one page per page of a `deck()` / `stops` movie; `--all-stops` makes a page of every step). The review tool writes `stops.png` for such a page, one picture per stop.
182
+ - **One-command review for the AI that wrote the page:** `npx pixi-effects check my-video.html` (or `node ai/tools/check.mjs my-video.html`; Node ≥ 22 and Chrome installed, no dependencies). It opens the page in its own headless Chrome and reports warnings, layout (`movie.inspect` over the whole timeline), the soundtrack (`movie.inspectAudio`) and a real export decoded again, and writes a contact sheet PNG and a `timeline.html` (every layer as a bar on a time axis) to look at. Exit code 0 / 1.
183
183
 
184
184
  They ship in the npm package (`node_modules/pixi-effects/ai/`).
185
185
 
@@ -217,6 +217,8 @@ Rendering uses the [WebCodecs API](https://developer.mozilla.org/docs/Web/API/We
217
217
  | WebM (vp9)| ✅ | ✅ | ✅ | ✅ |
218
218
  | MOV (avc) | ✅ | ✅ | ✅ | ✅ |
219
219
 
220
+ **Renderer:** the movie tries WebGPU first and falls back to WebGL (with a console warning) when WebGPU is missing or its device cannot start, so a page needs no setting. Every filter the library ships has a GLSL and a WGSL version. A frame is the same picture however it was reached **on one renderer**; the two renderers can differ in a few edge pixels, so do not compare an export from a WebGPU machine and one from a WebGL machine byte for byte. Tested on Chrome with an Apple GPU only (WebGPU on Metal, WebGL through ANGLE); other GPUs, Windows and Linux are untested.
221
+
220
222
  Some codecs (notably `aac` audio in older Firefox) may need the [mediabunny polyfill encoders](https://mediabunny.dev/) (`@mediabunny/aac-encoder`, etc.).
221
223
 
222
224
  ## License
package/ai/CHAT.md CHANGED
@@ -60,7 +60,7 @@ That is a complete, working video (the block between the two markers). Change th
60
60
  - **Scenes.** Stack layers with `at` / `duration` over an opaque background rect. Do not remove layers.
61
61
  - **Sound with no files:** `{ type: 'audio', sfx: 'swoosh', at: 2 }`. Presets: `click pop swoosh swipe hit riser chime beep coin glitch typewriter`; optional `pitch`, `brightness`. A `riser` is loudest at its **end**: start it `duration` seconds before the hit. This page cannot load music or images from your own files; use shapes, text, sfx and `music` (below).
62
62
  - **Repeat with code, never by copying:** `Array.from({ length: 12 }, (_, i) => ({ type: 'shape', shape: 'circle', radius: 20, at: i * 0.15, duration: DURATION - i * 0.15, initial: { x: 120 + i * 90, y: 360, fillColor: '#ffd166' }, keyframes: [{ at: 0, from: { scale: 0 }, to: { scale: 1 }, duration: 0.4, ease: 'back.out(2)' }] }))` returns twelve layers: spread them into `sequences` with `...`.
63
- - **Helpers already imported** in the template: `kenBurns` (slow zoom for an image layer), `withFade`, `wiggle` (seeded shake as keyframes), `stagger`, `animateText` (per letter / word entrances), `followPath`, `particles` (seeded confetti / snow / sparks as layers), `deck` (slides). Each returns keyframes or layers: `keyframes: wiggle(...)`, `...particles(...)`. The full signatures are in `https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/reference/cheatsheet.md` (22 KB; read it only if you need a helper).
63
+ - **Helpers already imported** in the template: `kenBurns` (slow zoom for an image layer), `withFade`, `wiggle` (seeded shake as keyframes), `stagger`, `animateText` (per letter / word entrances), `followPath`, `particles` (seeded confetti / snow / sparks as layers), `deck` (slides). Each returns keyframes or layers: `keyframes: wiggle(...)`, `...particles(...)`. The full signatures are in `https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/cheatsheet.md` (22 KB; read it only if you need a helper).
64
64
  - **Advanced:** `const INIT = { assets: [...], composition: { transitions: [...] } }` in the edit block adds `movie.init` options (the page merges it); most videos need nothing here.
65
65
  - **Not available on this page:** named filters beyond `blur`, `noise`, `alpha`, `colorMatrix`, `grain` (the other 38 need the `pixi-filters` package; film grain is `{ type: 'grain' }`), three.js layers, your own image / video / music files. Say so rather than guessing.
66
66
 
@@ -107,4 +107,4 @@ Read your file once for these: every layer has a `type`; every keyframe `at` is
107
107
 
108
108
  A blank preview with no red box usually means the chat's preview window blocks external scripts or does not allow `eval`. Tell the person to **save the file as `video.html` and open it in a browser** (Chrome, Edge or Firefox on a computer); that always works.
109
109
 
110
- More: the full skill (`ai/SKILL.md`) and pitfalls (`ai/reference/pitfalls.md`) are for an AI with a shell and a browser; this page is the short version for a chat.
110
+ More: the full skill (`skills/pixi-effects/SKILL.md`) and pitfalls (`skills/pixi-effects/reference/pitfalls.md`) are for an AI with a shell and a browser; this page is the short version for a chat.
@@ -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.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"
64
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.0/dist/index.js",
65
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.0/dist/Controller.js",
66
+ "pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.26.0/dist/Presenter.js"
67
67
  }
68
68
  }
69
69
  </script>
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * pixi-effects check — review a composition page in ONE command, without driving a browser by hand.
4
4
  *
5
- * node ai/tools/check.mjs my-video.html (or: npx pixi-effects-check my-video.html)
5
+ * node ai/tools/check.mjs my-video.html (or: npx pixi-effects check my-video.html)
6
6
  *
7
7
  * It starts a private static server and a private headless Chrome (nothing shared with other sessions), opens the page,
8
8
  * waits for `window.__ready === true`, then checks: console warnings / errors, `movie.inspect` over the whole timeline
@@ -11,7 +11,7 @@
11
11
  * (`sheet.png`, `<name>.<format>`, `report.json`; a presentation with `stops` also gets `stops.png`, one picture per stop) and a short summary is printed. Exit code 0 = nothing to fix, 1 = problems.
12
12
  *
13
13
  * Needs: Node >= 22 (built-in WebSocket), Chrome / Chromium installed (or --chrome PATH / CHROME=PATH). No npm dependencies.
14
- * The page must follow ai/template.html: it exposes `window.movie` and sets `window.__ready = true` (and `window.__logs`).
14
+ * The page must follow skills/pixi-effects/template.html: it exposes `window.movie` and sets `window.__ready = true` (and `window.__logs`).
15
15
  *
16
16
  * Options: --draft (the export is a draft: half size, low quality, no motion blur: a much smaller file; the drawing itself is not faster) · --at LIST (pictures at moments you name: 3.5, 50%, f120, title@end → frames/*.png and at.png) ·
17
17
  * --query "a=1&b=2" (added to the page address: for a page that reads it, such as one video of a batch) ·
@@ -342,6 +342,9 @@ export async function runCheck(opts, log = console.log) {
342
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
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
344
  if (rows.some(([, px]) => px > 0)) w.depthBlur = Object.fromEntries(rows);
345
+ // light: how lit each threeD layer is at this moment (left out when the composition has no light)
346
+ const lit = await cdp.eval(`movie.inspect(${w.frame}).then(r => r.layers.filter(l => l.light !== undefined).map(l => [l.path, l.light]))`).catch(() => []);
347
+ if (lit.length) w.light = Object.fromEntries(lit);
345
348
  }
346
349
  const atSheet = await cdp.eval(`movie.contactSheet({ frames: ${JSON.stringify(wanted.map(w => w.frame))}, as: 'dataURL' })`);
347
350
  const atFile = path.join(outDir, 'at.png');
@@ -479,6 +482,10 @@ function finish(report, outDir, log) {
479
482
  const shown = Object.entries(w.depthBlur).slice(0, 8).map(([k, px]) => `${k} ${px} px`).join(', ');
480
483
  L.push(` depth ${w.label} — ${shown}${Object.keys(w.depthBlur).length > 8 ? ', …' : ''} (blur of each threeD layer at that moment; 0 = sharp)`);
481
484
  }
485
+ for (const w of (report.at ?? []).filter(x => x.light)) {
486
+ const shown = Object.entries(w.light).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([k, v]) => `${k} ${v}`).join(', '); // the most lit first
487
+ L.push(` light ${w.label} — ${shown}${Object.keys(w.light).length > 8 ? ', …' : ''} (the most lit threeD layers first: 1 = as bright as with no light, 0 = black; the number is the layer's middle, not what you can see)`);
488
+ }
482
489
  if (report.stops) {
483
490
  const s = report.stops;
484
491
  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`);
@@ -507,7 +514,7 @@ function finish(report, outDir, log) {
507
514
 
508
515
  // ───────────────────────────── CLI ─────────────────────────────
509
516
 
510
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
517
+ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) { // the real path: npm starts a bin through a symlink
511
518
  try {
512
519
  if (typeof WebSocket === 'undefined') throw new Error('Node >= 22 is needed (built-in WebSocket)');
513
520
  const opts = parseArgs(process.argv.slice(2));
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pixi-effects — one command for the three tools, named like the package so `npx pixi-effects check my-video.html` works with nothing installed.
4
+ *
5
+ * npx pixi-effects check my-video.html [--at 3,6] [--out dir] review it: warnings, layout, sound, a contact sheet, a real export
6
+ * npx pixi-effects render my-video.html -o my-video.mp4 export the file
7
+ * npx pixi-effects view my-video.html open it in a browser with a timeline
8
+ *
9
+ * Everything after the command goes to the tool unchanged (`check.mjs`, `render.mjs`, `view.mjs`; their own headers list every option), and so do the
10
+ * exit code and the output. The three older bins (`pixi-effects-check|render|view`) still work.
11
+ */
12
+ import { spawn } from 'node:child_process';
13
+ import { realpathSync } from 'node:fs';
14
+ import { dirname, resolve } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const TOOLS = { check: 'check.mjs', render: 'render.mjs', view: 'view.mjs' };
18
+ const USAGE = `usage:
19
+ pixi-effects check <page.html> [--at 3,6] [--out dir] review a video page: warnings, layout, sound, contact sheet, real export
20
+ pixi-effects render <page.html> -o <file.mp4> export the video
21
+ pixi-effects view <page.html> look at it in a browser, with a timeline
22
+ (every option of a command is listed in the header of its file: node_modules/pixi-effects/ai/tools/<command>.mjs)`;
23
+
24
+ function nearest(word) {
25
+ const d = (a, b) => { const m = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]); for (let j = 1; j <= b.length; j++) m[0][j] = j;
26
+ for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) m[i][j] = Math.min(m[i - 1][j] + 1, m[i][j - 1] + 1, m[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)); return m[a.length][b.length]; };
27
+ const best = Object.keys(TOOLS).map(k => [k, d(word, k)]).sort((x, y) => x[1] - y[1])[0];
28
+ return best[1] <= 2 ? best[0] : null;
29
+ }
30
+
31
+ /** Which tool a command line asks for: `{ tool, args }`, or `{ usage }` (no command / --help), or `{ error }`. */
32
+ export function route(argv) {
33
+ const [cmd, ...args] = argv;
34
+ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') return { usage: USAGE };
35
+ if (TOOLS[cmd]) return { tool: TOOLS[cmd], args };
36
+ const guess = /\.html?$/i.test(cmd) ? 'check' : nearest(cmd);
37
+ return { error: `pixi-effects: unknown command "${cmd}"${guess ? `; did you mean "${guess}"?` : ''} The commands are: ${Object.keys(TOOLS).join(', ')}.\n${USAGE}` };
38
+ }
39
+
40
+ const self = fileURLToPath(import.meta.url);
41
+ if (process.argv[1] && realpathSync(resolve(process.argv[1])) === realpathSync(self)) {
42
+ const r = route(process.argv.slice(2));
43
+ if (r.usage) { console.log(r.usage); process.exit(process.argv.length > 2 ? 0 : 2); }
44
+ if (r.error) { console.error(r.error); process.exit(2); }
45
+ const child = spawn(process.execPath, [resolve(dirname(self), r.tool), ...r.args], { stdio: 'inherit' });
46
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => child.kill(sig)); // a harness that stops this process stops the tool (and its Chrome) too
47
+ child.on('exit', (code, signal) => process.exit(code ?? (signal ? 128 : 1)));
48
+ }
@@ -2,10 +2,10 @@
2
2
  /**
3
3
  * pixi-effects render — turn a composition page into a video file, headless, in ONE command.
4
4
  *
5
- * node ai/tools/render.mjs my-video.html -o my-video.mp4 (or: npx pixi-effects-render my-video.html -o out.mp4)
5
+ * node ai/tools/render.mjs my-video.html -o my-video.mp4 (or: npx pixi-effects render my-video.html -o out.mp4)
6
6
  *
7
7
  * It starts a private static server and a private headless Chrome, opens the page, waits for `window.__ready === true`
8
- * (the contract of ai/template.html), runs `movie.render()` in the page and streams the file to disk. Nothing is decoded or
8
+ * (the contract of skills/pixi-effects/template.html), runs `movie.render()` in the page and streams the file to disk. Nothing is decoded or
9
9
  * reviewed: that is `pixi-effects-check`'s job. Use this one to produce the file (a script, CI, a batch).
10
10
  *
11
11
  * Needs: Node >= 22 (built-in WebSocket), Chrome / Chromium installed (or CHROME=PATH / --chrome PATH). No npm dependencies.
@@ -200,7 +200,7 @@ export async function runRender(opts, log = console.log, progress = () => {}) {
200
200
 
201
201
  // ───────────────────────────── CLI ─────────────────────────────
202
202
 
203
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
203
+ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) { // the real path: npm starts a bin through a symlink
204
204
  try {
205
205
  if (typeof WebSocket === 'undefined') throw new Error('Node >= 22 is needed (built-in WebSocket)');
206
206
  const opts = parseRenderArgs(process.argv.slice(2));
package/ai/tools/view.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * pixi-effects view — look at a composition page WITH its timeline, and scrub the one with the other.
4
4
  *
5
- * node ai/tools/view.mjs my-video.html (or: npx pixi-effects-view my-video.html)
5
+ * node ai/tools/view.mjs my-video.html (or: npx pixi-effects view my-video.html)
6
6
  *
7
7
  * It serves the page's folder on a private port and opens your browser on a viewer: the page itself on top (its own player
8
8
  * works as usual) and, under it, the timeline of every layer (`movie.timelineSvg()`) with a playhead that follows the movie.
@@ -10,7 +10,7 @@
10
10
  * ← / → step a frame (Shift: a second), Home / End jump to the ends. Stop it with Ctrl-C.
11
11
  *
12
12
  * Needs: Node >= 18. A browser (the default one is opened; --no-open prints the address instead). The page must follow
13
- * ai/template.html (`window.movie`, `window.__ready = true`) and use a pixi-effects that has `movie.timelineSvg()` (0.7+).
13
+ * skills/pixi-effects/template.html (`window.movie`, `window.__ready = true`) and use a pixi-effects that has `movie.timelineSvg()` (0.7+).
14
14
  * Options: --port N (default: a free one) · --no-open · --query "a=1" (added to the page URL) · --root DIR (default: the nearest folder above the page with dist/)
15
15
  */
16
16
  import http from 'node:http';
@@ -252,7 +252,7 @@ export async function startViewer(opts) {
252
252
  return { server, url: `http://127.0.0.1:${port}${VIEWER}` };
253
253
  }
254
254
 
255
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
255
+ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) { // the real path: npm starts a bin through a symlink
256
256
  try {
257
257
  const opts = parseViewArgs(process.argv.slice(2));
258
258
  if (!opts.page) {
@@ -1,5 +1,5 @@
1
1
  import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
2
- import { S as SequenceSpec, C as CompositionShape, a1 as AudioDescriptor } from './types-DBbZVKGS.js';
2
+ import { S as SequenceSpec, C as CompositionShape, a6 as AudioDescriptor } from './types-CoT0ypcx.js';
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, a1 as AudioDescriptor } from './types-DBbZVKGS.cjs';
2
+ import { S as SequenceSpec, C as CompositionShape, a6 as AudioDescriptor } from './types-CoT0ypcx.cjs';
3
3
  import { gsap } from 'gsap';
4
4
 
5
5
  /**
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ var chunkSY27HZC5_cjs = require('./chunk-SY27HZC5.cjs');
4
+ require('./chunk-DP37TTMO.cjs');
5
+ require('./chunk-3DLYAPK7.cjs');
6
+ require('./chunk-ECA2WNRX.cjs');
7
+ require('./chunk-TWZNOUM7.cjs');
8
+
9
+
10
+
11
+ Object.defineProperty(exports, "CompositionSequence", {
12
+ enumerable: true,
13
+ get: function () { return chunkSY27HZC5_cjs.CompositionSequence; }
14
+ });
15
+ //# sourceMappingURL=Composition-2GJSCHPP.cjs.map
16
+ //# sourceMappingURL=Composition-2GJSCHPP.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-BK5SH6J7.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-2GJSCHPP.cjs"}
@@ -0,0 +1,7 @@
1
+ export { CompositionSequence } from './chunk-XS6GSJCN.js';
2
+ import './chunk-PRZ4U6JN.js';
3
+ import './chunk-AXZUKNXF.js';
4
+ import './chunk-TLYTYBWX.js';
5
+ import './chunk-BXOCEZ4U.js';
6
+ //# sourceMappingURL=Composition-C4FSWJZF.js.map
7
+ //# sourceMappingURL=Composition-C4FSWJZF.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-Q5TVG3HM.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-C4FSWJZF.js"}
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkUZTKEYFP_cjs = require('./chunk-UZTKEYFP.cjs');
3
+ var chunkTWZNOUM7_cjs = require('./chunk-TWZNOUM7.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
- chunkUZTKEYFP_cjs.warnUnknownOptions("Controller theme", theme, Object.keys(THEME_PROPERTIES));
459
+ chunkTWZNOUM7_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-DMxOoTdW.cjs';
1
+ import { h as Movie } from './Movie-DXReF4-W.cjs';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
- import './types-DBbZVKGS.cjs';
4
+ import './types-CoT0ypcx.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--_TJvdJa.js';
1
+ import { h as Movie } from './Movie-DP5p1r4E.js';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
- import './types-DBbZVKGS.js';
4
+ import './types-CoT0ypcx.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-NHICCDTC.js';
1
+ import { warnUnknownOptions } from './chunk-BXOCEZ4U.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 { V as Stop, e as AssetSpec, c as CompositionSpec } from './types-DBbZVKGS.js';
3
+ import { _ as Stop, e as AssetSpec, c as CompositionSpec } from './types-CoT0ypcx.js';
4
4
 
5
5
  interface AudioInspectOptions {
6
6
  /** Width of each loudness window in seconds. Default 0.1. */
@@ -144,6 +144,8 @@ interface LayerInfo {
144
144
  onCanvas: 'full' | 'partial' | 'none' | null;
145
145
  /** Depth of field (threeD layers only): the blur radius in canvas pixels this frame, from the layer's depth and the camera's focus; 0 = sharp. */
146
146
  depthBlur?: number;
147
+ /** Light (threeD layers that receive lights): how lit the layer is this frame, 0..1+ (the brightness of the light falling on its middle; 1 = as with no light, 0 = black). Left out when there is no light. */
148
+ light?: number;
147
149
  }
148
150
  interface InspectReport {
149
151
  frame: number;
@@ -443,6 +445,8 @@ declare class Movie {
443
445
  off(event: string, fn: Listener): this;
444
446
  emit(event: string, ...args: unknown[]): void;
445
447
  get isReady(): boolean;
448
+ /** True while the movie has sound but the browser keeps it silent until the viewer taps (iOS Safari, a page nobody has touched): a player can show "tap for sound"; the sound starts at a tap or key press that arrives while play() is pending, or at the next play() made from a tap. */
449
+ get audioBlocked(): boolean;
446
450
  /** The poster time as a frame number, or null. */
447
451
  get posterFrame(): number | null;
448
452
  /** Draw the poster frame on the canvas and leave the playhead where it is (frame 0): the picture before the movie plays. */
@@ -1,6 +1,6 @@
1
1
  import { Application } from 'pixi.js';
2
2
  import { gsap } from 'gsap';
3
- import { V as Stop, e as AssetSpec, c as CompositionSpec } from './types-DBbZVKGS.cjs';
3
+ import { _ as Stop, e as AssetSpec, c as CompositionSpec } from './types-CoT0ypcx.cjs';
4
4
 
5
5
  interface AudioInspectOptions {
6
6
  /** Width of each loudness window in seconds. Default 0.1. */
@@ -144,6 +144,8 @@ interface LayerInfo {
144
144
  onCanvas: 'full' | 'partial' | 'none' | null;
145
145
  /** Depth of field (threeD layers only): the blur radius in canvas pixels this frame, from the layer's depth and the camera's focus; 0 = sharp. */
146
146
  depthBlur?: number;
147
+ /** Light (threeD layers that receive lights): how lit the layer is this frame, 0..1+ (the brightness of the light falling on its middle; 1 = as with no light, 0 = black). Left out when there is no light. */
148
+ light?: number;
147
149
  }
148
150
  interface InspectReport {
149
151
  frame: number;
@@ -443,6 +445,8 @@ declare class Movie {
443
445
  off(event: string, fn: Listener): this;
444
446
  emit(event: string, ...args: unknown[]): void;
445
447
  get isReady(): boolean;
448
+ /** True while the movie has sound but the browser keeps it silent until the viewer taps (iOS Safari, a page nobody has touched): a player can show "tap for sound"; the sound starts at a tap or key press that arrives while play() is pending, or at the next play() made from a tap. */
449
+ get audioBlocked(): boolean;
446
450
  /** The poster time as a frame number, or null. */
447
451
  get posterFrame(): number | null;
448
452
  /** Draw the poster frame on the canvas and leave the playhead where it is (frame 0): the picture before the movie plays. */
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkUZTKEYFP_cjs = require('./chunk-UZTKEYFP.cjs');
3
+ var chunkTWZNOUM7_cjs = require('./chunk-TWZNOUM7.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
- chunkUZTKEYFP_cjs.warnUnknownOptions("Presenter", options, OPTION_KEYS);
150
+ chunkTWZNOUM7_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-DMxOoTdW.cjs';
1
+ import { h as Movie } from './Movie-DXReF4-W.cjs';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
- import './types-DBbZVKGS.cjs';
4
+ import './types-CoT0ypcx.cjs';
5
5
 
6
6
  interface PresenterOptions {
7
7
  canvas: HTMLCanvasElement;
@@ -1,7 +1,7 @@
1
- import { h as Movie } from './Movie--_TJvdJa.js';
1
+ import { h as Movie } from './Movie-DP5p1r4E.js';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
- import './types-DBbZVKGS.js';
4
+ import './types-CoT0ypcx.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-NHICCDTC.js';
1
+ import { warnUnknownOptions } from './chunk-BXOCEZ4U.js';
2
2
 
3
3
  // src/Presenter.ts
4
4
  var OPTION_KEYS = ["canvas", "keyboard", "clickToAdvance", "swipe", "indicator", "autoAdvance", "loop", "accent", "onExit"];
@@ -1,6 +1,6 @@
1
- import { setBlendFilterFactory } from './chunk-YEPDQ7I3.js';
2
- import './chunk-E7RONUZ7.js';
3
- import './chunk-NHICCDTC.js';
1
+ import { setBlendFilterFactory } from './chunk-AXZUKNXF.js';
2
+ import './chunk-TLYTYBWX.js';
3
+ import './chunk-BXOCEZ4U.js';
4
4
  import { BlendModeFilter, ExtensionType, extensions } from 'pixi.js';
5
5
 
6
6
  var SEPARABLE = {
@@ -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-HCP5XTNP.js.map
90
- //# sourceMappingURL=blendModes-HCP5XTNP.js.map
89
+ //# sourceMappingURL=blendModes-G3NOAU3T.js.map
90
+ //# sourceMappingURL=blendModes-G3NOAU3T.js.map