pixi-effects 0.50.0 → 0.51.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 (84) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +7 -7
  3. package/ai/CHAT.md +1 -1
  4. package/ai/chat-template.html +11 -5
  5. package/ai/tools/check.mjs +10 -4
  6. package/dist/3d.cjs +38 -12
  7. package/dist/3d.cjs.map +1 -1
  8. package/dist/3d.d.cts +12 -1
  9. package/dist/3d.d.ts +12 -1
  10. package/dist/3d.js +29 -3
  11. package/dist/3d.js.map +1 -1
  12. package/dist/Composition-WNQOIRX7.js +8 -0
  13. package/dist/{Composition-GVWWLFDL.js.map → Composition-WNQOIRX7.js.map} +1 -1
  14. package/dist/Composition-Z2EGCGXV.cjs +17 -0
  15. package/dist/{Composition-47U5U637.cjs.map → Composition-Z2EGCGXV.cjs.map} +1 -1
  16. package/dist/Controller.d.cts +2 -2
  17. package/dist/Controller.d.ts +2 -2
  18. package/dist/{Movie-Y7iHg2V8.d.ts → Movie-CbGxOdtH.d.ts} +10 -1
  19. package/dist/{Movie-DogzDBuW.d.cts → Movie-uuiyZPyi.d.cts} +10 -1
  20. package/dist/{Spans-3SEZSTQV.js → Spans-R4CWLDIM.js} +7 -6
  21. package/dist/Spans-R4CWLDIM.js.map +1 -0
  22. package/dist/{Spans-R426NVY3.cjs → Spans-ZWXK3MM5.cjs} +12 -11
  23. package/dist/Spans-ZWXK3MM5.cjs.map +1 -0
  24. package/dist/{blendModes-2IDILRN6.cjs → blendModes-4LPMH4AB.cjs} +5 -5
  25. package/dist/{blendModes-2IDILRN6.cjs.map → blendModes-4LPMH4AB.cjs.map} +1 -1
  26. package/dist/{blendModes-4QH44GOL.js → blendModes-QTMINP4W.js} +4 -4
  27. package/dist/{blendModes-4QH44GOL.js.map → blendModes-QTMINP4W.js.map} +1 -1
  28. package/dist/{chunk-UTUUOQ47.js → chunk-4V65M7NW.js} +116 -66
  29. package/dist/chunk-4V65M7NW.js.map +1 -0
  30. package/dist/{chunk-VVUSZVP7.cjs → chunk-6KVW442J.cjs} +129 -29
  31. package/dist/chunk-6KVW442J.cjs.map +1 -0
  32. package/dist/{chunk-J4ZP76IL.js → chunk-BFFIXDTK.js} +3 -3
  33. package/dist/chunk-BFFIXDTK.js.map +1 -0
  34. package/dist/{chunk-GXGIISFS.cjs → chunk-HMI6W5V3.cjs} +5 -5
  35. package/dist/{chunk-GXGIISFS.cjs.map → chunk-HMI6W5V3.cjs.map} +1 -1
  36. package/dist/{chunk-SSVHAF3Q.js → chunk-JWIST65R.js} +106 -7
  37. package/dist/chunk-JWIST65R.js.map +1 -0
  38. package/dist/{chunk-O2MD3L4J.cjs → chunk-LEYRK5OT.cjs} +132 -92
  39. package/dist/chunk-LEYRK5OT.cjs.map +1 -0
  40. package/dist/{chunk-C2HL5236.js → chunk-MEACOV6Q.js} +4 -4
  41. package/dist/{chunk-C2HL5236.js.map → chunk-MEACOV6Q.js.map} +1 -1
  42. package/dist/{chunk-3FCDRLW3.cjs → chunk-PBGZ47ZS.cjs} +3 -3
  43. package/dist/chunk-PBGZ47ZS.cjs.map +1 -0
  44. package/dist/{chunk-TXNQLMRT.js → chunk-T557JZPF.js} +45 -5
  45. package/dist/chunk-T557JZPF.js.map +1 -0
  46. package/dist/{chunk-YOKRPAKA.cjs → chunk-WKPPMOFU.cjs} +158 -108
  47. package/dist/chunk-WKPPMOFU.cjs.map +1 -0
  48. package/dist/index.cjs +39 -39
  49. package/dist/index.d.cts +3 -3
  50. package/dist/index.d.ts +3 -3
  51. package/dist/index.js +5 -5
  52. package/dist/loader.css +1 -1
  53. package/dist/present.cjs +5 -5
  54. package/dist/present.d.cts +2 -2
  55. package/dist/present.d.ts +2 -2
  56. package/dist/present.js +5 -5
  57. package/dist/three.cjs +41 -8
  58. package/dist/three.cjs.map +1 -1
  59. package/dist/three.d.cts +14 -1
  60. package/dist/three.d.ts +14 -1
  61. package/dist/three.js +36 -3
  62. package/dist/three.js.map +1 -1
  63. package/dist/{types-BZngm4nT.d.cts → types-CpCqYSZ0.d.cts} +8 -0
  64. package/dist/{types-BZngm4nT.d.ts → types-CpCqYSZ0.d.ts} +8 -0
  65. package/docs/api.md +19 -0
  66. package/docs/dsl.md +21 -3
  67. package/llms-full.txt +50 -6
  68. package/package.json +1 -1
  69. package/skills/pixi-effects/SKILL.md +4 -3
  70. package/skills/pixi-effects/reference/cheatsheet.md +1 -0
  71. package/skills/pixi-effects/reference/pitfalls.md +5 -0
  72. package/skills/pixi-effects/template.html +14 -8
  73. package/dist/Composition-47U5U637.cjs +0 -16
  74. package/dist/Composition-GVWWLFDL.js +0 -7
  75. package/dist/Spans-3SEZSTQV.js.map +0 -1
  76. package/dist/Spans-R426NVY3.cjs.map +0 -1
  77. package/dist/chunk-3FCDRLW3.cjs.map +0 -1
  78. package/dist/chunk-J4ZP76IL.js.map +0 -1
  79. package/dist/chunk-O2MD3L4J.cjs.map +0 -1
  80. package/dist/chunk-SSVHAF3Q.js.map +0 -1
  81. package/dist/chunk-TXNQLMRT.js.map +0 -1
  82. package/dist/chunk-UTUUOQ47.js.map +0 -1
  83. package/dist/chunk-VVUSZVP7.cjs.map +0 -1
  84. package/dist/chunk-YOKRPAKA.cjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.51.0
