pixi-effects 0.39.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +16 -30
- package/ai/chat-template.html +3 -3
- package/ai/tools/check.mjs +26 -7
- package/ai/tools/gltfProbe.mjs +122 -0
- package/ai/tools/media.mjs +8 -1
- package/ai/tools/pixi-effects.mjs +1 -1
- package/dist/{Base-BKKwxavW.d.cts → Base-DQWIJ6bJ.d.cts} +1 -1
- package/dist/{Base-ioh_AjLB.d.ts → Base-Jgkud1Aw.d.ts} +1 -1
- package/dist/Composition-N6BOROJ2.cjs +16 -0
- package/dist/{Composition-3MNLNT2E.cjs.map → Composition-N6BOROJ2.cjs.map} +1 -1
- package/dist/Composition-YGBQX3HZ.js +7 -0
- package/dist/{Composition-P37AD4GN.js.map → Composition-YGBQX3HZ.js.map} +1 -1
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/{Movie-BtwFI2ne.d.ts → Movie-CwqRLOzE.d.ts} +53 -2
- package/dist/{Movie-CSWUTnMd.d.cts → Movie-DWtxRiJ2.d.cts} +53 -2
- package/dist/{Spans-PLBQOSMV.js → Spans-6L2ZVECX.js} +4 -4
- package/dist/{Spans-PLBQOSMV.js.map → Spans-6L2ZVECX.js.map} +1 -1
- package/dist/{Spans-F6FBBCD3.cjs → Spans-XTIWYSNO.cjs} +9 -9
- package/dist/{Spans-F6FBBCD3.cjs.map → Spans-XTIWYSNO.cjs.map} +1 -1
- package/dist/{chunk-SJFDSHBP.cjs → chunk-3HZV3UHU.cjs} +87 -72
- package/dist/chunk-3HZV3UHU.cjs.map +1 -0
- package/dist/{chunk-JCPZJJNW.js → chunk-4QVQ5UJJ.js} +4 -4
- package/dist/{chunk-JCPZJJNW.js.map → chunk-4QVQ5UJJ.js.map} +1 -1
- package/dist/{chunk-UHCVO5TH.js → chunk-ICI6ACHY.js} +28 -13
- package/dist/chunk-ICI6ACHY.js.map +1 -0
- package/dist/{chunk-FV25Y5OG.cjs → chunk-RN5FWGGM.cjs} +144 -41
- package/dist/chunk-RN5FWGGM.cjs.map +1 -0
- package/dist/{chunk-HX73S7FD.cjs → chunk-X62H3RBG.cjs} +4 -4
- package/dist/{chunk-HX73S7FD.cjs.map → chunk-X62H3RBG.cjs.map} +1 -1
- package/dist/{chunk-F2ZHDUPL.js → chunk-Y2N6U3X5.js} +109 -6
- package/dist/chunk-Y2N6U3X5.js.map +1 -0
- package/dist/index.cjs +33 -33
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/loader.css +1 -1
- package/dist/present.cjs +3 -3
- package/dist/present.d.cts +2 -2
- package/dist/present.d.ts +2 -2
- package/dist/present.js +3 -3
- package/dist/three.cjs +551 -12
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +154 -7
- package/dist/three.d.ts +154 -7
- package/dist/three.js +547 -10
- package/dist/three.js.map +1 -1
- package/dist/{types-BXi-0iZS.d.ts → types-DIh93lFb.d.cts} +82 -1
- package/dist/{types-BXi-0iZS.d.cts → types-DIh93lFb.d.ts} +82 -1
- package/docs/api.md +37 -10
- package/docs/dsl.md +73 -5
- package/docs/media.md +1 -1
- package/llms-full.txt +209 -21
- package/llms.txt +1 -1
- package/package.json +11 -12
- package/skills/pixi-effects/SKILL.md +3 -3
- package/skills/pixi-effects/reference/cheatsheet.md +3 -2
- package/skills/pixi-effects/reference/recipes.md +92 -0
- package/skills/pixi-effects/template.html +9 -8
- package/dist/Composition-3MNLNT2E.cjs +0 -16
- package/dist/Composition-P37AD4GN.js +0 -7
- package/dist/chunk-F2ZHDUPL.js.map +0 -1
- package/dist/chunk-FV25Y5OG.cjs.map +0 -1
- package/dist/chunk-SJFDSHBP.cjs.map +0 -1
- package/dist/chunk-UHCVO5TH.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.41.0
|
|
4
|
+
|
|
5
|
+
`movie.pick` (what is under a point of the canvas) and a better fit of the light falloff in a `three` layer. Nothing breaks.
|
|
6
|
+
|
|
7
|
+
- **New: `movie.pick(x, y)`, `pickAll`, `canvasPoint(event)`: what is under a point of the canvas.** The layer on top (or every layer under the point, the top first) at the frame on screen, in canvas pixels: a flat layer by its own shape (a rotated bar by its rotated rectangle, a circle by the circle), a `threeD` card by its projected quad, a `three` layer by a ray through its camera (`hit.three.object` names the object it met, with the point and the distance; with `followCamera` it is the composition's camera). `canvasPoint(event)` turns a pointer event into canvas pixels when the canvas is shown at another size. A layer outside its life, at alpha 0, hidden by a parent or outside its mask is not hit; `{ only, ignore }` choose by name or path. No clock: it reads the frame as the last draw left it, so it is a function of the frame (a seek back gives the same answer). Not in this version: alpha-exact hits, hover and drag. `check --at 3.5 --pick 640,360` (repeatable) prints what is under that point at each moment, so an AI can answer "is the button where I think" without looking. Recipe `pick-click`; `docs/api.md`; tests `tests/core/pick.test.ts`, `tests/films/pick` (real Chrome: rotated bar, circle, a turned card, a cube, a gliding camera, a canvas shown at twice its size).
|
|
8
|
+
|
|
9
|
+
- **Changed: `followLights` fits the falloff better.** `falloff: 'smooth'` on a mirrored light is now three's cut-off distance and gain chosen from where the fall starts (before: the cut-off at `radius + falloffDistance`), which halves the largest difference to the composition's curve (0.15–0.40 before, 0.09–0.17 for a fall that is not steep; in the real picture mean 0.08–0.28 of 255, was 1.0–1.2). `'inverseSquare'` was already exact outside the radius (mean 0.02–0.03) but blows out inside it (three keeps rising where the composition holds 1; measured mean 21 of 255 with the lit objects inside the radius): the layer now says so once, with the distance and the way out (a smaller radius, or `'smooth'`). `tests/films/three-space`, `tests/three/spaceBridge.test.ts`.
|
|
10
|
+
|
|
11
|
+
## 0.40.0
|
|
12
|
+
|
|
13
|
+
The `three` layer grows up: it can live in the composition's space, it loads glTF models, `check` holds it to the library's contract, and it can use three's WebGPU renderer. Nothing else changed; one type-only break (the last entry).
|
|
14
|
+
|
|
15
|
+
- **New: a `three` layer can live in the composition's space (`followCamera`, `followLights`, `followFog`).** `followCamera: true` puts the layer's `PerspectiveCamera` where the composition's `camera` layer is, every frame (position, look-at point, vertical fov, the handheld offsets), so one `cameraPath()` or `orbit()` flies through the flat layers and the three scene together. The scene is in composition pixels with y flipped (the composition's `(x, y, z)` is `(x, -y, z)` in three); a point lands on the same pixel as the composition's own projection (to 1e-6 px; against the composition's cards in the real picture: mean 0.14–0.19 of 255, edges only). `followLights: true` makes the composition's `light` layers three lights (ambient, point, spot, parallel; position, colour, intensity, cone, falloff; the intensity times π, because three divides a diffuse surface by π), moved every frame; **a light with `castsShadows` casts in three** (the layer's own meshes choose who casts and receives: a card in front of a wall under a spot throws the same shadow as the composition's cards, mean 0.2–0.35 of 255, WebGL and WebGPU, at 480 × 270 and at 1280 × 720). With `MeshLambertMaterial` and three's colour management off the planes shade like the cards (mean 0.02–0.2 of 255); with three's defaults close (0.8–1.4). `followFog: true` makes the camera's fog the scene's `Fog`: close, not equal (three fades by `smoothstep`, the composition linearly, and has no `fogAmount` below 1, which warns once; mean 0.5–1.8 of 255 where the fog's own effect is 2–7). `ctx.space` gives the raw camera, lights and fog. A composition whose only consumer is such a layer no longer warns that its camera or light has no effect, or that no layer casts a shadow. Pinned by `tests/films/three-space` against the composition's own cards. See `docs/dsl.md` → three.
|
|
16
|
+
- **New: glTF models in the free `three` layer: `threeGltf`, `threeFrameCamera`, `pixi-effects probe model.glb`.** `threeGltf(url, { animation, speed, offset, loop })` returns `{ object, clips, size, pose(t), fit(px), dispose() }`: the group's origin is the centre of the model, `pose(t)` is `AnimationMixer.setTime` (a function of the time: a seek, an export and a repeat give the same frame), `fit(px)` scales the longest side to pixels, and skinned meshes are drawn always (three culls them by a bounding sphere that does not follow them: a moving, shadow-casting Fox drew its shadow depending on the frames drawn before it). `threeFrameCamera(camera, object, { direction, margin })` puts a camera where the whole model fits (a three-quarter view, from the corners of the bounding box rather than a sphere). `probe model.glb` (or `.gltf`) reads in Node, with no browser and no three.js, the clips with their lengths, the size (equal to what `threeGltf(...).size` measures; Khronos Fox: 25.185 × 79.029 × 154.72) and notes (Draco / KTX2 / Meshopt need a decoder that `threeGltf` does not set up; no animation; files to keep beside the model). The template's import map has `three/addons/`. Recipes `gltf-in-space` and `gltf-shadow-room` (a model running through a sweeping spotlight with its shadow on a wall and a lit flat caption), written and corrected after three fresh-AI trials on the real Khronos Fox. Tests: `tests/three/gltf.test.ts`, `tests/films/gltf-model`, `tests/tools/probeGltf.test.ts`.
|
|
17
|
+
- **New: `check` holds a `three` layer to "a function of the time", as it does a `canvas` layer.** It draws the scene at four moments in two orders (one draw thrown away first), reads the picture from the renderer's canvas and compares: a clock, `Math.random`, a mixer advanced with `mixer.update(dt)` or a speed that builds up from call to call is reported with the moments and the fix (`mixer.setTime(t)` / `threeGltf(url).pose(t)`); `Math.random` / `Date.now` / `performance.now` / `new Date` in `update` also warns at build. `check` prints a `three` line when every layer passed (WebGL and WebGPU), the canvas heading says "layer(s) (canvas / three)", and `movie.canvasReport()` rows have a `kind`. `tests/tools/threeDeterminism.test.ts`.
|
|
18
|
+
- **New: a page with too many `three` layers says so instead of going blank.** A browser keeps about 16 live WebGL contexts for a page (about 32 WebGPU: measured on Chrome); layers beyond that draw nothing and raise no error. The 13th live WebGL layer (the 29th WebGPU one) now warns once with the count and the way out, and a layer whose WebGL context the browser took away names itself. `docs/dsl.md` had said "roughly 8–16". `tests/films/three-budget` (20 layers in real Chrome).
|
|
19
|
+
- **New: a `three` layer can draw with three's WebGPU renderer (`renderer: 'webgpu'`), `antialias`, and a page-level default `registerThree({ renderer })`.** The layer used `WebGLRenderer` always, in its own offscreen context. `renderer: 'webgpu'` uses `WebGPURenderer` from `three/webgpu` (three 0.17x or later); the default stays `'webgl'` (a design review by two independent reviewers, with measurements on four large GLSL clips, rejected a silent fallback that runs `setup` twice: a second `setup` can draw a different video). The same lit scene draws within 0.06 of 255 per channel of the WebGL picture and each renderer is a function of the time (`tests/films/three-renderers`); the render alone is about the same for a box and about twice as fast on WebGPU for 2,880 instanced rods; an MP4 export is limited by the encode and took the same time. `antialias` (default `true`): WebGPURenderer blends fog and resolves multisampling in linear light before tone mapping, WebGLRenderer after it, so fog and thin hot edges differ by design; with `antialias: false` and no fog the two agree to 0.04 of 255. On `'webgpu'`, a `ShaderMaterial` / `RawShaderMaterial` / `onBeforeCompile` material stops the build with the fix (it used to draw a broken picture) and a `PMREMGenerator` taken from `three` is explained; where the browser has no WebGPU three's WebGL2 backend draws the same picture (`ThreeSequence.renderer` / `.backend` say what was used, and `movie.inspect()` lists them). Three's WebGPU renderer updates light uniforms once per animation-loop frame, so a seek or an export showed stale lights: the layer now advances three's node frame before each render and gives `time` the layer's own time. See `docs/dsl.md` → three.
|
|
20
|
+
- **Changed (types only): `ThreeContext.renderer` is `WebGLRenderer | WebGPURenderer`.** TypeScript code that used WebGL-only members of `ctx.renderer` has to narrow it (`ctx.renderer instanceof WebGLRenderer`); `setClearColor`, `toneMapping` and `toneMappingExposure` exist on both. `docs/api.md` → `pixi-effects/three` lists the whole entry.
|
|
21
|
+
|
|
3
22
|
## 0.39.0
|
|
4
23
|
|
|
5
24
|
**The tools tell the truth, and a file can be checked against what it must be**
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.
|
|
3
|
+
> **Status**: experimental — current release `0.41.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
|
|
|
7
7
|
## Make a video by asking an AI
|
|
8
8
|
|
|
@@ -21,11 +21,11 @@ Declarative composition and video rendering for the web. After Effects-style tim
|
|
|
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
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
|
-
- **Presentations** — `stops` in the composition pause the movie where you say; `Presenter` (`pixi-effects/present`) 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 (`exportPDF(movie)`, `pixi-effects render talk.html -o talk.pdf`).
|
|
24
|
+
- **Presentations** — `stops` in the composition pause the movie where you say; `Presenter` (`pixi-effects/present`) 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 (`exportPDF(movie)`, `pixi-effects render talk.html -o talk.pdf`).
|
|
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.
|
|
26
26
|
- **Audio-reactive visuals** — `audioEnvelope('music.mp3')` analyses loudness, bass / mid / treble and the beats per frame; `react()` turns them into keyframes, so the picture follows the music and still renders identically.
|
|
27
27
|
- **2.5D layers & camera** — add `threeD: true`, `z`, `rotationX/Y` and a `{ type: 'camera' }` layer for parallax, flips and dolly zooms; no three.js needed.
|
|
28
|
-
- **three.js layer** *(optional)* — drop a real three.js scene in as a layer via `pixi-effects/three`; keyframes drive its objects (`three.cube.rotation.y`).
|
|
28
|
+
- **three.js layer** *(optional)* — drop a real three.js scene in as a layer via `pixi-effects/three`; keyframes drive its objects (`three.cube.rotation.y`). It can live in the composition's space: the composition's camera, lights, shadows and fog drive the scene, so one `cameraPath()` flies through the flat layers and a glTF model together (`threeGltf`).
|
|
29
29
|
- **Built-in player UI — or your own** — a drop-in HTML5-`<video>`-style overlay controller (play, scrub, mute, volume, fullscreen, export-to-file) whose colours are one option (`theme: { accent: '#ff4d6d' }`); or build a player yourself on the movie's `<video>`-named events (`play pause ended seeking seeked volumechange error`).
|
|
30
30
|
- **MP4 / WebM / MOV export** — pick container and quality from the controller, call `movie.render()` from code, or render headless from the command line (`pixi-effects render`).
|
|
31
31
|
- **A timeline you can read and scrub** — `movie.timelineChart()` draws every layer as a bar on a time axis; `pixi-effects view` opens the page with that timeline under it, zoomable, with a playhead you can drag.
|
|
@@ -49,8 +49,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
49
49
|
"imports": {
|
|
50
50
|
"pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
|
|
51
51
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
52
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
53
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
52
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.41.0/dist/index.js",
|
|
53
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.41.0/dist/Controller.js"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
</script>
|
|
@@ -61,7 +61,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
61
61
|
</script>
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.
|
|
64
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.41.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.
|
|
65
65
|
|
|
66
66
|
> **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
|
|
67
67
|
>
|
|
@@ -69,7 +69,7 @@ Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg
|
|
|
69
69
|
|
|
70
70
|
#### Adding three.js (optional)
|
|
71
71
|
|
|
72
|
-
Add two more entries to the importmap above: three.js itself, and the `pixi-effects/three` entry.
|
|
72
|
+
Add two more entries to the importmap above: three.js itself, and the `pixi-effects/three` entry (and `three/addons/` if you load a glTF model with `threeGltf`).
|
|
73
73
|
|
|
74
74
|
```html
|
|
75
75
|
<script type="importmap">
|
|
@@ -77,7 +77,8 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
|
|
|
77
77
|
"imports": {
|
|
78
78
|
"...": "(everything from the importmap above)",
|
|
79
79
|
"three": "https://esm.sh/three@0.178.0",
|
|
80
|
-
"
|
|
80
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.178.0/examples/jsm/",
|
|
81
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.41.0/dist/three.js"
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
</script>
|
|
@@ -112,7 +113,7 @@ Always route `three` through the importmap — both your code and `pixi-effects/
|
|
|
112
113
|
</script>
|
|
113
114
|
```
|
|
114
115
|
|
|
115
|
-
The layer is a normal sprite once built, so 2D keyframes, masks and filters work on it. Full spec, rules and caveats (one WebGL context per layer, `update(t)` must be a pure function of `t`): [DSL reference § three](./docs/dsl.md#three) and [API reference](./docs/api.md#pixi-effectsthree).
|
|
116
|
+
The layer is a normal sprite once built, so 2D keyframes, masks and filters work on it. Full spec, rules and caveats (one WebGL context per layer, `update(t)` must be a pure function of `t`): [DSL reference § three](./docs/dsl.md#three) and [API reference](./docs/api.md#pixi-effectsthree).
|
|
116
117
|
|
|
117
118
|
With a bundler, `import { registerThree, three } from 'pixi-effects/three'` works the same once you've installed `three`.
|
|
118
119
|
|
|
@@ -164,7 +165,7 @@ sequences: [
|
|
|
164
165
|
]
|
|
165
166
|
```
|
|
166
167
|
|
|
167
|
-
`+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)
|
|
168
|
+
`+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).
|
|
168
169
|
|
|
169
170
|
## For AI agents
|
|
170
171
|
|
|
@@ -187,25 +188,10 @@ They ship in the npm package (`node_modules/pixi-effects/ai/`).
|
|
|
187
188
|
|
|
188
189
|
- [**DSL reference**](./docs/dsl.md) — composition, sequences, 3D layers & camera, three.js layer, keyframes, expressions, filters
|
|
189
190
|
- [**API reference**](./docs/api.md) — `Movie`, `Controller`, `pixi-effects/present`, `pixi-effects/three`, events, render options
|
|
190
|
-
- [**Interactive movies**](./examples/interactive/) — nine live samples of a movie you change while it plays (variables, `setVars`, `rebuild`, loop): rock paper scissors, a scrolling city, a tune from four chords, a card for anyone, choose your ending … ([
|
|
191
|
-
- [**
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
- `03-shapes.html` — every shape primitive
|
|
195
|
-
- `04-media.html` — image / video / audio
|
|
196
|
-
- `05-composition-mask.html` — nested compositions and masks
|
|
197
|
-
- `06-filters.html` — built-in and pixi-filters (as `{ type: 'custom' }`; most can now be written by name, see `docs/dsl.md` → Named filters)
|
|
198
|
-
- `07-transitions.html` — all seven transition kinds in one timeline
|
|
199
|
-
- `08-presets-export.html` — `kenBurns` preset and `movie.render()` from code
|
|
200
|
-
- `09-audio.html` — multi-track audio mixing with BGM ducking and SFX cues
|
|
201
|
-
- `10-three.html` — a three.js scene as a layer (`pixi-effects/three`)
|
|
202
|
-
- `11-depth.html` — 2.5D layers and camera: parallax, spin, dolly zoom
|
|
203
|
-
- `12-title-motion.html` — a staggered 2.5D title: letters fly in from depth, particles, orbiting camera
|
|
204
|
-
- `13-sfx.html` — synthesised sound effects (no audio files), a riser into a hit, a custom voice
|
|
205
|
-
- `15-custom-player.html` — a complete player of your own in about 40 lines (play, seek, volume, replay) using the movie's events
|
|
206
|
-
- `14-draw-on.html` — draw-on strokes (`trimStart` / `trimEnd`) and text that changes over time (`visibleChars`, `set: { text }`)
|
|
207
|
-
- `music-lab.html` — eight tunes written as text (no audio files): press play, read each score; `musicBuffer()` plays music with no movie
|
|
208
|
-
- `playground.html` — in-browser editor on the chat template's edit block: twelve examples, a problems panel from `movie.review()`, share links, save as HTML, and WebMCP tools for an AI agent
|
|
191
|
+
- [**Interactive movies**](./examples/interactive/) — nine live samples of a movie you change while it plays (variables, `setVars`, `rebuild`, loop): rock paper scissors, a scrolling city, a tune from four chords, a card for anyone, choose your ending … ([docs/api.md](./docs/api.md))
|
|
192
|
+
- [**Gallery**](./examples/gallery/) — 43 complete pieces, each one HTML file of data
|
|
193
|
+
- [**Recipes**](./skills/pixi-effects/reference/recipes.md) — tested code you can copy (`npx pixi-effects recipe`)
|
|
194
|
+
- [**Playground**](./examples/playground.html) — in-browser editor on the chat template's edit block: fourteen examples (the tested recipes), a problems panel from `movie.review()`, share links, save as HTML, and WebMCP tools for an AI agent
|
|
209
195
|
|
|
210
196
|
## Browser support
|
|
211
197
|
|
package/ai/chat-template.html
CHANGED
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"imports": {
|
|
60
60
|
"pixi.js": "https://cdn.jsdelivr.net/npm/pixi.js@8.22.0/dist/pixi.min.mjs",
|
|
61
61
|
"mediabunny": "https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm",
|
|
62
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
63
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
64
|
-
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
62
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.41.0/dist/index.js",
|
|
63
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.41.0/dist/Controller.js",
|
|
64
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.41.0/dist/present.js"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
</script>
|
package/ai/tools/check.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
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) ·
|
|
18
18
|
* --crop X,Y,W,H (with --at: each picture is that part of the canvas, in canvas pixels, enlarged up to 4x so a small figure can be judged) ·
|
|
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`) ·
|
|
19
20
|
* --onion A:B (one picture of the movement between A and B seconds, frames overlaid: onion.png) ·
|
|
20
21
|
* --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) ·
|
|
21
22
|
* --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 ·
|
|
@@ -81,10 +82,10 @@ export function findChrome(env = process.env, exists = fs.existsSync, platform =
|
|
|
81
82
|
return candidates.find(c => c && exists(c)) ?? null;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
export const USAGE = 'usage: node ai/tools/check.mjs <page.html> [--out DIR] [--frames N] [--formats mp4,webm] [--no-export] [--draft] [--at 3.5,title@end] [--crop X,Y,W,H] [--onion 1:3] [--seam] [--query a=1&b=2] [--var name=value] [--size 1920x1080,1080x1920] [--timeout S] [--root DIR] [--chrome PATH] [--strict] [--help]\n(the header of ai/tools/check.mjs explains each option)';
|
|
85
|
+
export const USAGE = 'usage: node ai/tools/check.mjs <page.html> [--out DIR] [--frames N] [--formats mp4,webm] [--no-export] [--draft] [--at 3.5,title@end] [--crop X,Y,W,H] [--pick X,Y] [--onion 1:3] [--seam] [--query a=1&b=2] [--var name=value] [--size 1920x1080,1080x1920] [--timeout S] [--root DIR] [--chrome PATH] [--strict] [--help]\n(the header of ai/tools/check.mjs explains each option)';
|
|
85
86
|
|
|
86
87
|
export function parseArgs(argv) {
|
|
87
|
-
const o = { page: null, out: null, frames: 12, formats: ['mp4'], export: true, timeout: 240, root: null, chrome: null, strict: false, at: null, crop: null, draft: false, onion: null, seam: false, query: null, vars: [], sizes: [], help: false };
|
|
88
|
+
const o = { page: null, out: null, frames: 12, formats: ['mp4'], export: true, timeout: 240, root: null, chrome: null, strict: false, at: null, crop: null, draft: false, onion: null, seam: false, query: null, pick: [], vars: [], sizes: [], help: false };
|
|
88
89
|
const need = (i, name) => { if (i + 1 >= argv.length) throw new Error(`${name} needs a value`); return argv[i + 1]; };
|
|
89
90
|
for (let i = 0; i < argv.length; i++) {
|
|
90
91
|
const a = argv[i];
|
|
@@ -102,6 +103,11 @@ export function parseArgs(argv) {
|
|
|
102
103
|
if (m.length !== 4 || !m.every(Number.isFinite) || m[0] < 0 || m[1] < 0 || m[2] < 1 || m[3] < 1) throw new Error(`--crop must look like 100,200,400,300 (x, y, width, height in canvas pixels, width and height at least 1), got "${argv[i]}"`);
|
|
103
104
|
o.crop = { x: Math.round(m[0]), y: Math.round(m[1]), width: Math.round(m[2]), height: Math.round(m[3]) };
|
|
104
105
|
}
|
|
106
|
+
else if (a === '--pick') {
|
|
107
|
+
const m = need(i++, a).split(',').map(Number);
|
|
108
|
+
if (m.length !== 2 || !m.every(Number.isFinite)) throw new Error(`--pick must look like 640,360 (x, y in canvas pixels), got "${argv[i]}"`);
|
|
109
|
+
o.pick.push({ x: m[0], y: m[1] });
|
|
110
|
+
}
|
|
105
111
|
else if (a === '--var') o.vars.push(parseVar(need(i++, a)));
|
|
106
112
|
else if (a === '--size') o.sizes.push(...parseSizes(need(i++, a)));
|
|
107
113
|
else if (a === '--seam') o.seam = true;
|
|
@@ -118,6 +124,7 @@ export function parseArgs(argv) {
|
|
|
118
124
|
else throw new Error(`unexpected argument ${a}`);
|
|
119
125
|
}
|
|
120
126
|
if (o.crop && !o.at) throw new Error('--crop crops the pictures of --at: give --at too (for example --at 3.5 --crop 100,200,400,300)');
|
|
127
|
+
if (o.pick.length && !o.at) throw new Error('--pick asks about the moments of --at: give --at too (for example --at 3.5 --pick 640,360)');
|
|
121
128
|
return o;
|
|
122
129
|
}
|
|
123
130
|
|
|
@@ -435,7 +442,7 @@ export async function runCheck(opts, log = console.log) {
|
|
|
435
442
|
const layoutProblems = rv.problems.filter(g => g.kind !== 'canvas'), canvasProblems = rv.problems.filter(g => g.kind === 'canvas');
|
|
436
443
|
report.inspect = { checkedFrames: rv.frames, issues: layoutProblems, review: rv.review, canvas: canvasProblems.map(g => g.message), canvasLayers: rv.canvas ?? [] };
|
|
437
444
|
if (layoutProblems.length) report.problems.push(`${layoutProblems.length} kind(s) of layout issue from movie.inspect`);
|
|
438
|
-
if (canvasProblems.length) report.problems.push(`${canvasProblems.length}
|
|
445
|
+
if (canvasProblems.length) report.problems.push(`${canvasProblems.length} layer(s) (canvas / three) that do not draw the same picture for the same time (movie.canvasProblems)`);
|
|
439
446
|
// fonts: a web font whose file did not load is a problem (the library lists a layer with no available font for review)
|
|
440
447
|
report.fonts = rv.fonts;
|
|
441
448
|
if (rv.fonts.failed.length) report.problems.push(`${rv.fonts.failed.length} web font(s) failed to load: ${rv.fonts.failed.join(', ')} (the layers that use them are drawn in a fallback)`);
|
|
@@ -466,6 +473,10 @@ export async function runCheck(opts, log = console.log) {
|
|
|
466
473
|
// depth of field: the blur (px) each threeD layer has at this moment, so a focus pull can be read as numbers (0 = sharp)
|
|
467
474
|
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(() => []);
|
|
468
475
|
if (rows.some(([, px]) => px > 0)) w.depthBlur = Object.fromEntries(rows);
|
|
476
|
+
// pick: what is under each --pick point at this moment (the frame is on screen after the snapshot above)
|
|
477
|
+
if (opts.pick.length) {
|
|
478
|
+
w.pick = await cdp.eval(`(${JSON.stringify(opts.pick)}).map(p => ({ x: p.x, y: p.y, hits: movie.pickAll(p.x, p.y).map(h => ({ name: h.name ?? h.path, path: h.path, kind: h.kind, object: h.three ? h.three.object : undefined })) }))`).catch(() => []);
|
|
479
|
+
}
|
|
469
480
|
// light: how lit each threeD layer is at this moment (left out when the composition has no light)
|
|
470
481
|
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(() => []);
|
|
471
482
|
if (lit.length) w.light = Object.fromEntries(lit);
|
|
@@ -682,11 +693,13 @@ function finish(report, outDir, log) {
|
|
|
682
693
|
if (g.length > 8) L.push(` … ${g.length - 8} more (report.json)`);
|
|
683
694
|
const cv = report.inspect.canvas ?? [];
|
|
684
695
|
const rows = report.inspect.canvasLayers ?? [];
|
|
685
|
-
if (cv.length) { L.push(` canvas ${cv.length}
|
|
686
|
-
|
|
687
|
-
const
|
|
696
|
+
if (cv.length) { L.push(` canvas ${cv.length} layer(s) (canvas / three) that do not draw the same picture for the same time:`); for (const m of cv.slice(0, 4)) L.push(` - ${m}`); }
|
|
697
|
+
for (const kind of ['canvas', 'three']) { // silence must not be the only sign that the check ran
|
|
698
|
+
const mine = rows.filter(r => (r.kind ?? 'canvas') === kind);
|
|
699
|
+
if (!mine.length) continue;
|
|
700
|
+
const good = mine.filter(r => r.ok), slow = [...mine].sort((a, b) => b.drawMs - a.drawMs)[0];
|
|
688
701
|
const ms = x => x < 10 ? x.toFixed(1) : String(Math.round(x));
|
|
689
|
-
if (good.length) L.push(`
|
|
702
|
+
if (good.length) L.push(` ${kind.padEnd(9)} ${cv.length ? 'the other ' : ''}${good.length} ${kind} layer(s) drew the same picture twice at ${good[0].moments.length} moments (two orders): deterministic · slowest draw ${ms(slow.drawMs)} ms${kind === 'canvas' ? ' on the CPU' : ''} (${slow.name})`);
|
|
690
703
|
}
|
|
691
704
|
const rv = report.inspect.review;
|
|
692
705
|
if (rv.length) {
|
|
@@ -700,6 +713,12 @@ function finish(report, outDir, log) {
|
|
|
700
713
|
L.push(` sizes ${g.map(x => `${x.size} ${x.issues.length ? x.issues.length + ' layout issue kind(s)' : 'no layout issue'}`).join(' · ')}${g.length > 1 ? ' (one contact sheet each: sheet.png, sheet-<size>.png)' : ''}`);
|
|
701
714
|
for (const x of g) for (const i of x.issues.slice(0, 3)) L.push(` - ${x.size}: ${i.message}`);
|
|
702
715
|
}
|
|
716
|
+
for (const w of (report.at ?? []).filter(x => x.pick)) {
|
|
717
|
+
for (const p of w.pick) {
|
|
718
|
+
const says = p.hits.length ? p.hits.map(h => h.kind === 'three' ? `${h.name} (three: ${h.object || '?'})` : h.name).join(', ') : 'nothing';
|
|
719
|
+
L.push(` pick ${w.label} (${p.x}, ${p.y}) — ${says}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
703
722
|
for (const w of (report.at ?? []).filter(x => x.depthBlur)) {
|
|
704
723
|
const shown = Object.entries(w.depthBlur).slice(0, 8).map(([k, px]) => `${k} ${px} px`).join(', ');
|
|
705
724
|
L.push(` depth ${w.label} — ${shown}${Object.keys(w.depthBlur).length > 8 ? ', …' : ''} (blur of each threeD layer at that moment; 0 = sharp)`);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pixi-effects probe model.glb` — what is in a glTF model, read in Node with no browser and no three.js: the clips (name and length), the size of the model in the file's own units
|
|
3
|
+
* (the bounding box of the meshes through the transforms of the nodes: the pose the file is stored in, which is what `threeGltf(...).size` measures), what it holds, and the notes an
|
|
4
|
+
* author trips on (a file that needs a decoder, a model with no clip).
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
const GLB_MAGIC = 0x46546c67, JSON_CHUNK = 0x4e4f534a;
|
|
10
|
+
const round = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
|
|
11
|
+
export const isGltfFile = (file) => /\.(glb|gltf)(\?.*)?$/i.test(file);
|
|
12
|
+
const isUrl = (f) => /^https?:\/\//i.test(f);
|
|
13
|
+
|
|
14
|
+
/** The JSON of a .glb (its first chunk) or a .gltf. */
|
|
15
|
+
function parseJson(file, buf) {
|
|
16
|
+
const name = isUrl(file) ? file : path.basename(file);
|
|
17
|
+
if (buf.length >= 12 && buf.readUInt32LE(0) === GLB_MAGIC) {
|
|
18
|
+
const version = buf.readUInt32LE(4);
|
|
19
|
+
if (version !== 2) throw new Error(`${name}: glTF version ${version} (only 2.0 is read)`);
|
|
20
|
+
let at = 12;
|
|
21
|
+
while (at + 8 <= buf.length) {
|
|
22
|
+
const len = buf.readUInt32LE(at), type = buf.readUInt32LE(at + 4);
|
|
23
|
+
if (type === JSON_CHUNK) return { json: JSON.parse(buf.subarray(at + 8, at + 8 + len).toString('utf8')), container: 'glb' };
|
|
24
|
+
at += 8 + len + ((4 - (len % 4)) % 4);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`${name}: a .glb with no JSON chunk`);
|
|
27
|
+
}
|
|
28
|
+
try { return { json: JSON.parse(buf.toString('utf8')), container: 'gltf' }; }
|
|
29
|
+
catch { throw new Error(`${name} is not a glTF model (it is neither a .glb, which starts with "glTF", nor glTF JSON)`); }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 4x4 matrices, column-major as glTF stores them.
|
|
33
|
+
const identity = () => [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
34
|
+
const multiply = (a, b) => { const o = new Array(16).fill(0); for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) for (let k = 0; k < 4; k++) o[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k]; return o; };
|
|
35
|
+
function local(node) {
|
|
36
|
+
if (node.matrix?.length === 16) return node.matrix;
|
|
37
|
+
const [tx, ty, tz] = node.translation ?? [0, 0, 0], [x, y, z, w] = node.rotation ?? [0, 0, 0, 1], [sx, sy, sz] = node.scale ?? [1, 1, 1];
|
|
38
|
+
return [
|
|
39
|
+
(1 - 2 * (y * y + z * z)) * sx, 2 * (x * y + z * w) * sx, 2 * (x * z - y * w) * sx, 0,
|
|
40
|
+
2 * (x * y - z * w) * sy, (1 - 2 * (x * x + z * z)) * sy, 2 * (y * z + x * w) * sy, 0,
|
|
41
|
+
2 * (x * z + y * w) * sz, 2 * (y * z - x * w) * sz, (1 - 2 * (x * x + y * y)) * sz, 0,
|
|
42
|
+
tx, ty, tz, 1,
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
const apply = (m, [x, y, z]) => [m[0] * x + m[4] * y + m[8] * z + m[12], m[1] * x + m[5] * y + m[9] * z + m[13], m[2] * x + m[6] * y + m[10] * z + m[14]];
|
|
46
|
+
|
|
47
|
+
/** The report of a glTF JSON. */
|
|
48
|
+
export function describeGltf(json, { file, bytes, container }) {
|
|
49
|
+
const nodes = json.nodes ?? [], meshes = json.meshes ?? [], accessors = json.accessors ?? [];
|
|
50
|
+
const notes = [];
|
|
51
|
+
const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
52
|
+
let measured = 0, unmeasured = 0;
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const visit = (i, parent) => {
|
|
55
|
+
if (seen.has(i) || !nodes[i]) return; // a node reached twice is a broken file, not a reason to loop
|
|
56
|
+
seen.add(i);
|
|
57
|
+
const world = multiply(parent, local(nodes[i]));
|
|
58
|
+
const mesh = meshes[nodes[i].mesh];
|
|
59
|
+
for (const prim of mesh?.primitives ?? []) {
|
|
60
|
+
const acc = accessors[prim.attributes?.POSITION];
|
|
61
|
+
if (!acc?.min || !acc?.max) { unmeasured++; continue; }
|
|
62
|
+
measured++;
|
|
63
|
+
for (const x of [acc.min[0], acc.max[0]]) for (const y of [acc.min[1], acc.max[1]]) for (const z of [acc.min[2], acc.max[2]]) {
|
|
64
|
+
const p = apply(world, [x, y, z]);
|
|
65
|
+
for (let k = 0; k < 3; k++) { if (p[k] < lo[k]) lo[k] = p[k]; if (p[k] > hi[k]) hi[k] = p[k]; }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const c of nodes[i].children ?? []) visit(c, world);
|
|
69
|
+
};
|
|
70
|
+
const scene = json.scenes?.[json.scene ?? 0];
|
|
71
|
+
const roots = scene?.nodes ?? nodes.map((_, i) => i).filter((i) => !nodes.some((n) => n.children?.includes(i)));
|
|
72
|
+
for (const r of roots) visit(r, identity());
|
|
73
|
+
const size = measured ? [0, 1, 2].map((k) => hi[k] - lo[k]) : null;
|
|
74
|
+
if (!size) notes.push('the size of the model could not be read (its position accessors have no min / max)');
|
|
75
|
+
else if (unmeasured) notes.push(`${unmeasured} mesh part(s) have no position min / max: the size leaves them out`);
|
|
76
|
+
|
|
77
|
+
const animations = (json.animations ?? []).map((a, index) => {
|
|
78
|
+
let duration = 0;
|
|
79
|
+
for (const s of a.samplers ?? []) { const t = accessors[s.input]; if (t?.max?.[0] > duration) duration = t.max[0]; }
|
|
80
|
+
return { index, name: a.name ?? `(unnamed ${index})`, duration: round(duration) };
|
|
81
|
+
});
|
|
82
|
+
if (!animations.length) notes.push('no animation: threeGltf poses nothing; the model stays in the pose it is stored in');
|
|
83
|
+
const required = json.extensionsRequired ?? [];
|
|
84
|
+
const decoders = required.filter((e) => /draco|meshopt|basisu|ktx2|webp/i.test(e));
|
|
85
|
+
if (decoders.length) notes.push(`needs ${decoders.join(', ')}: three's GLTFLoader needs a decoder for it (DRACOLoader / KTX2Loader / MeshoptDecoder), and threeGltf does not set one up: re-export the model without that compression, or load it yourself`);
|
|
86
|
+
const external = [...(json.buffers ?? []), ...(json.images ?? [])].filter((x) => x.uri && !/^data:/i.test(x.uri));
|
|
87
|
+
if (external.length) notes.push(`${external.length} file(s) are loaded next to the model (${external.slice(0, 3).map((x) => x.uri).join(', ')}${external.length > 3 ? ', …' : ''}): keep them beside it`);
|
|
88
|
+
return {
|
|
89
|
+
file, container: `glTF ${json.asset?.version ?? '2.0'} (${container})`, bytes,
|
|
90
|
+
counts: { nodes: nodes.length, meshes: meshes.length, skins: (json.skins ?? []).length, materials: (json.materials ?? []).length, textures: (json.textures ?? []).length },
|
|
91
|
+
size: size ? size.map((v) => round(v, 6)) : null, longest: size ? round(Math.max(...size), 6) : null,
|
|
92
|
+
animations, extensions: json.extensionsUsed ?? [], notes,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Read a local file or a URL and describe it. */
|
|
97
|
+
export async function probeGltf(file) {
|
|
98
|
+
let buf;
|
|
99
|
+
if (isUrl(file)) {
|
|
100
|
+
const res = await fetch(file);
|
|
101
|
+
if (!res.ok) throw new Error(`could not read ${file}: HTTP ${res.status}`);
|
|
102
|
+
buf = Buffer.from(await res.arrayBuffer());
|
|
103
|
+
} else {
|
|
104
|
+
if (!fs.existsSync(file)) throw new Error(`${file}: no such file`);
|
|
105
|
+
buf = fs.readFileSync(file);
|
|
106
|
+
}
|
|
107
|
+
const { json, container } = parseJson(file, buf);
|
|
108
|
+
return describeGltf(json, { file, bytes: isUrl(file) ? null : buf.length, container });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const num = (v) => String(round(v, 3));
|
|
112
|
+
const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
113
|
+
export function gltfText(r) {
|
|
114
|
+
const kb = (n) => (n >= 1048576 ? `${(n / 1048576).toFixed(2)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`);
|
|
115
|
+
const L = [r.file, ` ${r.container}${r.bytes != null ? ` · ${kb(r.bytes)}` : ''}`];
|
|
116
|
+
const c = r.counts;
|
|
117
|
+
L.push(` model ${plural(c.nodes, 'node')} · ${plural(c.meshes, 'mesh').replace('meshs', 'meshes')} · ${plural(c.skins, 'skin')} · ${plural(c.materials, 'material')} · ${plural(c.textures, 'texture')}`);
|
|
118
|
+
if (r.size) L.push(` size ${r.size.map(num).join(' × ')} (x × y × z in the file's units, as stored: the longest side is ${num(r.longest)}; threeGltf's fit(px) scales that side to px)`);
|
|
119
|
+
for (const a of r.animations) L.push(` clip ${a.index} "${a.name}" ${num(a.duration)} s`);
|
|
120
|
+
for (const n of r.notes) L.push(` note ${n}`);
|
|
121
|
+
return L.join('\n');
|
|
122
|
+
}
|
package/ai/tools/media.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* pixi-effects probe | trim | frames | sound — look at, listen to and cut footage, with no ffmpeg installed.
|
|
4
4
|
*
|
|
5
|
-
* npx pixi-effects probe <file|url> [--keyframes] [--json] what is in a file: container, duration, tracks, codecs, size, notes
|
|
5
|
+
* npx pixi-effects probe <file|url> [--keyframes] [--json] what is in a file: container, duration, tracks, codecs, size, notes (a .glb / .gltf model: clips and size)
|
|
6
6
|
* npx pixi-effects trim <file|url> <start> <end> -o <out> [--exact] [--force] [--json]
|
|
7
7
|
* cut [start, end] (seconds, or 1:02.5): by COPY when it can (no re-encode, no loss),
|
|
8
8
|
* and it says the real times; --exact re-encodes so the start is the frame asked for
|
|
@@ -22,6 +22,7 @@ import os from 'node:os';
|
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { createRequire } from 'node:module';
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
|
+
import { gltfText, isGltfFile, probeGltf } from './gltfProbe.mjs';
|
|
25
26
|
|
|
26
27
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
27
28
|
const PACKAGE_ROOT = path.resolve(HERE, '../..');
|
|
@@ -314,6 +315,12 @@ export function checkExpect(report, items) {
|
|
|
314
315
|
function bitrateWanted(op, raw) { const n = expectNumber('bitrate', raw); return `${op}${Math.round(n / 1e5) / 10} Mbps`; }
|
|
315
316
|
|
|
316
317
|
commands.probe = async ({ file, opts }, io) => {
|
|
318
|
+
if (isGltfFile(file)) { // a model, not footage: read in Node, no container library, no browser
|
|
319
|
+
if (opts.expect !== undefined || opts.keyframes) throw new UsageError('probe on a glTF model takes no --expect or --keyframes (they are for video and audio files); it prints the clips and the size');
|
|
320
|
+
const r = await probeGltf(file);
|
|
321
|
+
io.log(opts.json ? JSON.stringify(r, null, 2) : gltfText(r));
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
317
324
|
const items = opts.expect !== undefined ? parseExpect(opts.expect) : null; // a bad expectation is a usage error before any file is read
|
|
318
325
|
const r = await probeFile(file, { keyframes: !!opts.keyframes, exact: !!items });
|
|
319
326
|
const results = items ? checkExpect(r, items) : null;
|
|
@@ -22,7 +22,7 @@ const USAGE = `usage:
|
|
|
22
22
|
pixi-effects check <page.html> [--at 3,6] [--out dir] review a video page: warnings, layout, sound, contact sheet, real export
|
|
23
23
|
pixi-effects render <page.html> -o <file.mp4> export the video
|
|
24
24
|
pixi-effects view <page.html> look at it in a browser, with a timeline
|
|
25
|
-
pixi-effects probe <file|url> what is in a video or audio file (codecs, size, duration), with no ffmpeg
|
|
25
|
+
pixi-effects probe <file|url> what is in a video or audio file (codecs, size, duration), with no ffmpeg; or a .glb / .gltf model (its clips and its size)
|
|
26
26
|
pixi-effects trim <file|url> <start> <end> -o <out> cut a file (by copy when it can) and say the real times
|
|
27
27
|
pixi-effects frames <file|url> [--at 1,50%,end] [--sheet] pictures of a video at those times
|
|
28
28
|
pixi-effects sound <file|url> [--waveform] loudness, peak, silences of a file's sound
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Container } from 'pixi.js';
|
|
2
|
-
import {
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape } from './types-DIh93lFb.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a routed keyframe path to a concrete tween target.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Container } from 'pixi.js';
|
|
2
|
-
import {
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape } from './types-DIh93lFb.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a routed keyframe path to a concrete tween target.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk3HZV3UHU_cjs = require('./chunk-3HZV3UHU.cjs');
|
|
4
|
+
require('./chunk-X62H3RBG.cjs');
|
|
5
|
+
require('./chunk-JN7JHNZR.cjs');
|
|
6
|
+
require('./chunk-3SAS3ZE5.cjs');
|
|
7
|
+
require('./chunk-TWZNOUM7.cjs');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Object.defineProperty(exports, "CompositionSequence", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: function () { return chunk3HZV3UHU_cjs.CompositionSequence; }
|
|
14
|
+
});
|
|
15
|
+
//# sourceMappingURL=Composition-N6BOROJ2.cjs.map
|
|
16
|
+
//# sourceMappingURL=Composition-N6BOROJ2.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-N6BOROJ2.cjs"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { CompositionSequence } from './chunk-ICI6ACHY.js';
|
|
2
|
+
import './chunk-4QVQ5UJJ.js';
|
|
3
|
+
import './chunk-VNP7QCJE.js';
|
|
4
|
+
import './chunk-32MMNDDN.js';
|
|
5
|
+
import './chunk-BXOCEZ4U.js';
|
|
6
|
+
//# sourceMappingURL=Composition-YGBQX3HZ.js.map
|
|
7
|
+
//# sourceMappingURL=Composition-YGBQX3HZ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-YGBQX3HZ.js"}
|
package/dist/Controller.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { M as Movie } from './Movie-
|
|
1
|
+
import { M as Movie } from './Movie-DWtxRiJ2.cjs';
|
|
2
2
|
import 'pixi.js';
|
|
3
3
|
import './soundEntry-DT13DNt8.cjs';
|
|
4
|
-
import './types-
|
|
4
|
+
import './types-DIh93lFb.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The look of the bar. Each key sets a CSS custom property on the bar (so you can also set `--mc-accent` … in your own CSS, on the
|
package/dist/Controller.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { M as Movie } from './Movie-
|
|
1
|
+
import { M as Movie } from './Movie-CwqRLOzE.js';
|
|
2
2
|
import 'pixi.js';
|
|
3
3
|
import './soundEntry-DT13DNt8.js';
|
|
4
|
-
import './types-
|
|
4
|
+
import './types-DIh93lFb.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The look of the bar. Each key sets a CSS custom property on the bar (so you can also set `--mc-accent` … in your own CSS, on the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Application } from 'pixi.js';
|
|
2
2
|
import { A as AudioReport, a as AudioInspectOptions } from './soundEntry-DT13DNt8.js';
|
|
3
|
-
import {
|
|
3
|
+
import { b as Stop, A as AssetSpec, a as CompositionSpec } from './types-DIh93lFb.js';
|
|
4
4
|
|
|
5
5
|
type EaseFn = (p: number) => number;
|
|
6
6
|
|
|
@@ -149,6 +149,8 @@ interface CanvasRow {
|
|
|
149
149
|
ok: boolean;
|
|
150
150
|
/** Why not, in a sentence (empty when ok). */
|
|
151
151
|
problem: string;
|
|
152
|
+
/** Which kind of layer drew it: a `canvas` layer or a `three` layer (both are held to "a function of the time"). */
|
|
153
|
+
kind: 'canvas' | 'three';
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
/** One kind of issue, with how many frames it was seen in. Issues that differ only by numbers are one kind. */
|
|
@@ -208,6 +210,9 @@ interface LayerInfo {
|
|
|
208
210
|
depthBlur?: number;
|
|
209
211
|
/** 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. */
|
|
210
212
|
light?: number;
|
|
213
|
+
/** A `three` layer: the renderer it draws with (`'webgl'` / `'webgpu'`) and the backend under it (`'webgl2'`: three's WebGPURenderer found no WebGPU and used its own WebGL2 backend: the same picture). */
|
|
214
|
+
renderer?: 'webgl' | 'webgpu';
|
|
215
|
+
backend?: 'webgl' | 'webgpu' | 'webgl2';
|
|
211
216
|
}
|
|
212
217
|
interface InspectReport {
|
|
213
218
|
frame: number;
|
|
@@ -230,6 +235,39 @@ interface InspectOptions {
|
|
|
230
235
|
layers?: 'all' | 'visible' | 'none';
|
|
231
236
|
}
|
|
232
237
|
|
|
238
|
+
interface PickHit {
|
|
239
|
+
/** The layer's `name` (when it has one). */
|
|
240
|
+
name?: string;
|
|
241
|
+
/** Names (or `type#index`) from the root, joined by `/`: the same as `movie.inspect()`'s `path`. */
|
|
242
|
+
path: string;
|
|
243
|
+
type: string;
|
|
244
|
+
/** `'flat'`: a 2D layer; `'card'`: a `threeD` layer (its projected card); `'three'`: a `three` layer, and `three` says what the ray met. */
|
|
245
|
+
kind: 'flat' | 'card' | 'three';
|
|
246
|
+
/** The point asked about, in canvas pixels. */
|
|
247
|
+
point: {
|
|
248
|
+
x: number;
|
|
249
|
+
y: number;
|
|
250
|
+
};
|
|
251
|
+
/** A flat layer: the point in the layer's own pixels (the top-left of its picture is 0, 0). */
|
|
252
|
+
local?: {
|
|
253
|
+
x: number;
|
|
254
|
+
y: number;
|
|
255
|
+
};
|
|
256
|
+
/** A `three` layer: what the ray through that point met first: the object (its name, or `''`), where (three's own units), and how far from the camera. */
|
|
257
|
+
three?: {
|
|
258
|
+
object: unknown;
|
|
259
|
+
point: [number, number, number];
|
|
260
|
+
distance: number;
|
|
261
|
+
object3d?: unknown;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
interface PickOptions {
|
|
265
|
+
/** Only these layers can be hit (by `name` or by `path`). */
|
|
266
|
+
only?: string[];
|
|
267
|
+
/** These layers cannot be hit (by `name` or by `path`). */
|
|
268
|
+
ignore?: string[];
|
|
269
|
+
}
|
|
270
|
+
|
|
233
271
|
/** One layer (or a run of similar layers) on the timeline. Times are in seconds, absolute (from the start of the movie). */
|
|
234
272
|
interface TimelineRow {
|
|
235
273
|
/** `parent/child` for a layer inside a composition. */
|
|
@@ -673,6 +711,19 @@ declare class Movie {
|
|
|
673
711
|
* Where every layer is drawn at `frame` (canvas pixels, visibility) plus `issues` — text that is off
|
|
674
712
|
* the canvas, cut by an edge, empty, or overlapping other text. Seeks there and stays there.
|
|
675
713
|
*/
|
|
714
|
+
/**
|
|
715
|
+
* What is under the canvas point (x, y) (canvas pixels: the movie's own `width` x `height`; `canvasPoint(event)` makes them from a pointer event) at the frame on screen: the layer on
|
|
716
|
+
* top, or null. A flat layer is hit by its own shape, a `threeD` card by its projected quad, a `three` layer by a ray through its camera (`hit.three` has the object). It reads the
|
|
717
|
+
* frame as the last draw left it, so after `await movie.snapshot(frame)` or any seek it answers for that frame. `opts.only` / `opts.ignore`: layer names or paths.
|
|
718
|
+
*/
|
|
719
|
+
pick(x: number, y: number, opts?: PickOptions): PickHit | null;
|
|
720
|
+
/** Every layer under the canvas point, the one on top first (see `pick`). */
|
|
721
|
+
pickAll(x: number, y: number, opts?: PickOptions): PickHit[];
|
|
722
|
+
/** A pointer event's place in canvas pixels (the canvas may be shown larger or smaller than its own size): `movie.pick(...movie.canvasPoint(event))`. */
|
|
723
|
+
canvasPoint(event: {
|
|
724
|
+
clientX: number;
|
|
725
|
+
clientY: number;
|
|
726
|
+
}): [number, number];
|
|
676
727
|
inspect(frame?: number, opts?: InspectOptions): Promise<InspectReport>;
|
|
677
728
|
/** The canvas layers whose same frame is not the same picture (what `check` reports as a problem): one sentence each. */
|
|
678
729
|
canvasProblems(): string[];
|
|
@@ -763,4 +814,4 @@ declare class Movie {
|
|
|
763
814
|
destroy(): Promise<void>;
|
|
764
815
|
}
|
|
765
816
|
|
|
766
|
-
export { type ContactSheetOptions as C, type FontReport as F, type InspectOptions as I, type LayerInfo as L, Movie as M, type OnionSkinOptions as O, type
|
|
817
|
+
export { type ContactSheetOptions as C, type FontReport as F, type InspectOptions as I, type LayerInfo as L, Movie as M, type OnionSkinOptions as O, type PickHit as P, type RebuildOptions as R, type SnapshotOptions as S, type ThumbnailOptions as T, type VolumeEvent as V, type MotionBlurSpec as a, type FrameEvent as b, type InspectReport as c, type IssueGroup as d, type LoadProgressState as e, type LoadStage as f, type LoaderOption as g, type LoopEvent as h, type MotionBlurOptions as i, type MovieErrorEvent as j, type MovieOptions as k, type PickOptions as l, type ProgressEvent as m, type RebuildReason as n, type RebuildReport as o, type RenderOptions as p, type ReviewOptions as q, type ReviewReport as r, type TimelineData as s, type TimelineHtmlOptions as t, type TimelineRow as u, type TimelineSvgOptions as v, type TimelineTransition as w, dismissLoader as x, failLoader as y, resolveLoader as z };
|