pixi-effects 0.28.2 → 0.29.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 +52 -0
- package/README.md +10 -10
- package/ai/chat-template.html +5 -5
- package/ai/tools/check.mjs +18 -3
- package/ai/tools/pixi-effects.mjs +1 -1
- package/ai/tools/render.mjs +14 -9
- package/ai/tools/view.mjs +6 -2
- package/dist/Base-CB1_FPZD.d.cts +60 -0
- package/dist/Base-Ne74ztNl.d.ts +60 -0
- package/dist/Composition-MUKR3FYD.cjs +16 -0
- package/dist/{Composition-K6B3APM5.cjs.map → Composition-MUKR3FYD.cjs.map} +1 -1
- package/dist/Composition-RJGA7KDU.js +7 -0
- package/dist/{Composition-H7UNR5F5.js.map → Composition-RJGA7KDU.js.map} +1 -1
- package/dist/Controller.cjs +73 -45
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +16 -22
- package/dist/Controller.d.ts +16 -22
- package/dist/Controller.js +74 -40
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-CHcM8HNB.d.ts → Movie-C2rZFkrO.d.ts} +227 -119
- package/dist/{Movie-Cx5ozUGb.d.cts → Movie-DdnpLpw1.d.cts} +227 -119
- package/dist/{blendModes-HDOP6R7T.cjs → blendModes-ETMYWATF.cjs} +5 -5
- package/dist/{blendModes-HDOP6R7T.cjs.map → blendModes-ETMYWATF.cjs.map} +1 -1
- package/dist/{blendModes-PAVTW3BO.js → blendModes-RIVZXL5A.js} +4 -4
- package/dist/{blendModes-PAVTW3BO.js.map → blendModes-RIVZXL5A.js.map} +1 -1
- package/dist/{chunk-P4GY3Z4G.cjs → chunk-2DXPTKQZ.cjs} +6 -6
- package/dist/chunk-2DXPTKQZ.cjs.map +1 -0
- package/dist/{chunk-TYWFKCPY.js → chunk-4KB3GUEK.js} +5 -5
- package/dist/chunk-4KB3GUEK.js.map +1 -0
- package/dist/{chunk-5MZWXVTT.js → chunk-A3Q3SDSH.js} +35 -28
- package/dist/chunk-A3Q3SDSH.js.map +1 -0
- package/dist/chunk-CRQFDY7E.js +4182 -0
- package/dist/chunk-CRQFDY7E.js.map +1 -0
- package/dist/chunk-HREBQAY4.cjs +4206 -0
- package/dist/chunk-HREBQAY4.cjs.map +1 -0
- package/dist/{chunk-ZMAD3KJU.cjs → chunk-LYOKH7KE.cjs} +22 -3
- package/dist/chunk-LYOKH7KE.cjs.map +1 -0
- package/dist/{chunk-45QKLD6M.js → chunk-RCVZX2JP.js} +22 -3
- package/dist/chunk-RCVZX2JP.js.map +1 -0
- package/dist/{chunk-KBH33IQX.cjs → chunk-WHNADXXG.cjs} +121 -114
- package/dist/chunk-WHNADXXG.cjs.map +1 -0
- package/dist/{chunk-7VN2KI3U.js → chunk-ZAXYPXKW.js} +14 -6
- package/dist/chunk-ZAXYPXKW.js.map +1 -0
- package/dist/{chunk-MJW7KILM.cjs → chunk-ZVUO325O.cjs} +32 -24
- package/dist/chunk-ZVUO325O.cjs.map +1 -0
- package/dist/index.cjs +106 -4482
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -60
- package/dist/index.d.ts +15 -60
- package/dist/index.js +6 -4457
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/dist/{Presenter.cjs → present.cjs} +370 -31
- package/dist/present.cjs.map +1 -0
- package/dist/present.d.cts +278 -0
- package/dist/present.d.ts +278 -0
- package/dist/{Presenter.js → present.js} +358 -32
- package/dist/present.js.map +1 -0
- package/dist/three.cjs +6 -6
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +2 -2
- package/dist/{types-DpK1vwcl.d.ts → types-BzIB65Vk.d.cts} +11 -265
- package/dist/{types-DpK1vwcl.d.cts → types-BzIB65Vk.d.ts} +11 -265
- package/docs/api.md +44 -77
- package/docs/dsl.md +16 -14
- package/llms-full.txt +86 -115
- package/llms.txt +1 -1
- package/package.json +5 -5
- package/skills/pixi-effects/SKILL.md +5 -5
- package/skills/pixi-effects/reference/cheatsheet.md +9 -9
- package/skills/pixi-effects/reference/direction.md +2 -0
- package/skills/pixi-effects/reference/pitfalls.md +6 -6
- package/skills/pixi-effects/reference/recipes.md +4 -4
- package/skills/pixi-effects/template.html +7 -7
- package/dist/Base-C6f0fE8j.d.ts +0 -109
- package/dist/Base-DI1FCGcH.d.cts +0 -109
- package/dist/Composition-H7UNR5F5.js +0 -7
- package/dist/Composition-K6B3APM5.cjs +0 -16
- package/dist/Presenter.cjs.map +0 -1
- package/dist/Presenter.d.cts +0 -122
- package/dist/Presenter.d.ts +0 -122
- package/dist/Presenter.js.map +0 -1
- package/dist/chunk-45QKLD6M.js.map +0 -1
- package/dist/chunk-5MZWXVTT.js.map +0 -1
- package/dist/chunk-7VN2KI3U.js.map +0 -1
- package/dist/chunk-KBH33IQX.cjs.map +0 -1
- package/dist/chunk-MJW7KILM.cjs.map +0 -1
- package/dist/chunk-P4GY3Z4G.cjs.map +0 -1
- package/dist/chunk-TYWFKCPY.js.map +0 -1
- package/dist/chunk-ZMAD3KJU.cjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixi-effects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"require": { "types": "./dist/Controller.d.cts", "default": "./dist/Controller.cjs" }
|
|
14
14
|
},
|
|
15
15
|
"./loader.css": "./dist/loader.css",
|
|
16
|
-
"./
|
|
17
|
-
"import": { "types": "./dist/
|
|
18
|
-
"require": { "types": "./dist/
|
|
16
|
+
"./present": {
|
|
17
|
+
"import": { "types": "./dist/present.d.ts", "default": "./dist/present.js" },
|
|
18
|
+
"require": { "types": "./dist/present.d.cts", "default": "./dist/present.cjs" }
|
|
19
19
|
},
|
|
20
20
|
"./three": {
|
|
21
21
|
"import": { "types": "./dist/three.d.ts", "default": "./dist/three.js" },
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"main": "./dist/index.cjs",
|
|
26
26
|
"module": "./dist/index.js",
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
|
-
"bin": { "pixi-effects": "ai/tools/pixi-effects.mjs"
|
|
28
|
+
"bin": { "pixi-effects": "ai/tools/pixi-effects.mjs" },
|
|
29
29
|
"files": ["dist", "ai", "skills", "docs/dsl.md", "docs/api.md", "llms.txt", "llms-full.txt", "CHANGELOG.md", "README.md", "LICENSE"],
|
|
30
30
|
"keywords": [
|
|
31
31
|
"pixi", "pixijs", "video", "composition", "compositing", "rendering",
|
|
@@ -3,7 +3,7 @@ name: pixi-effects
|
|
|
3
3
|
description: Use when asked to make, animate, preview or export a video, motion graphics, an animated title, a lower third, kinetic type, a promo, a slideshow with transitions, an animated chart or data story, a social clip, a 2.5D / depth scene, or music and sound for a video, written as code in JavaScript or HTML. pixi-effects is a JS library where a video is a plain-object composition (text, shape, image, video, audio and file-free sfx / music, camera, light and shader layers with keyframes) that plays in the browser, is checked by a tool the agent can run, and exports MP4 / WebM / MOV with no server. Not for a project that already uses another video tool (Remotion, Motion Canvas, ffmpeg); follow that tool instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
> Written for pixi-effects 0.
|
|
6
|
+
> Written for pixi-effects 0.29.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
|
|
7
7
|
|
|
8
8
|
# pixi-effects — write a video as data
|
|
9
9
|
|
|
@@ -21,10 +21,10 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
21
21
|
- `await movie.inspect(frame)` → every layer's canvas bounds and visibility, plus `issues` (text off the canvas, cut off by an edge, empty, or overlapping other text). Run it at several frames; fix every issue it names.
|
|
22
22
|
- `await movie.snapshot(frame, { as: 'dataURL' })` → one frame, canvas only (no player bar).
|
|
23
23
|
- `movie.inspectAudio()` → **hear with numbers**: for every sound its `start`, `sound.length`, `sound.peakDb`, `sound.loudestAt` (line this up with the picture) and `sound.brightnessHz` (< 500 dark, > 8000 airy), plus `issues` (limited mix, inaudible or cut-off sounds). Check it whenever the movie has audio; `render()` encodes this same mix.
|
|
24
|
-
- **A moment, a movement, the sound (`pixi-effects
|
|
25
|
-
- **A deck** (`composition.stops`, `deck()`): `await movie.contactSheet({ frames: movie.stops.map(s => s.frame), as: 'dataURL' })` is one picture of every stop (the check writes it as `stops.png`); what the audience reads while a page waits is that picture, so check every one. `await
|
|
24
|
+
- **A moment, a movement, the sound (`pixi-effects check` options):** `--at 3.5,title@end,50%,f120` writes those pictures to `check-out/<name>/frames/*.png` (and `at.png`): the last frame of a layer is `name@end`, the middle `name@mid`. `--onion 1:3` writes `onion.png`, eight frames of seconds 1–3 overlaid (the later the stronger): a trail that shows a movement's path and easing (even steps = constant speed, bunched = slow). `report.audio` now has `loudness` (`integratedLufs`, `truePeakDb`, `clippedSamples`, `silences`), `scenes` (per named top-level composition), `cues` (when each sound starts) and `notes` (advice: quiet or loud for web video, little headroom, silent stretches; web video is typically −14 to −16 LUFS); `waveform.png` draws the mix with the scene edges and the cues. A web font that failed to load fails the check; a text layer none of whose fonts is installed is listed for review.
|
|
25
|
+
- **A deck** (`composition.stops`, `deck()`): `await movie.contactSheet({ frames: movie.stops.map(s => s.frame), as: 'dataURL' })` is one picture of every stop (the check writes it as `stops.png`); what the audience reads while a page waits is that picture, so check every one. `await inspectStops(movie)` (from `pixi-effects/present`) lists the stops where the picture is still changing (a stop landing before its animation ends); the check prints it as a review list: move the stop later or flag a settled one `pdf: true`. `await stopImages(movie, { which: 'pages' })` / `exportPDF(movie)` give the pages themselves.
|
|
26
26
|
From a browser tool the result is a `data:` URL string: `agent-browser eval "movie.contactSheet({ count: 6, as: 'dataURL' })" | python3 scripts/save-image.py /absolute/sheet.png`, then open the PNG. Sheet tiles are small (~480 px): use `snapshot` for detail. `inspect` output is long on busy scenes: pass `{ layers: 'none' }` and read `issues` first. Give layers a `name` so inspect paths are readable. A moving ticker is not flagged as cut off.
|
|
27
|
-
6. **Iterate** on what you saw (clipped text, collisions, wrong timing). Fast motion looks choppy: add `motionBlur: true` to `movie.init` (render, snapshot and contact sheet then show a film-style blur; renders take ~8× longer). Then export: `const blob = await movie.render({ format: 'mp4' })` (about real time; `movie.on('progress', …)` reports 0–100). **While you iterate, do not export the whole movie:** `check --no-export --at <the moment you changed>` takes seconds (3 to 10 s on the gallery pieces: most of it is loading the page and measuring the sound); to see motion, `render({ range: [from, to] })` / `pixi-effects
|
|
27
|
+
6. **Iterate** on what you saw (clipped text, collisions, wrong timing). Fast motion looks choppy: add `motionBlur: true` to `movie.init` (render, snapshot and contact sheet then show a film-style blur; renders take ~8× longer). Then export: `const blob = await movie.render({ format: 'mp4' })` (about real time; `movie.on('progress', …)` reports 0–100). **While you iterate, do not export the whole movie:** `check --no-export --at <the moment you changed>` takes seconds (3 to 10 s on the gallery pieces: most of it is loading the page and measuring the sound); to see motion, `render({ range: [from, to] })` / `pixi-effects render --range 10:15` / `--scene name` exports only that part (picture and sound); `draft: true` / `--draft` is half size, low quality and no motion blur: a much smaller file (a piece that asks for motion blur renders much faster as a draft: 4 s of `hanabi-night` took 14.0 s, and 8.2 s as a draft, about 7 s of either being the browser's start-up), but the drawing itself is not faster. Export the whole movie once, at the end.
|
|
28
28
|
|
|
29
29
|
## For a film (more than a title card): direct it first
|
|
30
30
|
|
|
@@ -46,7 +46,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
46
46
|
- **2.5D needs `threeD: true`** on each layer plus a `{ type: 'camera' }` layer; camera props go in `initial`/keyframes; keep every `z` below the camera distance (≈ 989 at 720p, fov 40); under a dolly zoom only `z = 0` stays fixed; **depth of field** is two camera props, `focus` (a threeD layer's name or a z) and `aperture` (30 default, 0 off), and a rack focus is a keyframe on `focus`.
|
|
47
47
|
- **Group / parent layers = `{ type: 'null', name: 'rig' }`** (draws nothing, only moves) + `parent: 'rig'` on the layers that should follow it: their `x`/`y` are then measured from the null's origin, and the null's position / rotation / scale / alpha carry them (orbits, clock hands, grouped fades). Nulls chain. Only `null` layers can be parents; not for `threeD` layers.
|
|
48
48
|
- **Time:** `speed` (slow motion, backward) and `time` (a curve of the layer's own seconds: freeze, rewind, ramp) on video, audio-file and composition layers; put what you want to rewind in a composition. Children of such a composition are in ITS seconds. `ease: 'cubic-bezier(.4, 0, .2, 1)'` is the CSS curve. See the recipe `rewind-and-replay` and pitfalls 69–73.
|
|
49
|
-
- **Built in:** `animateText()` (per-letter / per-word / per-line entrances and exits in one call: `...animateText('Title', style, { x, y, align: 'center', at: 0.5, duration: 5, in: 'rise', out: 'fade', stagger: { each: 0.05 } })`), `splitText()` / `measureText()` (per-letter / per-word layers, exact positions, by hand), `shape: 'arc'` (progress rings, pie / donut slices: animate `endAngle`), `fillGradient` (linear/radial, alpha stops → vignettes), `{value}` counters, `repeat`/`yoyo`, `orbit()`, seeded randomness (`rand(seed)` / `noise(x, seed)` in expressions, `random(seed)` in JS, never `Math.random()`) `wiggle()` (a seeded shake as keyframes; on a camera put it on `offsetX` / `offsetY` so it rides on the move) `cameraPath()` (a camera flight through `[x, y, z]` points as keyframes; layers it passes: `hideBehindCamera: true`) depth of field (camera `focus` + `aperture`: `{ type: 'camera', initial: { focus: 'title' } }` blurs the other threeD layers by depth; a rack focus is a keyframe on `focus`) `music` (a tune with no file: `{ type: 'audio', music: { bpm, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4' }, { inst: 'bass', notes: 'c2:2 g1:2' }], drums: { from: 8, kick: 'x...x...' } } }`: notes and chords as text, counted in beats; instruments `keys pluck pad bass sub lead bell musicbox`; `musicEnvelope(music)` feeds `react()`), `audioEnvelope()` / `bpmEnvelope()` + `react()` (visuals that follow music: `const env = await audioEnvelope('music.mp3', { frameRate })` before `movie.init`, then `keyframes: react(env, { duration, props: { scale: { base: 1, amount: 0.4, band: 'bass', release: 0.2 } } })`; `beats: true` pulses on the beats; `bpmEnvelope(120, …)` when there is no file), `deck()` + `Presenter` (a talk as one movie: pages, steps, stops that pause playback, arrow keys / click / swipe to move on: `...deck({ transition: { kind: 'slide', duration: 0.6 }, pages: [{ name, duration, stops, sequences }] })`, `new Presenter(movie, { canvas })` from `pixi-effects/presenter`; G = page overview, P = presenter view with notes and a timer; `pixi-effects
|
|
49
|
+
- **Built in:** `animateText()` (per-letter / per-word / per-line entrances and exits in one call: `...animateText('Title', style, { x, y, align: 'center', at: 0.5, duration: 5, in: 'rise', out: 'fade', stagger: { each: 0.05 } })`), `splitText()` / `measureText()` (per-letter / per-word layers, exact positions, by hand), `shape: 'arc'` (progress rings, pie / donut slices: animate `endAngle`), `fillGradient` (linear/radial, alpha stops → vignettes), `{value}` counters, `repeat`/`yoyo`, `orbit()`, seeded randomness (`rand(seed)` / `noise(x, seed)` in expressions, `random(seed)` in JS, never `Math.random()`) `wiggle()` (a seeded shake as keyframes; on a camera put it on `offsetX` / `offsetY` so it rides on the move) `cameraPath()` (a camera flight through `[x, y, z]` points as keyframes; layers it passes: `hideBehindCamera: true`) depth of field (camera `focus` + `aperture`: `{ type: 'camera', initial: { focus: 'title' } }` blurs the other threeD layers by depth; a rack focus is a keyframe on `focus`) `music` (a tune with no file: `{ type: 'audio', music: { bpm, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4' }, { inst: 'bass', notes: 'c2:2 g1:2' }], drums: { from: 8, kick: 'x...x...' } } }`: notes and chords as text, counted in beats; instruments `keys pluck pad bass sub lead bell musicbox`; `musicEnvelope(music)` feeds `react()`), `audioEnvelope()` / `bpmEnvelope()` + `react()` (visuals that follow music: `const env = await audioEnvelope('music.mp3', { frameRate })` before `movie.init`, then `keyframes: react(env, { duration, props: { scale: { base: 1, amount: 0.4, band: 'bass', release: 0.2 } } })`; `beats: true` pulses on the beats; `bpmEnvelope(120, …)` when there is no file), `deck()` + `Presenter` from `pixi-effects/present` (a talk as one movie: pages, steps, stops that pause playback, arrow keys / click / swipe to move on: `...deck({ transition: { kind: 'slide', duration: 0.6 }, pages: [{ name, duration, stops, sequences }] })`, `new Presenter(movie, { canvas })` from `pixi-effects/presenter`; G = page overview, P = presenter view with notes and a timer; `pixi-effects render talk.html -o talk.pdf` = a PDF of the pages), `particles()` (bursts, snow, confetti, sparks as seeded layers: `...particles({ count: 90, at: 2, life: [1, 1.8], area: { x: 640, y: 330 }, speed: [120, 420], gravity: 240, fade: { out: 0.6 } })`), `followPath()` (a layer along an SVG path: `keyframes: followPath({ d, duration, orient: true })`), path morphing (`{ shape: 'path', d, morphTo, initial/keyframes: morph 0 → 1 }`) and `stagger()` (delays for a wave of layers: `stagger(letters, { each: 0.05, from: 'center' })`, or `stagger(n, …)` for numbers; GSAP-style `each` / `amount` / `from` / `grid` / `ease`). **Not in the DSL (hand-roll with a JS loop):** other per-frame curves (waves, spirals: sample them into short linear keyframes). (Draw-on strokes `trimEnd`, text that changes over time `set: { text }` / `visibleChars`, shake `wiggle()` and particles are built in.)
|
|
50
50
|
- **Shader layer and warp:** `{ type: 'shader', fragment: 'void mainImage(out vec4 fragColor, in vec2 fragCoord) { … }', uniforms: { speed: 1, tint: '#ff8040' } }` draws a Shadertoy-style formula (GLSL ES 3.00; `iTime` `iResolution` `iFrame` are declared; keyframes move `'uniforms.speed'`; opaque by default; it cannot read the layers behind it; a compile error comes back in your own line numbers). `filters: [{ type: 'warp', kind: 'wave' | 'haze', strength: 6 }]` bends a layer's picture like water or heat.
|
|
51
51
|
- **Light, shadow, fog (2.5D):** a `{ type: 'light', kind: 'ambient', initial: { intensity: 0.2 } }` plus a `spot` / `point` / `parallel` light (`castsShadows: true` on the light AND on the `threeD` layers that cast) shades every `threeD` layer by angle and distance; `lit: false` opts a layer out; camera `fogNear` / `fogFar` / `fogColor` fade what is far. What no light reaches is black (add the ambient light); layers are drawn in distance order (no depth buffer). `check --at` prints how lit each layer is.
|
|
52
52
|
- **Blend modes:** `blendMode` takes 18 CSS names (`add` `screen` `multiply` `overlay` `soft-light` `hard-light` `color-dodge` `color-burn` `darken` `lighten` `difference` `exclusion` `hue` `saturation` `color` `luminosity` `linear-burn`, hyphenated) on any layer: glows and light leaks (`add`, `color-dodge`), colour casts and contrast (`soft-light`, `overlay`), vignettes (`multiply`). It blends with everything below the layer; on a composition each child blends (as one picture: `threeD: true` or a filter); 10+ of the new modes on screen at once warn; it works with every mask.
|
|
@@ -37,7 +37,7 @@ name, at, duration,
|
|
|
37
37
|
initial: { ...props applied before any keyframe },
|
|
38
38
|
keyframes: [ { at, duration, ease, set | to | from | (from + to), repeat?, yoyo?, repeatDelay? } ],
|
|
39
39
|
filters: [ { type: 'chromaKey', keyColor, threshold, smoothing, spill } | { type: 'glow', name?, outerStrength: 3, color: '#ffd166' } /* any named filter, see below */ | { type: 'custom', name, filter: <Pixi Filter> } ],
|
|
40
|
-
mask: <a layer spec> | 'matteName' | { layer: 'matteName'
|
|
40
|
+
mask: <a layer spec> | 'matteName' | { layer: 'matteName' | <a layer spec>, channel: 'alpha' | 'luma', invert: true } | [ …names or { layer: 'name', … } ], // an inline mask with no `at` of its own starts and ends with the layer it masks (its keyframes count from that layer's start); an explicit mask `at` is composition time. A NAME points at a sibling layer (the matte, shared by any number of layers, NOT drawn itself, alive for its own `at`/`duration`); a list is an intersection; subtract = `invert: true`
|
|
41
41
|
filterArea: { x, y, width, height } // in the layer's OWN coordinates; lets blur/glow draw past the layer's bounds
|
|
42
42
|
threeD: true // opt into 2.5D (see below)
|
|
43
43
|
blendMode: 'normal' | 'add' | 'screen' | 'multiply' | 'overlay' | 'soft-light' | 'hard-light' | 'color-dodge' | 'color-burn' | 'darken' | 'lighten' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity' | 'linear-burn' // CSS names (hyphenated); blends with EVERYTHING BELOW the layer; add / screen brighten (glows), multiply darkens, soft-light / overlay for colour casts and contrast, color-dodge for light sources; on a composition the children each blend (to blend it as one picture: threeD: true or a filter); the 14 new ones cost ~2 ms a layer on WebGL (10+ on screen warns)
|
|
@@ -119,7 +119,7 @@ transitions: [{ kind: 'crossfade' | 'wipe' | 'iris' | 'slide' | 'dip' | 'zoom' |
|
|
|
119
119
|
## Presets
|
|
120
120
|
|
|
121
121
|
```js
|
|
122
|
-
import { kenBurns, withFade, wiggle, stagger, followPath, cameraPath, particles,
|
|
122
|
+
import { kenBurns, withFade, wiggle, stagger, followPath, cameraPath, particles, audioEnvelope, bpmEnvelope, musicEnvelope, react, random } from 'pixi-effects';
|
|
123
123
|
kenBurns({ asset, name, at, duration, motion: 'still'|'scale'|'rotation'|'position',
|
|
124
124
|
fit: 'cover'|'contain', ease, /* scale */ origin, zoom, direction, /* rotation */ angle, /* position */ from, to })
|
|
125
125
|
withFade(spec, { in: 0.5, out: 0.5 }) // alpha fade; `out` needs spec.duration
|
|
@@ -135,12 +135,12 @@ keyframes: followPath({ d: 'M 120 560 C 360 120 920 120 1160 560', duration: 4,
|
|
|
135
135
|
// a presentation: stops pause playback (`composition.stops: [2, { at: 5, page: 'Name', notes, advance, pdf }]`; `pdf: true` = the stop that pictures its page in a PDF, `pdf: false` = keep it out; default the page's last stop; a `page` stop begins a page, the others are steps); deck() lays pages out
|
|
136
136
|
await movie.init({ canvas, width, height, frameRate, ...deck({ transition: { kind: 'slide', direction: 'left', duration: 0.6 },
|
|
137
137
|
pages: [{ name: 'Hello', duration: 3.2 /* last stop + transition */, stops: [2.6] /* page-local */, notes, sequences: [ /* page-local time; a step = a layer starting at the previous stop */ ] }] }) });
|
|
138
|
-
new Presenter(movie, { canvas }); // import { Presenter } from 'pixi-effects/
|
|
139
|
-
|
|
138
|
+
new Presenter(movie, { canvas }); // import { deck, Presenter, next, prev, … } from 'pixi-effects/present' (an import-map line of its own): → ↓ Space Enter click = next, ← = back, Home / End, number+Enter, B / W, F, ?
|
|
139
|
+
next(movie) / prev(movie) / goToStop(movie, i) / goToPage(movie, n) / movie.stops / onStop(movie, fn) // (from 'pixi-effects/present'; the core has movie.playUntil(seconds)) Controller: marks on the seek bar, play / Space / click = play to the next stop (pauseAtStops: false = straight through), a Present button, PDF in the download panel
|
|
140
140
|
// Presenter extras: G = page overview (pictures), P = presenter view (a second window: live picture, next picture, notes, timer); start() makes the pictures before the audience sees anything
|
|
141
|
-
await
|
|
142
|
-
await
|
|
143
|
-
await
|
|
141
|
+
await stopImages(movie, { as: 'dataURL', scale: 0.25 }) // [{ stop, page, image }] one per page (its last stop); { which: 'stops' } one per stop (a page's picture honours `pdf` flags)
|
|
142
|
+
await inspectStops(movie) // { stops, issues }: stops whose picture is still changing (a stop landing before its animation ends); the check lists them
|
|
143
|
+
await exportPDF(movie, { title }) // Blob: a PDF page per page of the deck; CLI: pixi-effects render talk.html -o talk.pdf (--all-stops)
|
|
144
144
|
const env = await audioEnvelope('music.mp3', { frameRate: 30 }) // BEFORE movie.init: .series.level|bass|mid|treble (0–1 per frame), .beats [s], .bpm, .at(t, band); bands: { name: [Hz, Hz] }
|
|
145
145
|
const env = bpmEnvelope(120, { duration: 12 }) // no audio file: kick / snare / hats exactly on a tempo
|
|
146
146
|
keyframes: react(env, { duration: 12, props: { scale: { base: 1, amount: 0.4, band: 'bass', attack: 0.02, release: 0.2 }, alpha: { base: 0.4, amount: 0.6, beats: true, decay: 0.15 } }, audioOffset: 0, loop: false })
|
|
@@ -154,7 +154,7 @@ measureText('Hello', style) // → { width, height,
|
|
|
154
154
|
splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
|
|
155
155
|
// → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
|
|
156
156
|
```
|
|
157
|
-
Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same,
|
|
157
|
+
Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave; by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
158
158
|
|
|
159
159
|
## Assets
|
|
160
160
|
|
|
@@ -174,7 +174,7 @@ await movie.contactSheet({ count: 6, as: 'dataURL' }) // ONE image of several
|
|
|
174
174
|
await movie.snapshot(60, { as: 'dataURL' }) // one frame, canvas only (no player bar) — use it for detail; sheet tiles are small
|
|
175
175
|
await movie.inspect(60, { layers: 'none' }) // issues only (default lists the visible layers); text off-canvas / cut off / empty / overlapping text. Name your layers so paths are readable
|
|
176
176
|
await movie.onionSkin({ from: 1, to: 3, count: 8, as: 'dataURL' }) // frames overlaid into ONE picture of a movement (later = stronger): path and easing at a glance
|
|
177
|
-
movie.review({ at: 'title@end' }) // what `pixi-effects
|
|
177
|
+
movie.review({ at: 'title@end' }) // what `pixi-effects check` looks at, as data: { frames, problems (text cut off, off-canvas, no size), review (text overlaps: look at them), fonts, audio, at }; leaves the movie as it was. `strict: true` makes every issue a problem
|
|
178
178
|
movie.resolveAt('3.5, 50%, f120, title@end') // moments → [{ label, frame }]; a mistake says what could not be read
|
|
179
179
|
movie.inspectFonts() // { missing: [{ layer, family }] (no font of the list is installed), failed: [web fonts whose file did not load] }
|
|
180
180
|
movie.inspectAudio() // the soundtrack as numbers; also loudness { integratedLufs, truePeakDb, clippedSamples, silences }, scenes, cues, notes (advice: -14 to -16 LUFS is typical for web video) — — you cannot listen: sources[i] = { layer, start, end, peakDb, sound: { length, peakDb, loudestAt, brightnessHz } } (`peakDb` is the MIX's peak between `start` and `end`, not the layer's own: `sound.peakDb` is the sound's; for a sound inside a remapped composition `start` / `end` span every pass it is heard, forward, backward, replayed, in the movie's seconds), windows (`{ t, rmsDb, peakDb, brightnessHz }` per 0.1 s), issues (limited mix, inaudible / cut-off sounds)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Directing a film (for any AI that makes video with code)
|
|
2
2
|
|
|
3
|
+
> **No shell or files (a browser chat)?** This guide is not for you: follow https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/CHAT.md. You can still plan: put three lines in your reply (the one sentence to remember, the real assets you were given, the beats in order) and ask the person before you build.
|
|
4
|
+
|
|
3
5
|
Read this before you write a composition for anything longer than a title card: a product film, a company introduction, a launch, a data story. It is about the decisions around the renderer, so it names no tool; `SKILL.md` has the pixi-effects commands.
|
|
4
6
|
|
|
5
7
|
A one-line prompt makes the model fill every missing decision with its safest default: large text in the middle, a gradient behind it, everything fading in, the logo last. The fix is not a cleverer prompt. It is to remove the missing decisions before anything is drawn, and to judge what was drawn with evidence.
|
|
@@ -77,21 +77,21 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
77
77
|
47. **Browser tooling: prefer `npx pixi-effects check page.html`** — it runs its own private Chrome, so none of the following can bite. If you drive a browser by hand instead: the `agent-browser` daemon is shared, so a `contactSheet` can come back from another session: after saving an image, look at it and confirm it is YOUR piece. In zsh a variable holding `agent-browser --session x` is not split into words (use a small wrapper script). `inspect` now clips overlaps to the visible area, ignores the two scenes of a running transition, treats text inside a moving (panned) composition as moving, and does not report text scaled to 0 as "no size"; it still does not compare text with picture content or shapes.
|
|
78
78
|
48. **A composition's `alpha` is applied to each child separately, not to the flattened group** (measured: two overlapping opaque circles in a composition at `alpha: 0.5` show the lower one through the upper one in the overlap). Fading a group that has overlapping children therefore shows the overlap; fade the children themselves, or keep overlapping parts out of the group. Expressions have no way to read a sibling's size: measure text with `canvas.measureText` (the font string Pixi uses) and compute pill widths in JS. *(docs)*
|
|
79
79
|
49. **sfx timing and level.** An sfx layer's `duration` is the sound's length (it stretches, it does not trim). Align the loud point: a `riser` peaks at its end, a `swoosh` 0.16 s in. Many sounds at the same instant add up: above 0.9 the mix is limited and a warning names the layers to turn down. Preset names, knobs and voice fields are closed lists — a guess (`whoosh`, `ding`, `impact`, `sawtooth`, `lp`, `decay`) warns with the right word. *(fixed: warns)*
|
|
80
|
-
50. **An image (or video) works as a mask,
|
|
80
|
+
50. **An image (or video) works as a mask, inverted too** *(fixed)*: PIXI hides a sprite mask with `renderable = false` and the lifespan toggles used to switch it back on, so the mask picture was drawn over the layer (a white disc) and an inverted mask looked ignored. Use the picture's alpha (or red channel) as the shape: an opaque word or logo on a transparent canvas image is a text-shaped mask with real glyph edges. A `text` layer also works as a mask (cut to its glyphs; measured with a heavy sans `I`). *(docs / fixed)*
|
|
81
81
|
51. **Arcs and rings are `shape: 'arc'`** (`radius`, `innerRadius`, `startAngle`, `endAngle`, degrees; 0° = 3 o'clock, clockwise). A progress ring is an arc with `startAngle: -90` and an animated `endAngle` (−90 → 270); `strokeCap: 'round'` rounds BOTH ends. Fill it for a pie slice, add `innerRadius` for a donut slice (a 120-polygon donut is no longer needed). Style (`strokeColor`, `strokeWidth`, `fillColor`) goes in `initial`; `strokeCap` / `strokeJoin` may be top level. *(fixed)*
|
|
82
82
|
52. **Measure and split text with `measureText` / `splitText`, not by hand.** `splitText(text, style, { by, x, y, align })` returns each piece's x / y / width in the renderer's own font metrics (kerning kept; verified: a whole line vs the same line in per-letter layers differs only by anti-aliasing). The pieces are for text layers with the default top-left anchor and the same `style`. Load a web font before measuring (`await document.fonts.load('24px "Name"')`) or the fallback font's widths are used. Size a pill from `measureText(...).width + padding` instead of `0.58 × fontSize × length`. *(fixed)*
|
|
83
83
|
53. **Draw-on and typewriter need no masks or per-letter layers.** `trimStart` / `trimEnd` (0–1 of the outline, animatable, every shape but `arc`) draw a stroke on: `trimEnd: 0` plus a keyframe `to: { trimEnd: 1 }`. The fill is not trimmed, so fade `fillAlpha` in afterwards; use `strokeCap: 'round'`. Text changes over time with `set: { text }` (swap at a time) and `visibleChars` (typewriter; left-align it so it grows rightward). A `to: { text }` cannot tween a string and warns. *(fixed)*
|
|
84
84
|
54. **A deck page lasts until its last stop plus the transition.** `deck()` lays pages out one after another in page-local time and a `transition` overlaps them: a page shorter than its last stop (or shorter than the transition) loses its final step or flashes. A step is a layer that starts at the previous stop (`at: <previous stop>`), so the thing that appears on a press is fully visible when the movie stops. The transition option is `kind`, not `type`. If the last stop is mid-exit on paper, flag the stop you want in the PDF: `{ at, pdf: true }` (in `deck()`: `stops: [1.2, { at: 3, pdf: true }]`), and `pdf: false` keeps one out.
|
|
85
|
-
55. **Presenter and Controller both listen to click and keys.** Use the `Presenter` for a talk (or let the Controller's Present button start one); do not run both on the same canvas yourself. `movie.play()` ignores stops, but the Controller's play button / Space / click-on-picture play to the next stop and pause (`pauseAtStops: false` plays through); `
|
|
85
|
+
55. **Presenter and Controller both listen to click and keys.** Use the `Presenter` for a talk (or let the Controller's Present button start one); do not run both on the same canvas yourself. `movie.play()` ignores stops, but the Controller's play button / Space / click-on-picture play to the next stop and pause (`pauseAtStops: false` plays through); `next(movie)` (from `pixi-effects/present`) plays to the next stop (a second press skips to it), `prev(movie)` jumps back (nothing plays in reverse).
|
|
86
86
|
56. **Analyse audio before `init`, and give it the movie's frame rate.** `const env = await audioEnvelope('music.mp3', { frameRate })` is async; `react()` is pure and bakes the keyframes, so there is no per-frame callback. A custom `bands` list replaces the defaults (include `bass` if you want it; beats are picked there). No file yet: `bpmEnvelope(120, { duration })` is a stand-in with a kick on every beat. Offset the music with `audioOffset` when the audio layer does not start at 0.
|
|
87
|
-
57. **Review a deck by its stops, not by its timeline.** `pixi-effects
|
|
87
|
+
57. **Review a deck by its stops, not by its timeline.** `pixi-effects check` writes `stops.png` (one picture per stop, in order) for a page with `stops`; the picture at a stop is what the audience reads while it waits. A stop that lands a few frames before an animation ends shows it half-finished (also in the PDF): `inspectStops(movie)` / the check's review list names such stops; give an animation a little more time than its step (e.g. the next stop 0.3 s after it ends).
|
|
88
88
|
58. **`music` counts BEATS, and every track has to add up.** A token's `:n` is beats (a quarter note), a 4/4 bar is 4: a bar of `c4:1 e4:1 g4:1` is a beat short and every later bar drifts. Keep all tracks the same length (the music lasts the longest; a track of rests `'_:16'` sets a length) and check the printed length with `movie.inspectAudio()`. 6/8 is a bar of 3 beats (dotted quarter `:1.5`, eighth `:0.5`) with `grid: 2`; `grid: 3` is triplet eighths in 4/4.
|
|
89
89
|
59. **A groove that must enter late needs `from`, not a long string.** `drums: [{ kick: 'x...' }, { from: 8, snare: '....x...' }]`. Patterns repeat; a drum object is a loop of its longest string. A `crash` on the last bar: its own section `{ from: 28, crash: 'x' }`.
|
|
90
90
|
60. **Build-ups are layers and `vol` points, not louder notes.** A track's `vol` can be `[[0, 0], [8, 0.7]]` (a fade over 8 beats); the pad's `attack` can be 2–3 s; enter instruments with rests. Per-note `!` and `,` only accent and soften.
|
|
91
91
|
61. **Music is a bed: `volume` 0.5–0.8 under speech or sfx.** At `volume: 1` it peaks at −6 dBFS (an sfx peaks at −12). A layer shorter than the music cuts it with a fade; longer is silent after it unless `loop: true`. Do not also add an mp3 bed.
|
|
92
92
|
62. **Chord names are voiced close around octave 3**: use `[c3 g3 e4]` for an exact voicing or `Am7/e` for a bass note; `swing` only moves notes that start on an off-beat eighth (`.5`) or sixteenth (`.75`); the bell and music box ring for `ring` seconds whatever the written length (shorten `ring` for fast bell lines); the synth is a handy bed, not an orchestra: keep tracks to 3–6, vary the bars, and end on the tonic.
|
|
93
93
|
63. **`animateText()` / `splitText()`: `x` and `y` are pixel numbers, and `y` is the TOP of the line.** An expression such as `x: 'GW/2'` throws (the letters are measured once, at build time): write `x: W / 2`. The pieces are centred half a line below `y`, so a line drawn under the text goes at `y + lineHeight` (about 1.2 × `fontSize`), not at `y`; look at the contact sheet, the layout check cannot see an underline through a word. (`check` writes into `check-out/` of the current folder unless you pass `--out`.)
|
|
94
|
-
64. **`animateText()`: `out
|
|
94
|
+
64. **`animateText()`: `out: { preset, at: T }` starts the exit at composition second T; without `at` the exit is the end of the layer run backwards.** `duration` is still how long until the layer ends (every piece is gone by `at + duration`): with `out.at`, give `duration` room for the exit and the wave after T (a warning says `out.at ≤ …` when it does not fit, or when T is before the entrance has finished). Without `out.at`, to be gone at time T set `duration: T − at` (title at 0.4 s that must be gone by 8.0 s: `duration: 7.6`). A hand-written exit keyframe works the other way: a negative `at` counts back from the end of its own layer, a positive one from the start of its own layer, never from the movie.
|
|
95
95
|
65. **A spring that does not spring, and `'auto'` that is not a spring.** `spring(1,170,26)` is critically damped (damping ratio 1): it never overshoots; at stiffness 170 use damping below 20 (`spring(1, 170, 12)`), or pick a preset (`spring.bouncy`, `spring.wobbly`). With a number `duration`, that number is the settle time (mass and stiffness only matter with `duration: 'auto'`). `duration: 'auto'` needs a spring ease (otherwise it warns and uses 0.5 s) and is for keyframes, not for `animateText` / `orbit` tweens. A colour that overshoots is clamped in its colour space; `alpha` past 1 shows nothing.
|
|
96
96
|
66. **Animating a gradient: one `fillGradient` object, partial, in the keyframe.** `to: { fillGradient: { angle: 360, stops: [...] } }` (keys you leave out stay). Not `'fillGradient.angle'`, not `gradientAngle`, not `fillGradient: 'red'` (each warns once with what to write). The layer must start with a `fillGradient`; `stops` keep their count; `type` cannot change. A fade between two colours in the default `rgb` passes through grey (green → magenta): set `colorSpace: 'oklch'`. Under a mask the stops still span the whole shape (see 39).
|
|
97
97
|
67. **A key the layer does not have is warned about now (it used to be ignored in silence).** `{ type: 'text', anchorX: 0.5 }` (anchors go in `initial`), `{ type: 'audio', sfx: 'click', pitch: 3 }` (the knob goes inside: `sfx: { preset: 'click', pitch: 3 }`), `{ type: 'shape', shape: 'rect', radius: 5 }` (that is a circle's key; a rect has `cornerRadius`), a mistyped property in a keyframe (`alhpa`), a mistyped text `style` key. The warning names the key, the kind of layer and what was probably meant; fix the key, do not silence the warning. Three gallery pieces had the `pitch` mistake for months: their sound effects were never pitched.
|
|
@@ -106,12 +106,12 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
106
106
|
76. **`aperture` is the lens diameter in px, not a 0–1 strength.** 30 is the default (clearly shallow), 0 turns it off, 60 is strong, 100 is extreme (small bright objects speckle); the blur never exceeds 32 px. 20 or more blurred layers at once warns (one filter pass each, more on WebGPU).
|
|
107
107
|
77. **A dolly does not need the camera's home distance typed in:** keep `z` automatic and animate `offsetZ` (negative = closer). Writing `z` yourself means typing `H / 2 / tan(fov / 2)` (about 989 at 720p, fov 40; as an expression `H / 2 / tan(20 * PI / 180)`). A bokeh field is a few big cards, not dozens of layers: a card at depth `z` fills the frame at size `(W / s) × (H / s)`, `s = camZ / (camZ − z)`.
|
|
108
108
|
78. **A blend mode mixes the layer with everything BELOW it, in the order of the array.** The backdrop goes first, the blended layer after it; a layer placed before its backdrop blends with nothing. Names are CSS names with the hyphen: `soft-light`, not `softLight` (the warning says which you meant).
|
|
109
|
-
79. **On a plain `composition` a `blendMode` makes each child blend by itself, not the composition as one picture.** To blend the whole group give the composition `threeD: true` (z 0, default camera: nothing else changes) or a filter. A text, image or video layer as the (not inverted) `mask` cannot carry a blend (it warns, draws normal); a shape mask and
|
|
109
|
+
79. **On a plain `composition` a `blendMode` makes each child blend by itself, not the composition as one picture.** To blend the whole group give the composition `threeD: true` (z 0, default camera: nothing else changes) or a filter. A text, image or video layer as the (not inverted) `mask` cannot carry a blend (it warns, draws normal); a shape mask and an inverted mask work.
|
|
110
110
|
80. **`soft-light` / `overlay` do almost nothing over near-black or near-white; `color-dodge` over dark gives a saturated colour, not white; `blendMode` cannot be keyframed** (use a second short-lived layer). **The 14 new modes cost about 2 ms a layer on WebGL; 10 or more on screen at once warns.** A glow, a haze or a colour cast is ONE layer with a gradient (`fillGradient`), not dozens of circles; keep `add` / `screen` / `multiply` for big piles of particles.
|
|
111
111
|
81. **A layer named in `mask` is a matte: it is not drawn, and it must be alive whenever the layers it cuts are.** `{ name: 'band', … }` plus `mask: 'band'` on the text hides the band itself; the matte's time is the composition's time (its own `at` / `duration`). Where the matte is missing the layers it cuts are invisible: the build warns when its lifespan does not cover its users. To show the band as well, add a second layer that looks the same.
|
|
112
112
|
82. **A `mask` name works inside one composition only.** The matte and the layers that name it are siblings in the same `sequences`. A layer inside a nested composition cannot name a matte outside it (warns, with the closest name if there is one, and the layer is drawn without the matte); a layer that a transition wraps keeps its matte. Put the matte inside the nested composition, or the layer next to it.
|
|
113
113
|
83. **`channel: 'luma'` reads brightness, `'alpha'` (the default) reads opacity.** A white shape is 1 for both, a black shape is 0 for luma and 1 for alpha, a grey `#808080` is 0.5 luma. Outside the matte layer's own shape both are 0, so a wide gradient rectangle slid across the layers is a soft moving reveal. Use `luma` with a grayscale image or a gradient layer; use the default for shapes and text. `luminance`, `brightness` and `grayscale` are not channel names: they warn ("did you mean luma?") and alpha is used.
|
|
114
|
-
84. **Subtract with `invert: true` in a list; a list multiplies.** `mask: ['panel', { layer: 'hole', invert: true }]` is "inside `panel`, outside `hole`".
|
|
114
|
+
84. **Subtract with `invert: true` in a list; a list multiplies.** `mask: ['panel', { layer: 'hole', invert: true }]` is "inside `panel`, outside `hole`". A mask written in place inverts the same way: `mask: { layer: { type: 'shape', … }, invert: true }` (the old `maskInverted: true` key is gone: it warns and says this). A luma wipe's `map` is a built-in name (`linear` `diagonal` `radial`) or a grayscale image asset; an unknown name warns once and wipes as if the map were white.
|
|
115
115
|
85. **One light layer lights every `threeD` layer, and what no light reaches is black.** Add `{ type: 'light', kind: 'ambient', initial: { intensity: 0.2 } }` (a white ambient light at 1 is the same as no light). A layer you want flat-lit is `lit: false`. Layers without `threeD` are never lit, and a light only counts while it is alive (give it the scene's `at` / `duration`): with no light alive the layers are drawn unlit.
|
|
116
116
|
86. **A shadow needs `castsShadows: true` on the light AND on the layer.** Either alone shows nothing (the build says which half is missing). The caster has to be a `threeD` layer in front of the receiver (between it and the light); a receiver takes at most 4 casters. A hard shadow can look like a dark slab: `shadowDiffusion: 10`–`20` softens it, and soft shadows cost the most, so use them on one or two lights. A spot's pool on the wall has radius about `distance × tan(coneAngle / 2)` and can hide behind the card that stands in front of it.
|
|
117
117
|
87. **The light's numbers go in `initial` / `keyframes`; `kind`, `falloff` and `castsShadows` are on the layer.** `{ type: 'light', intensity: 1 }` warns; so do `target` / `pointOfInterest` (use `lookAtX / lookAtY / lookAtZ`), `angle` (`coneAngle`), `castShadows` (`castsShadows`) and `intensity: 80` (it is a multiplier, 1 = full, not a percentage). A spot needs a look-at point (it defaults to the middle of the picture).
|
|
@@ -658,8 +658,8 @@ return { duration: d.duration, sequences: d.composition.sequences, transitions:
|
|
|
658
658
|
```js
|
|
659
659
|
// @docs-only
|
|
660
660
|
// in the page: the deck is spread into init; the Presenter (or the Controller, which then shows the stops and a Present button) plays it
|
|
661
|
-
import { Movie
|
|
662
|
-
import { Presenter } from 'pixi-effects/
|
|
661
|
+
import { Movie } from 'pixi-effects';
|
|
662
|
+
import { deck, Presenter } from 'pixi-effects/present'; // (add "pixi-effects/present" to the import map)
|
|
663
663
|
const movie = new Movie();
|
|
664
664
|
await movie.init({ canvas, width: 1280, height: 720, frameRate: 30, ...deck({ /* as above */ }) });
|
|
665
665
|
new Presenter(movie, { canvas }); // arrows / Space / Enter / click / swipe = next, ← = back, B = black screen, F = fullscreen, ? = keys
|
|
@@ -1066,7 +1066,7 @@ three({
|
|
|
1066
1066
|
|
|
1067
1067
|
## Subtitles from an SRT file
|
|
1068
1068
|
|
|
1069
|
-
Subtitles are just text layers. The function reads a `.srt` (numbers, `00:00:01,000 --> 00:00:03,500`, the words) and returns one layer per cue: it appears and disappears over 0.12 s, sits 90 px above the bottom edge, wraps at 1000 px (about 80 % of a 1280 px picture) and has a black outline so it reads on any picture. Two cues that overlap in time are drawn on top of each other: split a long line into two lines in the `.srt` rather than relying on wrapping. Check the result with `pixi-effects
|
|
1069
|
+
Subtitles are just text layers. The function reads a `.srt` (numbers, `00:00:01,000 --> 00:00:03,500`, the words) and returns one layer per cue: it appears and disappears over 0.12 s, sits 90 px above the bottom edge, wraps at 1000 px (about 80 % of a 1280 px picture) and has a black outline so it reads on any picture. Two cues that overlap in time are drawn on top of each other: split a long line into two lines in the `.srt` rather than relying on wrapping. Check the result with `pixi-effects check`; it reports text that is cut off by the edge.
|
|
1070
1070
|
|
|
1071
1071
|
```js
|
|
1072
1072
|
// @recipe subtitles
|
|
@@ -1130,7 +1130,7 @@ const scene = { type: 'composition', name: 'hook', at: 0, duration: 5.5, width:
|
|
|
1130
1130
|
|
|
1131
1131
|
## A batch of videos from a table (one video per row)
|
|
1132
1132
|
|
|
1133
|
-
A batch of videos is a loop outside the video. The page reads what changes from its address (`new URLSearchParams(location.search)`), and `pixi-effects
|
|
1133
|
+
A batch of videos is a loop outside the video. The page reads what changes from its address (`new URLSearchParams(location.search)`), and `pixi-effects render --query "name=Aiko&score=92"` opens it with those values. Names with spaces or non-Latin letters must be URL-encoded (`jq -sRr @uri` as below, or `encodeURIComponent` in Node). Each video starts its own browser, so N videos cost N start-ups: try one with `--draft` (half size, fast), then render the real ones. To make the pieces of one row differ in more than text, use the same values to pick colours or an image name.
|
|
1134
1134
|
|
|
1135
1135
|
```js
|
|
1136
1136
|
// @docs-only batch-page
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>pixi-effects video</title>
|
|
7
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/loader.css" /> <!-- the loader: HTML + CSS only, so it is on screen at once and keeps moving while the page builds the video -->
|
|
8
8
|
<style>
|
|
9
9
|
body { background: #0d1220; margin: 0; padding: 16px; }
|
|
10
10
|
.stage { position: relative; width: min(960px, 100%); } /* the player and the loader both sit inside this box */
|
|
@@ -37,23 +37,23 @@
|
|
|
37
37
|
"imports": {
|
|
38
38
|
"pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
|
|
39
39
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
40
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
41
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
42
|
-
"pixi-effects/
|
|
43
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
40
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/index.js",
|
|
41
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/Controller.js",
|
|
42
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/present.js",
|
|
43
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/three.js",
|
|
44
44
|
"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
|
|
45
45
|
"three": "https://esm.sh/three@0.178.0"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
</script>
|
|
49
|
-
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
49
|
+
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/index.js" /> <!-- after the import map: the library downloads while the page is parsed -->
|
|
50
50
|
|
|
51
51
|
<script type="module">
|
|
52
52
|
import { Movie, kenBurns, withFade } from 'pixi-effects';
|
|
53
53
|
import { Controller } from 'pixi-effects/controller';
|
|
54
54
|
// import { registerThree, three } from 'pixi-effects/three'; // only for a three.js layer; call registerThree() before init
|
|
55
55
|
// import * as THREE from 'three';
|
|
56
|
-
// A talk instead of a video: import { deck } from 'pixi-effects'; import
|
|
56
|
+
// A talk instead of a video: import { deck, Presenter } from 'pixi-effects/present'; (the import map above has "pixi-effects/present")
|
|
57
57
|
// const d = deck({ transition: { kind: 'slide', duration: 0.6 }, pages: [{ name, duration, stops: [at, ...], notes, sequences }] });
|
|
58
58
|
// composition: d.composition, duration: d.duration; then new Presenter(movie, { canvas }) instead of the Controller. Docs: reference/cheatsheet.md → Presets (the deck lines).
|
|
59
59
|
|
package/dist/Base-C6f0fE8j.d.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape, ac as Timeline, ad as AudioDescriptor } from './types-DpK1vwcl.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolves a routed keyframe path to a concrete tween target.
|
|
6
|
-
* `path` is everything after the registered prefix + dot (for
|
|
7
|
-
* `three.cube.rotation.y` under prefix `three`, path = `cube.rotation.y`).
|
|
8
|
-
* Return null to skip the key (the router is expected to have warned).
|
|
9
|
-
*/
|
|
10
|
-
type PathRouter = (path: string) => {
|
|
11
|
-
target: object;
|
|
12
|
-
prop: string;
|
|
13
|
-
} | null;
|
|
14
|
-
type PathRouters = Record<string, PathRouter>;
|
|
15
|
-
|
|
16
|
-
/** Variables available to expression evaluation. */
|
|
17
|
-
interface Scope {
|
|
18
|
-
w: number;
|
|
19
|
-
h: number;
|
|
20
|
-
W: number;
|
|
21
|
-
H: number;
|
|
22
|
-
GW: number;
|
|
23
|
-
GH: number;
|
|
24
|
-
contain: number;
|
|
25
|
-
cover: number;
|
|
26
|
-
t: number;
|
|
27
|
-
d: number;
|
|
28
|
-
T: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Internal marker we set on filter instances so partition path resolution finds them. */
|
|
32
|
-
interface NamedFilter extends Filter {
|
|
33
|
-
_name?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The slice of a Pixi renderer Layer3D needs (also what tests fake). */
|
|
37
|
-
interface SpaceHost {
|
|
38
|
-
render(options: {
|
|
39
|
-
container: Container;
|
|
40
|
-
target: RenderTexture;
|
|
41
|
-
transform: Matrix;
|
|
42
|
-
clear: boolean;
|
|
43
|
-
clearColor: number[];
|
|
44
|
-
}): void;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
declare abstract class Sequence {
|
|
48
|
-
spec: SequenceSpec;
|
|
49
|
-
parent: CompositionShape | null;
|
|
50
|
-
root: CompositionShape;
|
|
51
|
-
target: Container | null;
|
|
52
|
-
intrinsicWidth: number;
|
|
53
|
-
intrinsicHeight: number;
|
|
54
|
-
at: number;
|
|
55
|
-
duration: number | undefined;
|
|
56
|
-
filters: NamedFilter[];
|
|
57
|
-
/**
|
|
58
|
-
* Mask sequence built from `spec.mask`, if any. Built and added to the
|
|
59
|
-
* scene graph by the parent CompositionSequence; rendered as a mask
|
|
60
|
-
* (not a normal child) by PIXI when `target.mask = maskSequence.target`.
|
|
61
|
-
*/
|
|
62
|
-
maskSequence: Sequence | null;
|
|
63
|
-
/**
|
|
64
|
-
* Set by the parent composition on a sprite-based (image / video) mask. PIXI hides such a mask with
|
|
65
|
-
* `renderable = false`; the lifespan toggles must not switch it back on, or the mask is drawn as a picture.
|
|
66
|
-
*/
|
|
67
|
-
keepHidden: boolean;
|
|
68
|
-
/**
|
|
69
|
-
* Absolute start time on the global timeline (offset + at), recorded by
|
|
70
|
-
* bindTimeline. Movie._awaitVideoFrames uses it to determine the correct
|
|
71
|
-
* time to pass to awaitFrameAt. Subclasses that override bindTimeline
|
|
72
|
-
* without calling super must set this field themselves. Null until bound.
|
|
73
|
-
*/
|
|
74
|
-
absoluteStart: number | null;
|
|
75
|
-
constructor(spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape);
|
|
76
|
-
abstract build(): Promise<void>;
|
|
77
|
-
buildFilters(): void;
|
|
78
|
-
scope(): Scope;
|
|
79
|
-
/**
|
|
80
|
-
* Prefix routers contributed to the keyframe pipeline (e.g. `three.` in
|
|
81
|
-
* pixi-effects/three). Base sequences route nothing extra.
|
|
82
|
-
*/
|
|
83
|
-
protected pathRouters(): PathRouters;
|
|
84
|
-
/**
|
|
85
|
-
* The `initial` and `keyframes` that go onto the display object. A layer whose `time` runs a clock (video, composition) takes `time`
|
|
86
|
-
* out of them: it is not a property of the display object.
|
|
87
|
-
*/
|
|
88
|
-
protected displayProps(): {
|
|
89
|
-
initial: SequenceSpec['initial'];
|
|
90
|
-
keyframes: SequenceSpec['keyframes'];
|
|
91
|
-
};
|
|
92
|
-
bindTimeline(timeline: Timeline, offset?: number): void;
|
|
93
|
-
collectAudio(_out: AudioDescriptor[], _baseTime: number): void;
|
|
94
|
-
/**
|
|
95
|
-
* 2.5D hook. Runs once per rendered frame, after the timeline seek and the
|
|
96
|
-
* per-frame media sync, before the render. Compositions override it to
|
|
97
|
-
* project their `threeD` layers; everything else has nothing to do.
|
|
98
|
-
*/
|
|
99
|
-
updateSpace(_t: number, _host: SpaceHost): void;
|
|
100
|
-
/**
|
|
101
|
-
* Runs once per rendered frame, after the timeline seek and before the cull. Layers whose drawn
|
|
102
|
-
* geometry follows tweened state (shapes) redraw here, so the culler measures where the layer is NOW
|
|
103
|
-
* and not where it was drawn at the previous render.
|
|
104
|
-
*/
|
|
105
|
-
syncFrame(): void;
|
|
106
|
-
destroy(): void;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export { type PathRouters as P, Sequence as S };
|
package/dist/Base-DI1FCGcH.d.cts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape, ac as Timeline, ad as AudioDescriptor } from './types-DpK1vwcl.cjs';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolves a routed keyframe path to a concrete tween target.
|
|
6
|
-
* `path` is everything after the registered prefix + dot (for
|
|
7
|
-
* `three.cube.rotation.y` under prefix `three`, path = `cube.rotation.y`).
|
|
8
|
-
* Return null to skip the key (the router is expected to have warned).
|
|
9
|
-
*/
|
|
10
|
-
type PathRouter = (path: string) => {
|
|
11
|
-
target: object;
|
|
12
|
-
prop: string;
|
|
13
|
-
} | null;
|
|
14
|
-
type PathRouters = Record<string, PathRouter>;
|
|
15
|
-
|
|
16
|
-
/** Variables available to expression evaluation. */
|
|
17
|
-
interface Scope {
|
|
18
|
-
w: number;
|
|
19
|
-
h: number;
|
|
20
|
-
W: number;
|
|
21
|
-
H: number;
|
|
22
|
-
GW: number;
|
|
23
|
-
GH: number;
|
|
24
|
-
contain: number;
|
|
25
|
-
cover: number;
|
|
26
|
-
t: number;
|
|
27
|
-
d: number;
|
|
28
|
-
T: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Internal marker we set on filter instances so partition path resolution finds them. */
|
|
32
|
-
interface NamedFilter extends Filter {
|
|
33
|
-
_name?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The slice of a Pixi renderer Layer3D needs (also what tests fake). */
|
|
37
|
-
interface SpaceHost {
|
|
38
|
-
render(options: {
|
|
39
|
-
container: Container;
|
|
40
|
-
target: RenderTexture;
|
|
41
|
-
transform: Matrix;
|
|
42
|
-
clear: boolean;
|
|
43
|
-
clearColor: number[];
|
|
44
|
-
}): void;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
declare abstract class Sequence {
|
|
48
|
-
spec: SequenceSpec;
|
|
49
|
-
parent: CompositionShape | null;
|
|
50
|
-
root: CompositionShape;
|
|
51
|
-
target: Container | null;
|
|
52
|
-
intrinsicWidth: number;
|
|
53
|
-
intrinsicHeight: number;
|
|
54
|
-
at: number;
|
|
55
|
-
duration: number | undefined;
|
|
56
|
-
filters: NamedFilter[];
|
|
57
|
-
/**
|
|
58
|
-
* Mask sequence built from `spec.mask`, if any. Built and added to the
|
|
59
|
-
* scene graph by the parent CompositionSequence; rendered as a mask
|
|
60
|
-
* (not a normal child) by PIXI when `target.mask = maskSequence.target`.
|
|
61
|
-
*/
|
|
62
|
-
maskSequence: Sequence | null;
|
|
63
|
-
/**
|
|
64
|
-
* Set by the parent composition on a sprite-based (image / video) mask. PIXI hides such a mask with
|
|
65
|
-
* `renderable = false`; the lifespan toggles must not switch it back on, or the mask is drawn as a picture.
|
|
66
|
-
*/
|
|
67
|
-
keepHidden: boolean;
|
|
68
|
-
/**
|
|
69
|
-
* Absolute start time on the global timeline (offset + at), recorded by
|
|
70
|
-
* bindTimeline. Movie._awaitVideoFrames uses it to determine the correct
|
|
71
|
-
* time to pass to awaitFrameAt. Subclasses that override bindTimeline
|
|
72
|
-
* without calling super must set this field themselves. Null until bound.
|
|
73
|
-
*/
|
|
74
|
-
absoluteStart: number | null;
|
|
75
|
-
constructor(spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape);
|
|
76
|
-
abstract build(): Promise<void>;
|
|
77
|
-
buildFilters(): void;
|
|
78
|
-
scope(): Scope;
|
|
79
|
-
/**
|
|
80
|
-
* Prefix routers contributed to the keyframe pipeline (e.g. `three.` in
|
|
81
|
-
* pixi-effects/three). Base sequences route nothing extra.
|
|
82
|
-
*/
|
|
83
|
-
protected pathRouters(): PathRouters;
|
|
84
|
-
/**
|
|
85
|
-
* The `initial` and `keyframes` that go onto the display object. A layer whose `time` runs a clock (video, composition) takes `time`
|
|
86
|
-
* out of them: it is not a property of the display object.
|
|
87
|
-
*/
|
|
88
|
-
protected displayProps(): {
|
|
89
|
-
initial: SequenceSpec['initial'];
|
|
90
|
-
keyframes: SequenceSpec['keyframes'];
|
|
91
|
-
};
|
|
92
|
-
bindTimeline(timeline: Timeline, offset?: number): void;
|
|
93
|
-
collectAudio(_out: AudioDescriptor[], _baseTime: number): void;
|
|
94
|
-
/**
|
|
95
|
-
* 2.5D hook. Runs once per rendered frame, after the timeline seek and the
|
|
96
|
-
* per-frame media sync, before the render. Compositions override it to
|
|
97
|
-
* project their `threeD` layers; everything else has nothing to do.
|
|
98
|
-
*/
|
|
99
|
-
updateSpace(_t: number, _host: SpaceHost): void;
|
|
100
|
-
/**
|
|
101
|
-
* Runs once per rendered frame, after the timeline seek and before the cull. Layers whose drawn
|
|
102
|
-
* geometry follows tweened state (shapes) redraw here, so the culler measures where the layer is NOW
|
|
103
|
-
* and not where it was drawn at the previous render.
|
|
104
|
-
*/
|
|
105
|
-
syncFrame(): void;
|
|
106
|
-
destroy(): void;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export { type PathRouters as P, Sequence as S };
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { CompositionSequence } from './chunk-5MZWXVTT.js';
|
|
2
|
-
import './chunk-7VN2KI3U.js';
|
|
3
|
-
import './chunk-TYWFKCPY.js';
|
|
4
|
-
import './chunk-45QKLD6M.js';
|
|
5
|
-
import './chunk-BXOCEZ4U.js';
|
|
6
|
-
//# sourceMappingURL=Composition-H7UNR5F5.js.map
|
|
7
|
-
//# sourceMappingURL=Composition-H7UNR5F5.js.map
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunkKBH33IQX_cjs = require('./chunk-KBH33IQX.cjs');
|
|
4
|
-
require('./chunk-MJW7KILM.cjs');
|
|
5
|
-
require('./chunk-P4GY3Z4G.cjs');
|
|
6
|
-
require('./chunk-ZMAD3KJU.cjs');
|
|
7
|
-
require('./chunk-TWZNOUM7.cjs');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Object.defineProperty(exports, "CompositionSequence", {
|
|
12
|
-
enumerable: true,
|
|
13
|
-
get: function () { return chunkKBH33IQX_cjs.CompositionSequence; }
|
|
14
|
-
});
|
|
15
|
-
//# sourceMappingURL=Composition-K6B3APM5.cjs.map
|
|
16
|
-
//# sourceMappingURL=Composition-K6B3APM5.cjs.map
|