4
+
5
+ Nothing breaks: a 2D layer can stay next to an object of a scene.
6
+
7
+ - **`follow: 'planets/Mars'`: a label next to an object, every frame.** Any 2D layer (text, shape, image, composition, null) is carried to where an object of a `scene3d`, `three` or `scene2d` layer is on screen (a name its setup returned in `objects`, or a node's label / `name`), or another layer's anchor point (`follow: 'title'`, a path through nested compositions, or a unique name found anywhere); its own `x` / `y` become an offset from there. It is placed after every layer is set for the frame (the scene layers' `update(t)` and `followCamera`), so it sits where the object is drawn in playback, a seek and an export, and it is hidden while the object is behind the camera or the followed layer is not on screen. An unknown layer or object is warned with the names there are (did you mean); a `threeD` follower, a target inside a `threeD` card and an offset larger than the composition are said in a sentence. In a trial (2026-11-14) both agents that labelled orbiting planets wrote the camera projection by hand and copied the camera's ease. Film `follow`.
8
+ - **`movie.project(target)`** → `{ x, y } | null`: the same point for your own code, in canvas pixels (the space `movie.pick` takes).
9
+ - **`check --do "click planets/Mars"`** clicks an object of a scene by name, where `movie.project` says it is (it knew only layer names).
10
+ - **`release:check` runs the files that failed in the full run once more, alone** (not when more than 8 failed): three full runs in a row had each timed out a different real-browser test under the load of the run itself; it names the files it ran again.
11
+
12
+ ## 0.50.1
13
+
14
+ Fixes from a trial of 8 fresh agents (Sonnet, Opus) making videos with `scene2d` / `scene3d` on 0.50.0; nothing breaks.
15
+
16
+ - **Fixed: an `.ogg` sound played nothing, with no warning.** PixiJS's own video loader took `.ogg` as a video, so an audio layer on it was silent and `check` said nothing; `.m4a`, `.flac` and `.opus` had no loader at all and stopped `init` with "Cannot read properties of null". An audio asset may now be `.mp3`, `.wav`, `.ogg`, `.oga`, `.m4a`, `.aac`, `.flac` or `.opus` (a query after the name is fine), and a sound asset that did not load, or a video with no sound track used as one, stops `init` with the layer, the asset, its file and the formats. Test film `audio-formats`.
17
+ - **PixiJS's official skills are named where an agent writes PixiJS code.** `scene2d` and `scene3d` hold plain PixiJS / PixiJS 3D code, and PixiJS publishes official skills for both (`npx skills add pixijs/pixijs-skills`, MIT); SKILL.md, AGENTS.md, the README, the For AI page and the `scene2d` / `scene3d` sections say so, with the copy inside the npm package of the version the template loads (`pixi.js@8.23.0/skills/pixijs`, `@pixi/3d@0.1.0/skills/pixijs-3d`), readable with no install. The `scene3d` section lists the @pixi/3d geometries with their options and the materials (`emissive: { color, strength }`), and says that @pixi/3d ignores an option it does not know (3 of 4 agents grepped the package's types for them; one passed `tubeRadius` to `TorusGeometry` and got a fat ring).
18
+ - **A PixiJS deprecation keeps its message in `check` and the chat's red box.** PixiJS prints it as a collapsed console group titled with the message and only the stack inside, so the collected warning was a bare stack; both templates now put an open group's title before the warnings inside it.
19
+ - **The sound-effect knobs are shown in place.** 4 of 8 agents wrote `pitch` beside `sfx: 'chime'` (the warning caught it); SKILL.md and CHAT.md now show `sfx: { preset: 'chime', pitch: 7 }` with the units.
20
+ - **`three`: `ctx.objects`** in `update`, what setup returned plus `camera`, as the `scene2d` and `scene3d` layers already have.
21
+ - **`random(seed)` and `rand(seed)` have their own section** in the API reference, so `help random` answers with them (it printed the canvas layer).
22
+
3
23
  ## 0.50.0
4
24
 
5
25
  Nothing breaks: `scene2d`, a layer of PixiJS 2D objects built by your code (core, nothing to import), and two fixes for every engine: a rebuild that keeps a scene starts its keyframes from setup's values, and a routed `tint` keyframe moves by channel.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pixi-effects
2
2
 
3
- > **Status**: experimental — current release `0.50.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.51.0`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
4
4
 
5
5
  **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 43 pieces written as plain data by AI models · **[For AI →](https://yjmtmtk.github.io/pixi-effects/for-ai.html)** · what an AI reads and runs · **[Playground →](https://yjmtmtk.github.io/pixi-effects/examples/playground.html)** in the browser.
6
6
 
@@ -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. **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.
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`. For the PixiJS code you may write inside a `scene2d` or `scene3d` layer, PixiJS's own official skills help too: `npx skills add pixijs/pixijs-skills` ([pixijs/pixijs-skills](https://github.com/pixijs/pixijs-skills)). **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/), with a small timeline of its own and strict TypeScript types. The ease names and the way `to` / `from` / `set` read are [GSAP](https://gsap.com/)'s, with thanks. Render to MP4 / WebM / MOV via [mediabunny](https://mediabunny.dev/).
16
16
 
@@ -52,8 +52,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
52
52
  "imports": {
53
53
  "pixi.js": "https://esm.sh/pixi.js@8.23.0?bundle-deps",
54
54
  "mediabunny": "https://esm.sh/mediabunny",
55
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.50.0/dist/index.js",
56
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.50.0/dist/Controller.js"
55
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.51.0/dist/index.js",
56
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.51.0/dist/Controller.js"
57
57
  }
58
58
  }
59
59
  </script>
@@ -66,7 +66,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
66
66
 
67
67
  The `mediabunny` line is needed only to export (the download button, `movie.render()`) or for a `video` layer; a page that only plays can leave it out and never fetches it (or call `useMediabunny(url)` instead, see [the API](docs/api.md#where-the-encoder-comes-from-usemediabunnysource)).
68
68
 
69
- Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.50.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.
69
+ Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.51.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.
70
70
 
71
71
  > **A 3D scene?** Add three more entries (`pixi-effects/3d`, `@pixi/3d`, `@pixi/3d/extras`) — see [Adding a 3D scene](#adding-a-3d-scene-optional) below. **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
72
72
  >
@@ -81,7 +81,7 @@ Add three entries to the importmap above (PixiJS 3D needs `pixi.js` 8.23.0 or la
81
81
  {
82
82
  "imports": {
83
83
  "...": "(everything from the importmap above)",
84
- "pixi-effects/3d": "https://cdn.jsdelivr.net/npm/pixi-effects@0.50.0/dist/3d.js",
84
+ "pixi-effects/3d": "https://cdn.jsdelivr.net/npm/pixi-effects@0.51.0/dist/3d.js",
85
85
  "@pixi/3d": "https://cdn.jsdelivr.net/npm/@pixi/3d@0.1.0/dist/pixijs-3d.min.mjs",
86
86
  "@pixi/3d/extras": "https://cdn.jsdelivr.net/npm/@pixi/3d@0.1.0/dist/pixijs-3d-extras.min.mjs"
87
87
  }
@@ -125,7 +125,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
125
125
  "...": "(everything from the importmap above)",
126
126
  "three": "https://esm.sh/three@0.178.0",
127
127
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.178.0/examples/jsm/",
128
- "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.50.0/dist/three.js"
128
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.51.0/dist/three.js"
129
129
  }
130
130
  }
131
131
  </script>
package/ai/CHAT.md CHANGED
@@ -58,7 +58,7 @@ That is a complete, working video (the block between the two markers). Change th
58
58
  - **Shapes:** `shape: 'rect' | 'circle' | 'ellipse' | 'arc' | 'line' | 'polygon' | 'path'`. `rect`: `width height cornerRadius`; `circle`: `radius`; `line`: `from: [x, y], to: [x, y]` (canvas coordinates, no `x, y`); `path`: `d` (SVG path data). Style: `fillColor strokeColor strokeWidth fillAlpha`.
59
59
  - **Text:** `style` is a PixiJS text style (`fontSize fontFamily fontWeight fill letterSpacing align wordWrap wordWrapWidth lineHeight`). Use fonts that exist everywhere (`Georgia, serif`, `Arial, sans-serif`, `ui-monospace, monospace`); this page loads no web fonts. Estimate a text's width at about 0.6 × `fontSize` per character, keep important text 5 % away from the edges, and keep the bottom 60 px free (the player bar covers it in the preview, not in the file).
60
60
  - **Scenes.** Stack layers with `at` / `duration` over an opaque background rect. Do not remove layers.
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).
61
+ - **Sound with no files:** `{ type: 'audio', sfx: 'swoosh', at: 2 }`. Presets: `click pop swoosh swipe hit riser chime beep coin glitch typewriter`; to change one: `sfx: { preset: 'chime', pitch: 7 }` (`pitch` in semitones, `brightness` −1 … 1; inside `sfx`, never beside it). 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
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.
@@ -39,9 +39,15 @@
39
39
  document.getElementById('problems-text').textContent = '\n' + window.__logs.join('\n');
40
40
  };
41
41
  const add = (line) => { window.__logs.push(line); show(); };
42
+ // A console group's title leads the warnings inside it (PixiJS prints a deprecation as a group titled with its message, the stack inside).
43
+ let group = '';
44
+ const text = (a) => { if (typeof a[0] === 'string' && a[0].includes('%')) { let i = 1; a = [a[0].replace(/%([csdifoO])/g, (_, f) => f === 'c' ? (i++, '') : String(a[i++])), ...a.slice(i)]; } return a.map(x => (x && x.message) || String(x)).join(' '); };
45
+ const gc = console.groupCollapsed.bind(console), ge = console.groupEnd.bind(console);
46
+ console.groupCollapsed = (...a) => { group = text(a); gc(...a); };
47
+ console.groupEnd = () => { group = ''; ge(); };
42
48
  for (const k of ['warn', 'error']) {
43
49
  const orig = console[k].bind(console);
44
- console[k] = (...a) => { add(k + ': ' + a.map(x => (x && x.message) || String(x)).join(' ')); orig(...a); };
50
+ console[k] = (...a) => { add(k + ': ' + (group ? group + ' — ' : '') + text(a)); orig(...a); };
45
51
  }
46
52
  window.addEventListener('error', e => add('uncaught: ' + e.message));
47
53
  window.addEventListener('unhandledrejection', e => add('unhandled: ' + (e.reason && e.reason.message || e.reason)));
@@ -59,10 +65,10 @@
59
65
  "imports": {
60
66
  "pixi.js": "https://cdn.jsdelivr.net/npm/pixi.js@8.23.0/dist/pixi.min.mjs",
61
67
  "mediabunny": "https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm",
62
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.50.0/dist/index.js",
63
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.50.0/dist/Controller.js",
64
- "pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.50.0/dist/present.js",
65
- "pixi-effects/3d": "https://cdn.jsdelivr.net/npm/pixi-effects@0.50.0/dist/3d.js",
68
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.51.0/dist/index.js",
69
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.51.0/dist/Controller.js",
70
+ "pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.51.0/dist/present.js",
71
+ "pixi-effects/3d": "https://cdn.jsdelivr.net/npm/pixi-effects@0.51.0/dist/3d.js",
66
72
  "@pixi/3d": "https://cdn.jsdelivr.net/npm/@pixi/3d@0.1.0/dist/pixijs-3d.min.mjs",
67
73
  "@pixi/3d/extras": "https://cdn.jsdelivr.net/npm/@pixi/3d@0.1.0/dist/pixijs-3d-extras.min.mjs"
68
74
  }
@@ -19,7 +19,7 @@
19
19
  * --pick X,Y (with --at, repeatable: what is under that canvas point at each moment, the top layer first, and the object a three layer met: `movie.pick`) ·
20
20
 
21
21
  * --do "click Lime; hover 640,360; drag card > 770,240; wait 1; key Space" (real mouse and key events on the page, one step after the other, after everything else: what was under the pointer, which
22
- * variables changed, whether the page warned, one picture per step in steps/; a point is x,y in canvas pixels or a layer name (the middle of its box); repeatable) ·
22
+ * variables changed, whether the page warned, one picture per step in steps/; a point is x,y in canvas pixels, a layer name (the middle of its box) or layer/object, an object of a scene3d, three or scene2d layer (where movie.project says it is); repeatable) ·
23
23
  * --onion A:B (one picture of the movement between A and B seconds, frames overlaid: onion.png) ·
24
24
  * --seam (a movie made to loop: the last frame to the first is compared with the steps between ordinary neighbouring frames; a jump is a problem) ·
25
25
  * --strict (text overlaps and stops where the picture is still changing fail the check; by default they are only listed for review) · --out DIR · --frames N (contact sheet tiles, default 12) · --formats mp4,webm (default mp4) · --no-export ·
@@ -104,7 +104,7 @@ const CRITICAL_TYPES = new Set(['Script', 'Document', 'Stylesheet', 'Other', und
104
104
  const DO_VERBS = ['click', 'hover', 'drag', 'wait', 'key'];
105
105
  const pointOf = (text) => { const m = text.trim().match(/^(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)$/); return m ? { x: Number(m[1]), y: Number(m[2]) } : { name: text.trim() }; };
106
106
  /**
107
- * `--do` steps: "click 640,360; hover Lime; drag card > 770,240; wait 0.5; key Space" (`;` or a new line between steps). A point is `x,y` in canvas pixels or a layer name; the points of a
107
+ * `--do` steps: "click 640,360; hover Lime; drag card > 770,240; wait 0.5; key Space" (`;` or a new line between steps). A point is `x,y` in canvas pixels, a layer name or `layer/object` (an object of a scene layer); the points of a
108
108
  * drag are separated by `>` or by spaces (a name with spaces needs `>`). Returns [{ verb, points?, seconds?, key? }].
109
109
  */
110
110
  export function parseDo(text) {
@@ -867,8 +867,14 @@ export async function runSteps(cdp, steps, outDir, size, consoleLines, seenLogs)
867
867
  const points = [];
868
868
  for (const pt of step.points) {
869
869
  if (pt.name === undefined) { points.push({ ...pt }); continue; }
870
- const box = await cdp.eval(`(async () => { const r = (await movie.inspect(movie.currentFrame)).layers.find(l => l.name === ${JSON.stringify(pt.name)} || l.path === ${JSON.stringify(pt.name)}); return r && r.bounds ? { x: r.bounds.x + r.bounds.width / 2, y: r.bounds.y + r.bounds.height / 2 } : null; })()`);
871
- if (!box) throw new Error(`no layer named "${pt.name}" at this moment (it may not be alive yet: put a wait before, or give x,y)`);
870
+ let box = await cdp.eval(`(async () => { const r = (await movie.inspect(movie.currentFrame)).layers.find(l => l.name === ${JSON.stringify(pt.name)} || l.path === ${JSON.stringify(pt.name)}); return r && r.bounds ? { x: r.bounds.x + r.bounds.width / 2, y: r.bounds.y + r.bounds.height / 2 } : null; })()`);
871
+ if (!box) {
872
+ // not a layer: an object of a scene ("world/mover"), where movie.project says it is
873
+ const p = await cdp.eval(`(() => { try { const p = movie.project(${JSON.stringify(pt.name)}); return p ? { x: p.x, y: p.y } : { hidden: true }; } catch (e) { return { error: String(e && e.message || e).replace(/^pixi-effects: /, '') }; } })()`);
874
+ if (p && Number.isFinite(p.x)) box = p;
875
+ else if (p?.hidden) throw new Error(`"${pt.name}" is not on screen at this moment (behind the camera, or its layer is not alive: put a wait before, or give x,y)`);
876
+ else throw new Error(pt.name.includes('/') && p?.error ? p.error : `no layer named "${pt.name}" at this moment (it may not be alive yet: put a wait before, or give x,y)`);
877
+ }
872
878
  points.push({ x: Math.round(box.x), y: Math.round(box.y), name: pt.name });
873
879
  }
874
880
  res.label = `${step.verb} ${points.map(pt => pt.name ? `${pt.name} (${pt.x}, ${pt.y})` : `${pt.x},${pt.y}`).join(' > ')}`;
package/dist/3d.cjs CHANGED
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var chunkVVUSZVP7_cjs = require('./chunk-VVUSZVP7.cjs');
4
- var chunkGXGIISFS_cjs = require('./chunk-GXGIISFS.cjs');
5
- var chunk3FCDRLW3_cjs = require('./chunk-3FCDRLW3.cjs');
3
+ var chunk6KVW442J_cjs = require('./chunk-6KVW442J.cjs');
4
+ require('./chunk-VQXZ2NOI.cjs');
5
+ var chunkHMI6W5V3_cjs = require('./chunk-HMI6W5V3.cjs');
6
+ var chunkPBGZ47ZS_cjs = require('./chunk-PBGZ47ZS.cjs');
6
7
  var chunkDOLTCXCL_cjs = require('./chunk-DOLTCXCL.cjs');
7
8
  var _3d = require('@pixi/3d');
8
9
  var pixi_js = require('pixi.js');
@@ -247,7 +248,7 @@ async function layerTexture(ctx, name, options = {}) {
247
248
  var OWN_KEYS = ["width", "height", "view", "resolution", "setup", "update", "dispose", "followCamera", "followLights", "followFog", "reuse"];
248
249
  var THREE_ONLY = ["renderer", "shareGpu", "antialias"];
249
250
  var LAYER_OWNED = ["width", "height", "autoResize"];
250
- var Scene3DSequence = class extends chunkVVUSZVP7_cjs.Sequence {
251
+ var Scene3DSequence = class extends chunk6KVW442J_cjs.Sequence {
251
252
  _view = null;
252
253
  _ctx = null;
253
254
  _objects = {};
@@ -272,7 +273,7 @@ var Scene3DSequence = class extends chunkVVUSZVP7_cjs.Sequence {
272
273
  /** True once another layer took this layer's scene: destroying this one must not let it go. */
273
274
  _donated = false;
274
275
  /** What setup left on the objects the keyframes reach, shared with a layer that takes the scene over (core/setupValues.ts). */
275
- _setupValues = new chunkVVUSZVP7_cjs.SetupValues();
276
+ _setupValues = new chunk6KVW442J_cjs.SetupValues();
276
277
  _pendingRebinds = [];
277
278
  _lightMirror = null;
278
279
  _fogMirror = null;
@@ -303,14 +304,14 @@ var Scene3DSequence = class extends chunkVVUSZVP7_cjs.Sequence {
303
304
  if (this.duration === void 0) this.duration = this.parent?.duration ?? this.root.duration;
304
305
  this._checkKeys();
305
306
  this._checkTint();
306
- if (typeof spec.update === "function" && chunkVVUSZVP7_cjs.NOT_PURE.test(spec.update.toString())) {
307
+ if (typeof spec.update === "function" && chunk6KVW442J_cjs.NOT_PURE.test(spec.update.toString())) {
307
308
  this._say("pure", "update calls Math.random, Date.now, performance.now or new Date, so it is not deterministic: the same frame would draw differently in playback, in a seek and in an export. Derive the value from t");
308
309
  }
309
310
  const renderer = this.root.pixiRenderer;
310
311
  if (renderer && !renderer.renderPipes.view3d) {
311
312
  throw new Error(`${this._who()}: PixiJS 3D was loaded after the renderer started; import 'pixi-effects/3d' (and call register3D()) before movie.init`);
312
313
  }
313
- const scope = chunkVVUSZVP7_cjs.buildScope(this, this.parent, this.root);
314
+ const scope = chunk6KVW442J_cjs.buildScope(this, this.parent, this.root);
314
315
  const width = resolveDim(spec.width, scope) ?? this.parent?.width ?? this.root.width;
315
316
  const height = resolveDim(spec.height, scope) ?? this.parent?.height ?? this.root.height;
316
317
  const viewOptions = { ...spec.view ?? {} };
@@ -419,7 +420,7 @@ var Scene3DSequence = class extends chunkVVUSZVP7_cjs.Sequence {
419
420
  }
420
421
  /** Unknown keys (with the likely one), and the keys only a three layer has. */
421
422
  _checkKeys() {
422
- const valid = [...chunk3FCDRLW3_cjs.COMMON, ...OWN_KEYS];
423
+ const valid = [...chunkPBGZ47ZS_cjs.COMMON, ...OWN_KEYS];
423
424
  for (const key of Object.keys(this.spec)) {
424
425
  if (valid.includes(key)) continue;
425
426
  if (THREE_ONLY.includes(key)) {
@@ -500,8 +501,8 @@ var Scene3DSequence = class extends chunkVVUSZVP7_cjs.Sequence {
500
501
  const current = view.filters ?? [];
501
502
  const others = current.filter((f) => f !== this._look && f !== this._blend);
502
503
  const authored = String(this.spec.blendMode ?? "normal");
503
- if (others.length === 0 && chunkGXGIISFS_cjs.isAdvancedBlend(authored) && !this._blend) {
504
- this._blend = chunkGXGIISFS_cjs.blendFilterFor(authored);
504
+ if (others.length === 0 && chunkHMI6W5V3_cjs.isAdvancedBlend(authored) && !this._blend) {
505
+ this._blend = chunkHMI6W5V3_cjs.blendFilterFor(authored);
505
506
  if (this._blend) view.blendMode = "normal";
506
507
  }
507
508
  const mode = String(view.blendMode ?? "normal");
@@ -738,6 +739,31 @@ var Scene3DSequence = class extends chunkVVUSZVP7_cjs.Sequence {
738
739
  material: hit.target.material
739
740
  };
740
741
  }
742
+ /**
743
+ * `follow` / `movie.project`: where an object of the scene is on the canvas (a name setup returned in `objects`, or the label of any node), from the view's own projection
744
+ * of the node's origin; null while it is behind the camera, undefined for no such object.
745
+ */
746
+ screenPoint(object) {
747
+ const node = this._nodeNamed(object);
748
+ if (!node) return void 0;
749
+ const p = this._view?.worldToScreen(node.getGlobalPosition(new _3d.Vector3()));
750
+ return p ? { x: p.x, y: p.y } : null;
751
+ }
752
+ /** The names `screenPoint` knows: the objects setup returned, then the labelled nodes of the scene. */
753
+ objectNames() {
754
+ const names = new Set(Object.keys(this._objects).filter((k) => this._nodeNamed(k)));
755
+ const visit = (n) => {
756
+ if (n.label && n.label !== n.constructor?.name) names.add(n.label);
757
+ for (const c of n.children ?? []) visit(c);
758
+ };
759
+ if (this._view) for (const c of this._view.root.children) visit(c);
760
+ return [...names];
761
+ }
762
+ _nodeNamed(name) {
763
+ const own = this._objects[name];
764
+ if (own && typeof own.getGlobalPosition === "function") return own;
765
+ return this._view?.root.getChildByLabel(name, true) ?? null;
766
+ }
741
767
  /**
742
768
  * `hit.scene3d.grab(plane)`: a function of a point (u, v) of the layer's picture that gives where the pointer's ray meets a plane through the grabbed point, and how far that is
743
769
  * from it (`delta`): move the object by it. `plane`: 'screen' (facing the camera: the default), 'floor' (horizontal: y up), 'wall' (z constant), or a normal [x, y, z].
@@ -865,7 +891,7 @@ function reuseKeyOf(spec, width, height) {
865
891
  function resolveDim(v, scope) {
866
892
  if (v === void 0) return void 0;
867
893
  if (typeof v === "number") return v;
868
- return chunkVVUSZVP7_cjs.evaluateExpr(v, scope);
894
+ return chunk6KVW442J_cjs.evaluateExpr(v, scope);
869
895
  }
870
896
  var label2 = "pixi-effects: loadModel";
871
897
  async function loadModel(url, options = {}) {
@@ -951,7 +977,7 @@ function register3D() {
951
977
  }
952
978
  _3d.Model3D.defaultOptions.autoUpdate = false;
953
979
  _3d.AnimationPlayer.defaultOptions.autoUpdate = false;
954
- chunkVVUSZVP7_cjs.registerSequenceType("scene3d", Scene3DSequence);
980
+ chunk6KVW442J_cjs.registerSequenceType("scene3d", Scene3DSequence);
955
981
  }
956
982
  function scene3d(spec) {
957
983
  return spec;