pixi-effects 0.3.0 → 0.5.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 +53 -0
- package/README.md +10 -6
- package/ai/SKILL.md +9 -4
- package/ai/reference/cheatsheet.md +19 -7
- package/ai/reference/pitfalls.md +32 -7
- package/ai/reference/recipes.md +117 -0
- package/ai/template.html +6 -3
- package/ai/tools/check.mjs +385 -0
- package/dist/{Base-BDw6dKRB.d.cts → Base-CPSFBkPm.d.ts} +12 -1
- package/dist/{Base-Ba4Ta4ap.d.ts → Base-DnRAxroQ.d.cts} +12 -1
- package/dist/Composition-63EXHNJJ.cjs +13 -0
- package/dist/{Composition-6FDH5OPM.cjs.map → Composition-63EXHNJJ.cjs.map} +1 -1
- package/dist/Composition-TUCRSMJU.js +4 -0
- package/dist/{Composition-FEYAUMFI.js.map → Composition-TUCRSMJU.js.map} +1 -1
- package/dist/Controller.cjs +37 -13
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +4 -2
- package/dist/Controller.d.ts +4 -2
- package/dist/Controller.js +37 -13
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-W8ISiEBB.d.cts → Movie-CuU_aaCm.d.cts} +56 -2
- package/dist/{Movie-DMpaT67V.d.ts → Movie-VyDiUYOj.d.ts} +56 -2
- package/dist/{chunk-ZL262ZRM.cjs → chunk-23M5QTWW.cjs} +70 -20
- package/dist/chunk-23M5QTWW.cjs.map +1 -0
- package/dist/{chunk-SIRVULFF.cjs → chunk-2RR43OIN.cjs} +256 -60
- package/dist/chunk-2RR43OIN.cjs.map +1 -0
- package/dist/{chunk-DIJG2RSF.js → chunk-5Q5XZG7Z.js} +59 -9
- package/dist/chunk-5Q5XZG7Z.js.map +1 -0
- package/dist/chunk-DKGG7WD4.js +61 -0
- package/dist/chunk-DKGG7WD4.js.map +1 -0
- package/dist/{chunk-PN5A6QA7.js → chunk-UBPZLGLU.js} +255 -61
- package/dist/chunk-UBPZLGLU.js.map +1 -0
- package/dist/chunk-UQBGW5DO.cjs +64 -0
- package/dist/chunk-UQBGW5DO.cjs.map +1 -0
- package/dist/index.cjs +336 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -5
- package/dist/index.d.ts +54 -5
- package/dist/index.js +320 -75
- package/dist/index.js.map +1 -1
- package/dist/sfx-MZJZXYY3.cjs +446 -0
- package/dist/sfx-MZJZXYY3.cjs.map +1 -0
- package/dist/sfx-S37DDC66.js +434 -0
- package/dist/sfx-S37DDC66.js.map +1 -0
- package/dist/three.cjs +5 -5
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +1 -1
- package/dist/{types-3c8Vymgw.d.ts → types-7VtTEKUW.d.cts} +102 -6
- package/dist/{types-3c8Vymgw.d.cts → types-7VtTEKUW.d.ts} +102 -6
- package/docs/api.md +36 -0
- package/docs/dsl.md +56 -4
- package/llms-full.txt +268 -21
- package/llms.txt +1 -1
- package/package.json +3 -1
- package/dist/Composition-6FDH5OPM.cjs +0 -13
- package/dist/Composition-FEYAUMFI.js +0 -4
- package/dist/chunk-DIJG2RSF.js.map +0 -1
- package/dist/chunk-PN5A6QA7.js.map +0 -1
- package/dist/chunk-SIRVULFF.cjs.map +0 -1
- package/dist/chunk-ZL262ZRM.cjs.map +0 -1
package/llms-full.txt
CHANGED
|
@@ -14,11 +14,13 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
14
14
|
|
|
15
15
|
1. **Start from `template.html`** (copy it; it loads the library from a CDN, collects every console warning into `window.__logs`, and exposes `window.movie`). One HTML file, plain JS. Default canvas 1280×720 @ 30 fps.
|
|
16
16
|
2. **Write the composition as data.** Anything repeated (letters, bars, particles, scenes) is a JS function or loop that returns specs — never copy-paste objects. Derive coordinates and times from constants/data, not magic numbers.
|
|
17
|
-
3. **Run
|
|
17
|
+
3. **Run the check — one command does steps 3, 4 and the export:** `node ai/tools/check.mjs my-video.html` (Node ≥ 22 and Chrome installed; no npm install; in an installed package it is `npx pixi-effects-check my-video.html`). It starts its own private Chrome (nothing is shared with other sessions), waits for `window.__ready`, and prints: console warnings, `movie.inspect` over the WHOLE timeline (grouped), `movie.inspectAudio()`, then exports an mp4 and decodes it again (video size and length, audio length, loudness per second, silence). It writes `check-out/<name>/sheet.png` — **open that picture and look at it** — plus the mp4 and `report.json`. Exit 0 = nothing to fix; exit 1 = a list of problems; fix them and run it again. Text that overlaps text is listed under `review` (not a failure: ghost layers, glow copies and per-letter boxes are often intentional; look at the sheet; `--strict` makes them fail). The manual steps below are for looking at one frame closely.
|
|
18
|
+
**Or by hand: run it and read the logs first.** `window.__ready` must be `true` and `window.__logs` should be `[]`. A pixi-effects warning is an instruction: fix what it names.
|
|
18
19
|
4. **Look at it** — you cannot judge motion from code. The library has the tools built in:
|
|
19
20
|
- `await movie.contactSheet({ count: 6, as: 'dataURL' })` → ONE image of several labelled frames (pass `frames: [...]` / `times: [...]` to choose; include the middle of every transition and the last second). Save it and view it.
|
|
20
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.
|
|
21
22
|
- `await movie.snapshot(frame, { as: 'dataURL' })` → one frame, canvas only (no player bar).
|
|
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.
|
|
22
24
|
From a browser tool the result is a `data:` URL string: `agent-browser eval "movie.contactSheet({ count: 6, as: 'dataURL' })" | python3 ai/tools/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.
|
|
23
25
|
5. **Iterate** on what you saw (clipped text, collisions, wrong timing). Then export: `const blob = await movie.render({ format: 'mp4' })` (about real time; `movie.on('progress', …)` reports 0–100).
|
|
24
26
|
|
|
@@ -28,10 +30,12 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
28
30
|
- **Layers are hidden, not removed, after their lifespan; z-order is array order.** Switch scenes by stacking layers with `at`/`duration` over an opaque background rect.
|
|
29
31
|
- **Angles are degrees. `+z` is toward the viewer. Numbers can be expressions** (`'GW/2 - w/2'`; no `pi`; no per-frame variable).
|
|
30
32
|
- **Anchors**: rect/circle/ellipse are centred on `x,y`; text and image are top-left (use `anchorX/anchorY: 0.5`); compositions use `pivotX/pivotY`. A bar that grows from its base needs `anchorY: 1`.
|
|
31
|
-
-
|
|
33
|
+
- **Sound effects need no files:** `{ type: 'audio', sfx: 'swoosh', at: 2 }` — presets `click pop swoosh swipe hit riser chime beep coin glitch typewriter`; knobs `pitch`, `brightness`, `seed`. The layer lasts as long as the sound (`duration` stretches it); a `riser` is loudest at its end (`at = hit − duration`), a `swoosh` 0.16 s in. A music file shorter than its layer goes silent — `loop: true`.
|
|
32
34
|
- **Masks live in the parent's coordinates and don't follow the masked layer.** **`filterArea` is in the layer's own coordinates.**
|
|
33
35
|
- **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.
|
|
34
|
-
- **Built in:** `fillGradient` (linear/radial, alpha stops → vignettes), `{value}` counters, `repeat`/`yoyo`, `orbit()`. **Not in the DSL (hand-roll, recipes exist or loops are easy):** other per-frame curves, per-letter text animators (one layer per letter),
|
|
36
|
+
- **Built in:** `splitText()` / `measureText()` (per-letter / per-word layers, exact positions), `shape: 'arc'` (progress rings, pie / donut slices: animate `endAngle`), `fillGradient` (linear/radial, alpha stops → vignettes), `{value}` counters, `repeat`/`yoyo`, `orbit()`. **Not in the DSL (hand-roll, recipes exist or loops are easy):** stroke draw-on (trim path), text that changes over time (other than `{value}`), other per-frame curves and jitter, per-letter text animators (one layer per letter), particle emitters, group/parent layers.
|
|
37
|
+
- **Blend modes:** `blendMode: 'add' | 'screen' | 'multiply'` on any layer (glows, light leaks).
|
|
38
|
+
- **One `Movie` per page.** A sound effect needs no `duration` (it lasts as long as its clip). Seeking is safe: counters, colours and growing shapes are right after any jump.
|
|
35
39
|
|
|
36
40
|
## Layout sanity (the cheap bugs a screenshot catches)
|
|
37
41
|
|
|
@@ -42,6 +46,7 @@ Centred text must be positioned by `anchorX/anchorY: 0.5`; estimate text width a
|
|
|
42
46
|
- `reference/cheatsheet.md` — every layer type, prop, default and rule on one page.
|
|
43
47
|
- `reference/recipes.md` — tested building blocks: slam type, lower-third, marquee, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions + music, looping, particles, gradients/vignette, three.js metal.
|
|
44
48
|
- `reference/pitfalls.md` — the full list of real mistakes, with status.
|
|
49
|
+
- `tools/check.mjs` — the one-command review (see Workflow step 3).
|
|
45
50
|
- `template.html` — the starting file. (It loads the library from a CDN; when testing against a local build, point the three `pixi-effects*` importmap entries at `../../dist/…`.)
|
|
46
51
|
- `tools/save-image.py` — save a `dataURL` result (contact sheet / snapshot) to a PNG.
|
|
47
52
|
- Full reference: `docs/dsl.md` and `docs/api.md` in the repository.
|
|
@@ -87,39 +92,42 @@ name, at, duration,
|
|
|
87
92
|
initial: { ...props applied before any keyframe },
|
|
88
93
|
keyframes: [ { at, duration, ease, set | to | from | (from + to), repeat?, yoyo?, repeatDelay? } ],
|
|
89
94
|
filters: [ { type: 'chromaKey', keyColor, threshold, smoothing, spill } | { type: 'custom', name, filter: <Pixi Filter> } ],
|
|
90
|
-
mask: <a layer spec>, maskInverted,
|
|
95
|
+
mask: <a layer spec>, maskInverted, // a mask with no `at` of its own starts and ends with the layer it masks (its keyframes count from that layer's start); an explicit mask `at` is composition time
|
|
91
96
|
filterArea: { x, y, width, height } // in the layer's OWN coordinates; lets blur/glow draw past the layer's bounds
|
|
92
97
|
threeD: true // opt into 2.5D (see below)
|
|
98
|
+
blendMode: 'normal' | 'add' | 'screen' | 'multiply' // add / screen brighten (glows, light leaks: overlapping halos add up); on a composition its children inherit the mode
|
|
93
99
|
```
|
|
94
100
|
|
|
95
101
|
Animatable props (`initial` / keyframes): `x y alpha rotation scale scaleX scaleY pivotX pivotY anchorX anchorY skewX skewY tint width height visible autoAlpha`, plus for shapes their style/geometry, for text `fill`, for audio `volume`, for 3D `z rotationX rotationY`, filter params as `'filters.<name>.<param>'`, three objects as `'three.<obj>.<path>'`.
|
|
96
102
|
|
|
97
|
-
Keyframe kinds: `set` (jump), `to` (animate to), `from` (animate from), `from`+`to`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed). `ease` = any GSAP ease (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`…). Default ease is linear.
|
|
103
|
+
Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate to), `from` (animate from), `from`+`to`. `from` / `from+to` show their start value from the layer's start, so a delayed fade-in needs no `initial.alpha`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed). `ease` = any GSAP ease (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`…). Default ease is linear.
|
|
98
104
|
|
|
99
105
|
## Layer types
|
|
100
106
|
|
|
101
|
-
**text** — `text`, `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping }`) — that is how counters work. Other content cannot change over time.
|
|
107
|
+
**text** — `text`, `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */ }`) — that is how counters work. Other content cannot change over time. Multi-line text is centre-aligned by default (`style.align: 'left'` to change).
|
|
102
108
|
|
|
103
109
|
**image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'`. Default anchor top-left; natural size = `w`,`h`.
|
|
104
110
|
|
|
105
111
|
**video** — `asset`, `loop`, `audio`, `volume`; `initial: { scale: 'cover' }`.
|
|
106
112
|
|
|
107
|
-
**audio** — `asset`, `loop`, `volume`; fade with volume keyframes.
|
|
113
|
+
**audio** — a file: `asset`, `loop`, `volume`; `volume` (top level, or in `initial` like every other layer's starting values); fade with volume keyframes (`{ at: -2, to: { volume: 0 }, duration: 2 }` fades the last 2 s). Omit `duration` for a one-shot clip: it lasts exactly as long as the clip. With `loop: true` and no `duration` it lasts until the composition ends. An explicit `duration` longer than the clip goes silent after the clip unless `loop: true`.
|
|
114
|
+
**Or a sound effect with no file — `sfx`:** `{ type: 'audio', sfx: 'swoosh', at: 2 }` is the whole thing. Presets, default length in s: `click` 0.04 · `pop` 0.12 · `swoosh` 0.5 (loudest 0.16 s in, moves left → right) · `swipe` 0.22 · `hit` 0.7 · `riser` 1 (loudest at its END: `at = hit − duration`) · `chime` 1.4 · `beep` 0.16 · `coin` 0.4 · `glitch` 0.35 · `typewriter` 0.06. Knobs: `sfx: { preset, pitch /* semitones ±24 */, brightness /* −1 dark … 1 bright */, seed /* another take */ }`. **The layer's `duration` IS the sound's length** (omit it; set it to stretch, e.g. a riser); `volume` is its level (1 = the preset's standard level, peaking at −12 … −18 dBFS by preset). No `loop`: one layer per hit (a JS loop). Custom sound: `sfx: { voices: [{ wave: 'noise' | 'sine' | 'square' | 'saw' | 'triangle', freq: 440 | [from, to], filter: { type: 'lowpass' | 'highpass' | 'bandpass', freq: [from, to] }, envelope: 'fall' | 'bell' | 'swell' | 'hold', pan: [-1, 1] }] }`. The export (mp4 / webm / mov) encodes this same mix (lossy AAC / Opus), on time within a few ms.
|
|
108
115
|
|
|
109
|
-
**shape** — `shape: 'rect' | 'circle' | 'ellipse' | 'line' | 'polygon' | 'path'`:
|
|
116
|
+
**shape** — `shape: 'rect' | 'circle' | 'ellipse' | 'arc' | 'line' | 'polygon' | 'path'`:
|
|
110
117
|
|
|
111
118
|
| shape | geometry (top level **or** in `initial`) |
|
|
112
119
|
|---|---|
|
|
113
120
|
| rect | `width height cornerRadius anchorX anchorY` |
|
|
114
121
|
| circle | `radius anchorX anchorY` |
|
|
115
122
|
| ellipse | `radiusX radiusY anchorX anchorY` |
|
|
123
|
+
| arc | `radius innerRadius startAngle endAngle` — degrees, **0° = 3 o'clock, clockwise**; centred on `x, y`; stroke only = an open arc line (`strokeCap: 'round'`), with a fill = a sector, with `innerRadius` = a donut slice; a sweep ≥ 360° = a full circle. A progress ring: `startAngle: -90` and animate `endAngle` from −90 to 270 |
|
|
116
124
|
| line | `from: [x,y] to: [x,y]` — plain canvas coordinates (omit `x,y`; if you give them they place the line's midpoint). Stroke in `initial`: `strokeColor`, `strokeWidth` |
|
|
117
125
|
| polygon | `points: [[x,y],…] open` — canvas coordinates like `line` |
|
|
118
126
|
| path | `d` (SVG path data) — canvas coordinates like `line` |
|
|
119
127
|
|
|
120
|
-
Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha strokeWidth`; `colorSpace: 'oklab'|'oklch'` for clean colour tweens. rect/circle/ellipse are **centred on `x,y` by default** (`anchorX/anchorY` default 0.5): for a bar growing from its base use `anchorY: 1` (or `anchorX: 0` for left-to-right). **`fillGradient`** (top level or `initial`; instead of `fillColor`): `{ type?: 'linear'|'radial', stops: [[0, '#000'], [1, 'rgba(0,0,0,.6)']], angle? /* linear, deg, 90 = top→bottom */, center?, innerRadius?, radius? /* radial, 0–1 of bounds */ }` — stops may have alpha, so a radial transparent→dark is a vignette.
|
|
128
|
+
Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha strokeWidth`; `colorSpace: 'oklab'|'oklch'` for clean colour tweens. rect/circle/ellipse are **centred on `x,y` by default** (`anchorX/anchorY` default 0.5): for a bar growing from its base use `anchorY: 1` (or `anchorX: 0` for left-to-right). **`fillGradient`** (top level or `initial`; instead of `fillColor`): `{ type?: 'linear'|'radial', stops: [[0, '#000'], [1, 'rgba(0,0,0,.6)']], angle? /* linear, deg, 90 = top→bottom */, center?, innerRadius?, radius? /* radial, 0–1 of bounds */ }` — stops may have alpha, so a radial transparent→dark is a vignette. Stroke ends and corners: `strokeCap: 'butt'|'round'|'square'`, `strokeJoin: 'miter'|'round'|'bevel'` (top level or in `initial`; not animated — `round` keeps sharp corners like an M from poking out).
|
|
121
129
|
|
|
122
|
-
**composition** — `width height duration sequences transitions`. With `threeD: true` it is a **card**: its children are drawn into one texture and move / rotate in depth together. Children use the composition's local coordinates and times. Position/rotate/scale it as a unit; to rotate/scale about its centre set `pivotX/pivotY` to the centre and `x/y` to where that point should sit.
|
|
130
|
+
**composition** — `width height duration sequences transitions`. With `threeD: true` it is a **card**: its children are drawn into one texture (content outside its `width × height` is clipped: size it for a soft shadow too; a plain 2D composition does not clip, but is skipped when its whole rectangle is off screen) and move / rotate in depth together. Children use the composition's local coordinates and times. Position/rotate/scale it as a unit; to rotate/scale about its centre set `pivotX/pivotY` to the centre and `x/y` to where that point should sit.
|
|
123
131
|
|
|
124
132
|
**camera** (2.5D) — props in `initial`/keyframes: `x y z lookAtX lookAtY lookAtZ fov` (defaults: centred, looking at the z = 0 plane, `fov` 40, `z` auto = `(H/2)/tan(fov/2)` ≈ 989 at 720p). Never on the camera object itself. An orbit is `orbit()`; by hand: `x = cx + R·sin θ`, `z = R·cos θ`, `R = (H/2)/tan(fov/2)`, `lookAt` = the centre at `z = 0`.
|
|
125
133
|
|
|
@@ -150,6 +158,14 @@ orbit({ duration: 6, degrees: 40 /* , radius, center, start, fov, ease, at */ })
|
|
|
150
158
|
```
|
|
151
159
|
`kenBurns` covers the canvas, so source images should be at least canvas-sized (1920×1080 for 1280×720).
|
|
152
160
|
|
|
161
|
+
```js
|
|
162
|
+
import { measureText, splitText } from 'pixi-effects'; // text layout helpers (measured with the renderer's own fonts)
|
|
163
|
+
measureText('Hello', style) // → { width, height, lines }
|
|
164
|
+
splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
|
|
165
|
+
// → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
|
|
166
|
+
```
|
|
167
|
+
Per-letter / per-word animation = `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
168
|
+
|
|
153
169
|
## Assets
|
|
154
170
|
|
|
155
171
|
`assets: [{ name, src }]` in `movie.init` (images, audio, video). `src` may be a URL or a `data:`/`blob:` URL (e.g. `canvas.toDataURL()`).
|
|
@@ -167,6 +183,7 @@ movie.audioBuffer // mixed audio (after init), if
|
|
|
167
183
|
await movie.contactSheet({ count: 6, as: 'dataURL' }) // ONE image of several labelled frames — look at it. Options: frames | times | count, columns, cellWidth, as
|
|
168
184
|
await movie.snapshot(60, { as: 'dataURL' }) // one frame, canvas only (no player bar) — use it for detail; sheet tiles are small
|
|
169
185
|
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
|
|
186
|
+
movie.inspectAudio() // the soundtrack as numbers — you cannot listen: sources[i] = { layer, start, end, peakDb, sound: { length, peakDb, loudestAt, brightnessHz } }, windows (rms / peak / brightness per 0.1 s), issues (limited mix, inaudible / cut-off sounds)
|
|
170
187
|
new Controller(movie, { canvas }) // optional player bar (overlays the canvas bottom ~60px; not in the export)
|
|
171
188
|
```
|
|
172
189
|
|
|
@@ -175,7 +192,7 @@ new Controller(movie, { canvas }) // optional player bar (overlay
|
|
|
175
192
|
|
|
176
193
|
# pixi-effects pitfalls — what actually went wrong
|
|
177
194
|
|
|
178
|
-
Collected by having fresh AI sessions build
|
|
195
|
+
Collected by having fresh AI sessions build pieces from the docs alone: first six (kinetic type, news lower-third, slideshow, 2.5D showcase, three.js title, bar chart), then a portfolio of 16 more (items 32 and up), made by three different models. Each entry is a real stumble. **Status**: `fixed` = the library now handles or warns; `docs` = nothing can catch it, so remember it; `hand-roll` = the DSL has no feature, see the workaround.
|
|
179
196
|
|
|
180
197
|
A good habit that catches most of these: after `init`, read `window.__logs` (see `ai/template.html`) — every pixi-effects warning says what to change.
|
|
181
198
|
|
|
@@ -185,7 +202,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
185
202
|
2. **A layer's own `at` and a transition's `at` are composition time** (seconds from the parent composition's start). Only keyframes are layer-local.
|
|
186
203
|
3. **Layers are hidden, not removed, after `[at, at+duration)`** and keep their last values. Z-order is array order (later = on top). To switch scenes, stack scenes with `at`/`duration`, each starting with an opaque full-screen rect. *(docs)*
|
|
187
204
|
4. **Transitions need overlapping lifespans.** Both layers must be alive for the whole window: scene `i` starts at `i·(L−T)`, the transition's `at` is the next scene's start, total = `n·(L−T)+T`. A scene that ends while the transition is still running leaves a gap. *(docs)*
|
|
188
|
-
5. **
|
|
205
|
+
5. **Sound effects need no file at all:** `{ type: 'audio', sfx: 'pop', at }` — the layer lasts exactly as long as the sound. **Audio length of a FILE.** Omit `duration` on a one-shot clip: the layer lasts exactly as long as the file (nothing to measure; the only sample file is `examples/_assets/bgm.mp3`, 6 s). `loop: true` with no `duration` lasts until the composition ends. An explicit `duration` longer than the clip goes silent after the clip unless `loop: true` (`bgm.mp3` is 6 s) and warns. Volume fades work like any keyframe: `volume: 0` + `{ at: 0, to: { volume: v }, duration: 2 }` + `{ at: -2, to: { volume: 0 }, duration: 2 }` (the last 2 s only; `set` is a jump). *(fixed: volume keyframes used to ramp from the previous keyframe's end, so the music faded across the whole piece)*
|
|
189
206
|
6. **Snap times to the frame grid (`Math.round(t*30)/30`) when two things must stay locked** (a bar and its label). Drive both from one JS easing function with per-frame `set` keyframes rather than mixing a tween with computed positions. *(docs)*
|
|
190
207
|
7. **Pick eases on purpose.** `expo.in` stays tiny until the very end (a "flood" circle never reaches the corners — use a bigger radius and `power2.in`). A zoom transition with `power2.out` looks like a cut because the incoming scene is nearly opaque after 40 %. Default is linear. *(docs)*
|
|
191
208
|
8. **`render()` runs in roughly real time** (6 s of 720p ≈ 6 s). Listen to `movie.on('progress', e => e.progress)` (0–100). *(docs)*
|
|
@@ -195,7 +212,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
195
212
|
9. **Defaults differ by type.** rect/circle/ellipse are centred on `x,y`; text and image are top-left; a composition has no anchor — use `pivotX/pivotY` and set `x,y` to where the pivot should land. For a bar growing from its base use `anchorY: 1` (or `anchorX: 0`). *(docs)*
|
|
196
213
|
10. **Shape geometry may be at the top level or in `initial`** (`width height radius anchorX …`). *(fixed: `initial` used to be silently ignored)*
|
|
197
214
|
11. **Angles are degrees** — `rotation`, `skew*`, `rotationX/Y` — and `rotation: -0.2` is almost no rotation. *(docs)*
|
|
198
|
-
12. **Text**: anchor 0.5 centres the line *box*, not the cap height (
|
|
215
|
+
12. **Text**: anchor 0.5 centres the line *box*, not the cap height (an underline under serif or italic text needs ~`0.9·size` below the centre: `0.46·size` touches the descenders of a g or y). Arial Black is ~0.8·size wide per glyph, monospace ~0.6·size; leave ~7 % for hold-scale. `style` accepts any PixiJS TextStyle field: `letterSpacing`, `wordWrap` + `wordWrapWidth`, `lineHeight`, `stroke`, `dropShadow`, `padding`. A glow (`dropShadow` with `blur`) is clipped unless `padding ≥ 2·blur`. *(docs)*
|
|
199
216
|
13. **`w` / `h` in expressions** are the layer's own size. For text they now describe the styled text (`x: '-w'` scrolls a marquee fully out). *(fixed)*
|
|
200
217
|
14. **Masks live in the parent's coordinate space and do not follow the masked layer.** Reveal by growing the mask's `width` from a fixed edge (`anchorX: 0`); exit by tweening `x` and `width` together; slide the content separately. *(docs)*
|
|
201
218
|
15. **`line` / `polygon` / `path` use plain canvas coordinates** when you give no `x,y` (they used to be drawn around (0,0)); with `x,y` those place the shape's centre. Stroke colour and width go in `initial`. A background pill must be sized from its text (~0.58 × fontSize per glyph + padding): `inspect` does not check text against shapes. *(fixed)*
|
|
@@ -208,14 +225,14 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
208
225
|
18. **Counters**: `text: '{value}'` + animate `value` (with `format`). Other text content cannot change over time. Earlier sessions generated one text layer per number (40+ per bar). *(fixed)*
|
|
209
226
|
19. **Loops**: `repeat` (finite) / `yoyo` / `repeatDelay` on any keyframe; infinite repeats are rejected with a warning. *(fixed)*
|
|
210
227
|
20. **No per-frame expression time.** Expressions are evaluated once at build. A camera orbit is `orbit()`; any other curve (waves, spirals) must be sampled into short linear keyframes (~0.1 s) in a JS loop. *(partly fixed)*
|
|
211
|
-
21. **
|
|
212
|
-
22. **Colour tweens look muddy in RGB** (red→green passes through brown). Use `colorSpace: 'oklab'` or `'oklch'` on the layer. *(docs)*
|
|
228
|
+
21. **Per-letter / per-word text is `splitText()` (one layer per piece, positions measured for you); no group/null parent, no particle emitter, no blend modes, no caption-linked-to-slide.** One layer per letter; many layers; seeded random loops; plain alpha. *(hand-roll)*
|
|
229
|
+
22. **Colour tweens look muddy in RGB** (red→green passes through brown). Use `colorSpace: 'oklab'` or `'oklch'` on the layer. In `oklch`, a nearly grey end (chroma < ~0.08, e.g. `#9a9ab4`) borrows the other end's hue, so grey → yellow no longer flashes through pink. *(docs / fixed)*
|
|
213
230
|
23. **Generated images**: `canvas.toDataURL()` as an asset `src` works. Make them ≥ canvas size or `kenBurns` zooms look soft. *(docs)*
|
|
214
231
|
|
|
215
232
|
## 2.5D / three.js
|
|
216
233
|
|
|
217
234
|
24. **`threeD` is required** for `z`, `rotationX`, `rotationY` (they warn otherwise). Camera props go in `initial`/keyframes, never on the camera object. `+z` is toward the viewer. *(fixed: warns)*
|
|
218
|
-
25. **A layer at/behind the camera plane is hidden** — the default camera sits at `z = (H/2)/tan(fov/2)` ≈ 989 at 720p, fov 40 (≈ 808 at fov 48, lower at higher fov). Keep every layer's `z` below it. *(fixed: warns once)*
|
|
235
|
+
25. **A layer at/behind the camera plane is hidden** — the default camera sits at `z = (H/2)/tan(fov/2)` ≈ 989 at 720p, fov 40 (≈ 808 at fov 48, lower at higher fov). Keep every layer's `z` below it. *(fixed: warns once)* **It is hidden whole when ANY corner of it passes the camera plane**: a big plane tilted with `rotationX` / `rotationY` (a ground plane, an orbit ring) vanishes as soon as one corner swings behind the camera (the warning then says a corner reaches the plane). Tilt it less, shrink it, or keep the camera clear of its depth range. *(docs / fixed: warns)*
|
|
219
236
|
26. **Under a dolly zoom only the `z = 0` plane stays put**; everything nearer balloons and clips. Headline at `z: 0`, near cards `z ≤ ~70`. Perspective also pulls far layers toward the centre: put far cards at the outer x, near cards inward, and keep ~10 % margin for an orbit sweep. `orbit({ dollyZoom: { from, to } })` does both together. *(docs)*
|
|
220
237
|
27. **Combining orbit and dolly zoom**: `orbit()` specifies `z`, so it stops following `fov` — write your own `fov` and `z = (H/2)/tan(fov/2)` keyframes for the zoom. *(docs)*
|
|
221
238
|
28. **Nearer `threeD` layers sort on top of text** — lay out around them; equal `z` keeps array order. A card (rect + icon + label that move together) is a `composition` with `threeD: true`. A final call-to-action or overlay should be a plain 2D layer (last in the array): it ignores the camera, and fade the 3D scene and headline out before it appears (a dim rect alone leaves them visible). *(docs)*
|
|
@@ -226,7 +243,32 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
226
243
|
30. **A scene change must keep the outgoing scene alive until the covering flood / wipe has finished**, and the next scene starts when it is complete; the flood circle must reach the corners (radius ≥ half the diagonal) and use `power2.in`, not `expo.in`. *(docs)*
|
|
227
244
|
30b. **Unknown option names now warn** (`contactSheet({ cols: 2 })` → did you mean `columns`; `init({ fps })` → `frameRate`). Layer names show up in `inspect` paths, so give layers a `name`. `inspect` ignores faint (alpha < 0.3) layers and text whose x / y is animated (tickers), so a scrolling marquee is not reported as cut off. Pixi may print `[BindGroup] … destroyed while still bound` warnings at `movie.destroy()`: harmless (playback and seeking never warn). *(docs)*
|
|
228
245
|
30c. **Look with the built-in tools, not OS screenshots**: `await movie.contactSheet({ count: 6, as: 'dataURL' })` returns one labelled image of the whole animation; `await movie.inspect(frame)` returns every layer's canvas bounds and `issues`. If you do use a browser tool, `screenshot` needs an **absolute path**, macOS `sed -i` needs `''`, and a static server may redirect `/x.html` → `/x` and drop `?query`. Keep the template's `try/catch` around `init` so failures land in `window.__logs`. Check ≥ 5 timestamps including mid-transition. *(fixed: tools built in)*
|
|
229
|
-
31. **Check audio
|
|
246
|
+
31. **Check audio with `movie.inspectAudio()`**: every sound measured on its own (`sound.length`, `peakDb`, `loudestAt`, `brightnessHz`), when it plays, loudness and brightness per 0.1 s window, and `issues` (the mix was limited, a layer is inaudible, an sfx is cut off by the end). Decoding a render by hand is no longer needed: `render()` encodes the same mix, on time in mp4 / mov / webm / mkv. *(fixed)*
|
|
247
|
+
|
|
248
|
+
## Round 2 — found by building 16 portfolio pieces (examples/gallery)
|
|
249
|
+
|
|
250
|
+
32. **Seeking is safe now** *(fixed)*: a `set` keyframe (counter value, text fill, image tint, shape style) is undone when you seek back before it, and shapes are redrawn before they are culled, so a rect growing from `width: 0` at the left edge draws after any jump seek (contact sheet, snapshot, scrubbing, export). The old workarounds (`width: 0.01`, a one-frame `to` instead of `set`) are not needed.
|
|
251
|
+
33. **A mask starts and ends with the layer it masks** *(fixed)*, so its keyframes count from that layer's start. A mask with an explicit `at` keeps composition time. Before, a mask `at: 0` reveal had finished before a layer that appeared at 8 s, and the layer stayed invisible without any warning.
|
|
252
|
+
34. **`from` and `from+to` show their start value from the LAYER's start**, not from the keyframe's `at` (GSAP behaviour): a delayed fade-in `{ at: 2, from: { alpha: 0 }, to: { alpha: 1 } }` needs no `initial.alpha`. To keep the current value until `at` and then jump, use `set`. *(docs)*
|
|
253
|
+
35. **One `Movie` per page.** Creating a second Movie (for an isolated repro) and destroying it breaks the first (`null.clear`). Reload instead. *(docs)*
|
|
254
|
+
36. **Multi-line text is centre-aligned by default** (`style.align: 'left'` for left-aligned paragraphs). *(docs)*
|
|
255
|
+
37. **Composition size.** A 2D composition does not clip its children (a child outside `width × height` still draws), but its rectangle is the culling area: if the whole rectangle is off the canvas the group is skipped even when a child is on screen — keep it near what it draws, or canvas-sized. A `threeD` composition (a card) IS clipped at its `width × height` (it is rendered into a texture that size): size it for the soft shadow too. A growing `threeD` shape or text is fine: its texture is resized in place (it used to warn "destroyed while still bound"). *(measured; docs / fixed)*
|
|
256
|
+
38. **Under an orbit the side the camera swings toward becomes the NEAR side**: a "far" card at `z: −200` can be drawn bigger than 1× on that side and run off the frame (`inspect` only checks text). Pull those cards inward / lower `z`, and check a frame at the extreme of the sweep. After `orbit()` ends the default camera returns, so to hold the final pose add a camera layer that starts there. Near particles balloon under a dolly zoom: cap particle `z` at about +150, or one becomes a fake full stop. *(docs)*
|
|
257
|
+
39. **`fillGradient` stops span the whole unmasked shape**, not the visible part: under a mask, place the stops for the visible fraction (e.g. 0.64→1). A translucent shape over near-black reads as grey: put the alpha in the gradient stops. *(docs)*
|
|
258
|
+
40. **Rotate a polygon / path / line about a chosen point with `pivotX` / `pivotY`** (in the shape's own canvas coordinates; put the same point in `x, y`): `{ points, initial: { pivotX: 600, pivotY: 300, x: 600, y: 300, rotation: 180 } }` turns about the vertex (600, 300) — measured. Without a pivot the shape turns about the centre of its bounds. `anchorX` / `anchorY` do nothing on these shapes (they are for rect / circle / ellipse). *(docs; the gallery wrapped every polygon in a composition unnecessarily)*
|
|
259
|
+
41. **Glow a whole set of layers** by putting them in one `composition` with a blur filter and `filterArea` (only a composition carries a filter for its children): wide enough for the blur, and the set is built once, so the extra copy is just a second group. *(docs)*
|
|
260
|
+
42. **Transition looks:** the default `dissolve` reads as a hard black-and-white camouflage and `scale: 220` as static; `scale: 90, smoothing: 0.5` is a soft film dissolve. The default edge softness of `iris` (0.02) and `wipe` blurs flat graphics at 720p: `smoothing: 0.004`–`0.01` gives a clean edge. *(docs)*
|
|
261
|
+
43. **Layers built in a loop need their z-order planned**: anything pushed to the array after the loop is drawn over what the loop made (rings crossed over a header). Keyframes on different properties of one layer may overlap in time. Keep a subject in the upper ~60 % of a source image if a caption will sit under it (`inspect` does not look at picture content). *(docs)*
|
|
262
|
+
44. **Zero-pad a counter with `format: { pad: 2 }`** (`18:42:{value}` shows `18:42:05`). A `{value}` placeholder that nothing sets or animates prints 0 and now warns: `{value}` is reserved, there is no way to print it literally. *(fixed)*
|
|
263
|
+
45. **The same mistake in a loop warns once**: the warning "keyframe starts after the layer ends" is summarised after three layers. The usual cause is an off-by-one (`f <= frames` instead of `f < frames`) that puts the last sample exactly at the layer's end. *(fixed)*
|
|
264
|
+
46. **Still not in the DSL (hand-roll; round-two notes have workarounds):** stroke draw-on / trim path on a line, polygon or path (use a growing rect mask for a one-direction reveal; a circular progress ring is `shape: 'arc'`), text content that changes over time (only `{value}`), per-frame jitter (film grain = seeded `set` keyframes on an oversized image), particle emitters, shared masks, `zIndex`.
|
|
265
|
+
46b. **Blend modes exist now** (`blendMode: 'add' | 'screen' | 'multiply'` on any layer, threeD layers and compositions included; a composition's children inherit it). Additive white cores no longer read grey on a coloured background. It also works on a layer with `filters` (a blurred glow with `add` / `multiply`): the mode is applied by the last filter, because a container's blend inside a filter pass multiplied against nothing and rendered black. With `add`, use dim saturated colours: overlaps sum, and near-white sums to pure white. *(fixed)*
|
|
266
|
+
47. **Browser tooling: prefer `node ai/tools/check.mjs page.html`** — it runs its own private Chrome, so none of the following can bite. If you drive a browser by hand instead: the `agent-browser` daemon is shared, so a `contactSheet` can come back from another session: after saving an image, look at it and confirm it is YOUR piece. In zsh a variable holding `agent-browser --session x` is not split into words (use a small wrapper script). `inspect` now clips overlaps to the visible area, ignores the two scenes of a running transition, treats text inside a moving (panned) composition as moving, and does not report text scaled to 0 as "no size"; it still does not compare text with picture content or shapes.
|
|
267
|
+
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)*
|
|
268
|
+
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)*
|
|
269
|
+
50. **An image (or video) works as a mask, `maskInverted` included** *(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 `maskInverted` 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)*
|
|
270
|
+
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)*
|
|
271
|
+
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)*
|
|
230
272
|
|
|
231
273
|
|
|
232
274
|
<!-- ===== ai/reference/recipes.md ===== -->
|
|
@@ -505,6 +547,7 @@ photos.forEach((_, i) => sequences.push(withFade({
|
|
|
505
547
|
}, { in: 0.4, out: 0.4 })));
|
|
506
548
|
sequences.push({ type: 'audio', asset: 'bgm', loop: true, volume: 0, duration: total,
|
|
507
549
|
keyframes: [{ at: 0, to: { volume: 0.8 }, duration: 2 }, { at: -2, to: { volume: 0 }, duration: 2 }] });
|
|
550
|
+
transitions.forEach(tr => sequences.push({ type: 'audio', sfx: 'swoosh', at: tr.at + tr.duration / 2 - 0.16 })); // a swoosh is loudest 0.16 s in: centre it on the transition
|
|
508
551
|
return { sequences, transitions, duration: total };
|
|
509
552
|
```
|
|
510
553
|
|
|
@@ -512,6 +555,122 @@ Source images should be at least canvas-sized (`kenBurns` zooms in): draw genera
|
|
|
512
555
|
|
|
513
556
|
---
|
|
514
557
|
|
|
558
|
+
## Sound effects without files
|
|
559
|
+
|
|
560
|
+
No `assets`, no files: a sound effect is one audio layer with `sfx`. It lasts as long as the sound.
|
|
561
|
+
|
|
562
|
+
```js
|
|
563
|
+
// @recipe sfx-minimal
|
|
564
|
+
return [
|
|
565
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0f1424' } },
|
|
566
|
+
{ type: 'text', name: 'word', text: 'HELLO', at: 2, duration: 3,
|
|
567
|
+
style: { fontSize: 120, fontWeight: '900', fill: '#ffffff' },
|
|
568
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
569
|
+
keyframes: [{ at: 0, from: { x: -400 }, to: { x: 'GW/2' }, duration: 0.4, ease: 'power3.out' }] },
|
|
570
|
+
{ type: 'audio', sfx: 'swoosh', at: 2 }, // flies in with a swoosh…
|
|
571
|
+
{ type: 'audio', sfx: 'hit', at: 2.4 }, // …and lands (0.4 s later) with a hit
|
|
572
|
+
];
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Check it without listening, then export — the file encodes this same mix (mp4 / mov: AAC, webm / mkv: Opus; lossy):
|
|
576
|
+
|
|
577
|
+
```js
|
|
578
|
+
// @docs-only render-and-verify
|
|
579
|
+
await movie.init({ canvas, width: 1280, height: 720, duration: 5, composition: { sequences } });
|
|
580
|
+
const a = movie.inspectAudio();
|
|
581
|
+
console.log(a.issues); // must be []
|
|
582
|
+
console.log(a.sources.map(s => [s.layer, s.start, s.sound.loudestAt, s.sound.brightnessHz]));
|
|
583
|
+
const blob = await movie.render({ format: 'mp4' });
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## Sound effects in sync with the picture
|
|
589
|
+
|
|
590
|
+
Line up the LOUD point, not the start: a `riser` is loudest at its end (`at = hit − duration`), a `swoosh` 0.16 s in, everything else at its start (`inspectAudio().sources[i].sound.loudestAt` tells you). Vary `pitch` for a series (a rising pitch reads as progress) and `seed` for repeats (no two typewriter keys alike).
|
|
591
|
+
|
|
592
|
+
```js
|
|
593
|
+
// @recipe sfx-cues
|
|
594
|
+
const BG = { type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0f1424' } };
|
|
595
|
+
const HIT = 1.5, RISE = 1.2; // the title slams in at 1.5 s; the riser leads into it
|
|
596
|
+
const sequences = [BG,
|
|
597
|
+
{ type: 'audio', name: 'riser', sfx: 'riser', at: HIT - RISE, duration: RISE, volume: 0.7 }, // a riser is loudest at its END
|
|
598
|
+
{ type: 'audio', name: 'hit', sfx: 'hit', at: HIT },
|
|
599
|
+
{ type: 'text', name: 'title', text: 'LAUNCH', at: HIT, duration: 8 - HIT,
|
|
600
|
+
style: { fontSize: 140, fontWeight: '900', fill: '#ffffff', fontFamily: 'Arial Black, Arial, sans-serif' },
|
|
601
|
+
initial: { x: 'GW/2', y: 260, anchorX: 0.5, anchorY: 0.5 },
|
|
602
|
+
keyframes: [{ at: 0, from: { scale: 2.4, alpha: 0 }, to: { scale: 1, alpha: 1 }, duration: 0.18, ease: 'expo.out' }] },
|
|
603
|
+
];
|
|
604
|
+
['PLAN', 'BUILD', 'SHIP'].forEach((label, i) => { // each item pops in with a pop; pitch climbs a step per item
|
|
605
|
+
const at = 2.4 + i * 0.5;
|
|
606
|
+
sequences.push({ type: 'text', name: 'item' + i, text: label, at, duration: 8 - at,
|
|
607
|
+
style: { fontSize: 44, fontWeight: 'bold', fill: '#ffd166' },
|
|
608
|
+
initial: { x: 360 + i * 280, y: 430, anchorX: 0.5, anchorY: 0.5 },
|
|
609
|
+
keyframes: [{ at: 0, from: { scale: 0, alpha: 0 }, to: { scale: 1, alpha: 1 }, duration: 0.25, ease: 'back.out(2)' }] });
|
|
610
|
+
sequences.push({ type: 'audio', name: 'pop' + i, sfx: { preset: 'pop', pitch: i * 2 }, at });
|
|
611
|
+
});
|
|
612
|
+
const CAPTION = 'ships friday', T0 = 4.4, STEP = 0.07; // typed caption: one letter + one key per step
|
|
613
|
+
[...CAPTION].forEach((ch, i) => {
|
|
614
|
+
const at = T0 + i * STEP;
|
|
615
|
+
sequences.push({ type: 'text', name: 'cap' + i, text: ch, at, duration: 8 - at,
|
|
616
|
+
style: { fontSize: 36, fill: '#9fb3d9', fontFamily: 'ui-monospace, Menlo, monospace' },
|
|
617
|
+
initial: { x: 640 - (CAPTION.length * 22) / 2 + i * 22, y: 540, anchorY: 0.5 } });
|
|
618
|
+
if (ch !== ' ') sequences.push({ type: 'audio', sfx: { preset: 'typewriter', seed: i }, at, volume: 0.6 }); // seed: every key sounds a little different
|
|
619
|
+
});
|
|
620
|
+
sequences.push({ type: 'audio', name: 'done', sfx: 'chime', at: T0 + CAPTION.length * STEP + 0.3 });
|
|
621
|
+
return sequences;
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## Progress ring (an arc) with a counter
|
|
627
|
+
|
|
628
|
+
`shape: 'arc'` is a ring when it has only a stroke: animate `endAngle` from the start angle (−90 = 12 o'clock) round to +270 for a full circle. A second arc under it is the track; `{value}` in the middle counts in step with the same ease.
|
|
629
|
+
|
|
630
|
+
```js
|
|
631
|
+
// @recipe progress-ring
|
|
632
|
+
const CX = 640, CY = 360, R = 120, TO = 0.82; // 82 % filled
|
|
633
|
+
const ease = 'power3.out', AT = 0.3, DUR = 2;
|
|
634
|
+
return [
|
|
635
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0f1424' } },
|
|
636
|
+
{ type: 'shape', shape: 'arc', name: 'track', radius: R, startAngle: 0, endAngle: 360,
|
|
637
|
+
initial: { x: CX, y: CY, strokeColor: '#27304d', strokeWidth: 24 } },
|
|
638
|
+
{ type: 'shape', shape: 'arc', name: 'progress', radius: R, startAngle: -90, endAngle: -90, strokeCap: 'round',
|
|
639
|
+
initial: { x: CX, y: CY, strokeColor: '#ffd166', strokeWidth: 24 },
|
|
640
|
+
keyframes: [{ at: AT, to: { endAngle: -90 + 360 * TO }, duration: DUR, ease }] },
|
|
641
|
+
{ type: 'text', name: 'percent', text: '{value}%', format: { decimals: 0 },
|
|
642
|
+
style: { fontSize: 72, fontWeight: 'bold', fill: '#ffffff' },
|
|
643
|
+
initial: { x: CX, y: CY, anchorX: 0.5, anchorY: 0.5, value: 0 },
|
|
644
|
+
keyframes: [{ at: AT, to: { value: TO * 100 }, duration: DUR, ease }] },
|
|
645
|
+
];
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Letters and words that animate one by one (`splitText`)
|
|
651
|
+
|
|
652
|
+
Text content cannot change, so a per-letter or per-word effect is one text layer per piece. `splitText(text, style, { by: 'chars' | 'words' | 'lines', x, y, align })` measures the text with the renderer's own font metrics (kerning kept) and returns each piece's `text`, `x`, `y`, `width`, `index` and `line`: put them straight into the layers. Use the same `style` for the whole text and each piece, and a web font must be loaded first. `measureText(text, style)` gives `{ width, height, lines }` for sizing pills and underlines.
|
|
653
|
+
|
|
654
|
+
```js
|
|
655
|
+
// @recipe split-text
|
|
656
|
+
const style = { fontFamily: 'Arial, sans-serif', fontSize: 110, fontWeight: 'bold', fill: '#ffffff' };
|
|
657
|
+
const sub = { fontFamily: 'Arial, sans-serif', fontSize: 44, fill: '#ffd166' };
|
|
658
|
+
const sequences = [{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0f1424' } }];
|
|
659
|
+
splitText('MAKE THINGS', style, { x: 640, y: 200, align: 'center' }).forEach(p => sequences.push({
|
|
660
|
+
type: 'text', name: 'letter-' + p.index, text: p.text, style, at: 0.3 + p.index * 0.06,
|
|
661
|
+
initial: { x: p.x, y: p.y, alpha: 0 }, // x / y are the top-left of the letter: anchorX / anchorY stay 0
|
|
662
|
+
keyframes: [{ at: 0, from: { y: p.y + 40, alpha: 0 }, to: { y: p.y, alpha: 1 }, duration: 0.35, ease: 'back.out(2)' }],
|
|
663
|
+
}));
|
|
664
|
+
splitText('one word at a time', sub, { by: 'words', x: 640, y: 380, align: 'center' }).forEach(p => sequences.push({
|
|
665
|
+
type: 'text', name: 'word-' + p.index, text: p.text, style: sub, at: 1.4 + p.index * 0.25,
|
|
666
|
+
initial: { x: p.x, y: p.y, alpha: 0 },
|
|
667
|
+
keyframes: [{ at: 0, to: { alpha: 1 }, duration: 0.3 }],
|
|
668
|
+
}));
|
|
669
|
+
return sequences;
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
515
674
|
## Looping motion
|
|
516
675
|
|
|
517
676
|
`repeat` (a finite count) and `yoyo` go on any keyframe. Total time = `duration × (repeat + 1)`.
|
|
@@ -671,7 +830,7 @@ interface SequenceCommon {
|
|
|
671
830
|
| Layer | Where `x, y` land by default | To change it |
|
|
672
831
|
|---|---|---|
|
|
673
832
|
| `shape` rect / circle / ellipse | the **centre** of the shape | `anchorX` / `anchorY` (0–1). A bar growing from its base: `anchorY: 1`; left to right: `anchorX: 0` |
|
|
674
|
-
| `shape` line / polygon / path | the centre of the shape's bounds |
|
|
833
|
+
| `shape` line / polygon / path | the centre of the shape's bounds | `pivotX` / `pivotY` (in the shape's own canvas coordinates): the point that sits at `x, y` and that rotation / scale turn about. **`anchorX` / `anchorY` do nothing on these.** |
|
|
675
834
|
| `text`, `image`, `video` | the **top-left** corner | `anchorX` / `anchorY: 0.5` to centre |
|
|
676
835
|
| `composition` | its top-left corner | `pivotX` / `pivotY` = the point that sits at `x, y` and that rotation / scale turn about |
|
|
677
836
|
|
|
@@ -683,7 +842,7 @@ A layer is **not removed** when its lifespan `[at, at + duration)` ends: it is o
|
|
|
683
842
|
|
|
684
843
|
### `text`
|
|
685
844
|
|
|
686
|
-
Renders a [PIXI.Text](https://pixijs.com/8.x/guides/components/scene-objects/text/text). Animatable position, rotation, scale, opacity and `fill`. `style` accepts any PixiJS `TextStyle` field and is applied once at build time — notably `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding` (numbers may be expressions). A `dropShadow` glow is clipped unless `padding` is at least twice its `blur`. Text content cannot change over time (
|
|
845
|
+
Renders a [PIXI.Text](https://pixijs.com/8.x/guides/components/scene-objects/text/text). Animatable position, rotation, scale, opacity and `fill`. `style` accepts any PixiJS `TextStyle` field and is applied once at build time — notably `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding` (numbers may be expressions). A `dropShadow` glow is clipped unless `padding` is at least twice its `blur`. Text content cannot change over time, with one exception: a `{value}` placeholder in `text` prints an animatable number (`text: '{value} users'`, `initial: { value: 0 }`, a keyframe `to: { value: 2480 }`; `format: { decimals, grouping }`) — that is how counters work. Any other change of wording is a second layer (stack layers with `at` / `duration`). Multi-line text is centre-aligned unless `style.align` says otherwise. In expressions `w` / `h` are the size of the styled text, so `x: '-w'` places it just off the left edge.
|
|
687
846
|
|
|
688
847
|
```ts
|
|
689
848
|
{
|
|
@@ -728,7 +887,7 @@ A text layer has one animatable number, `value`, printed wherever the text conta
|
|
|
728
887
|
}
|
|
729
888
|
```
|
|
730
889
|
|
|
731
|
-
`format`: `decimals` (default 0)
|
|
890
|
+
`format`: `decimals` (default 0), thousands `grouping` (default false) and `pad` (zero-pad the whole part to that many digits: `pad: 2` gives `05`, so a clock is `text: '18:42:{value}'` with `format: { pad: 2 }`). Put prefixes and suffixes in the text (`'${value}'`, `'{value}%'`). If `value` is animated but the text has no `{value}`, a warning says so.
|
|
732
891
|
|
|
733
892
|
### `image`
|
|
734
893
|
|
|
@@ -791,10 +950,40 @@ Audio-only sequence. No visual. Volume is animatable via keyframes.
|
|
|
791
950
|
|
|
792
951
|
Audio is mixed during `Movie.init()` and during `Movie.render()`. Volume keyframes interpolate linearly.
|
|
793
952
|
|
|
953
|
+
#### Sound effects without files: `sfx`
|
|
954
|
+
|
|
955
|
+
```ts
|
|
956
|
+
{ type: 'audio', sfx: 'swoosh', at: 2 } // a preset at its own length
|
|
957
|
+
{ type: 'audio', sfx: { preset: 'hit', pitch: -3, brightness: -0.5 }, at: 4 } // lower and darker
|
|
958
|
+
{ type: 'audio', sfx: 'riser', at: 3, duration: 1.5, volume: 0.7 } // duration IS the sound's length (stretched)
|
|
959
|
+
{ type: 'audio', at: 6, duration: 0.3, // a custom sound
|
|
960
|
+
sfx: { voices: [{ wave: 'square', freq: [1600, 200], filter: { type: 'lowpass', freq: 3000 }, envelope: 'fall' }] } }
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
The sound is synthesised from data at init (no asset, no network), deterministic, and the same mix in playback and in the exported file (encoded there: AAC / Opus are lossy). `at` is where it starts; `volume` and volume keyframes work as for files; `loop` is not available (one layer per hit). Knobs: `pitch` (semitones, ±24), `brightness` (−1 dark … 1 bright), `seed` (another take of the random parts). `volume: 1` is the preset's standard level, peaking at −12 … −18 dBFS depending on the preset; the finished mix is soft-limited above 0.9 with a warning naming the layers. A custom sound has up to 16 `voices`: `wave` (`sine` `triangle` `square` `saw` `noise`), `freq` (Hz or `[from, to]`), `filter` (`{ type: 'lowpass' | 'highpass' | 'bandpass', freq, q }`), `envelope` (`fall` `bell` `swell` `hold`), `pan`, `from` / `to` (part of the sound, 0–1), `gain`; its default length is 0.5 s.
|
|
964
|
+
|
|
965
|
+
| Preset | Default length | Loudest at | Brightness | Use for |
|
|
966
|
+
|---|---|---|---|---|
|
|
967
|
+
| `click` | 0.04 s | start | ≈ 12780 Hz | UI click, tick |
|
|
968
|
+
| `pop` | 0.12 s | start | ≈ 390 Hz | an element appearing |
|
|
969
|
+
| `swoosh` | 0.5 s | 0.16 s | ≈ 3070 Hz | transitions, fly-pasts (moves left → right) |
|
|
970
|
+
| `swipe` | 0.22 s | start | ≈ 13190 Hz | quick UI swipe |
|
|
971
|
+
| `hit` | 0.7 s | start | ≈ 150 Hz | impact, slam |
|
|
972
|
+
| `riser` | 1 s | its end | ≈ 11850 Hz | build-up into a hit |
|
|
973
|
+
| `chime` | 1.4 s | start | ≈ 1400 Hz | success, reveal |
|
|
974
|
+
| `beep` | 0.16 s | flat | ≈ 1030 Hz | countdown, alert |
|
|
975
|
+
| `coin` | 0.4 s | 0.07 s | ≈ 1870 Hz | reward, collect |
|
|
976
|
+
| `glitch` | 0.35 s | 0.22 s | ≈ 3970 Hz | digital error |
|
|
977
|
+
| `typewriter` | 0.06 s | start | ≈ 3260 Hz | one key (vary `seed` per key) |
|
|
978
|
+
|
|
979
|
+
Brightness is the spectral centroid that `movie.inspectAudio()` reports (`sound.brightnessHz`), at 48 kHz with no knobs.
|
|
980
|
+
|
|
794
981
|
### `composition`
|
|
795
982
|
|
|
796
983
|
Nested composition. Same shape as the root spec but with `type: 'composition'` and an explicit `width`/`height`. Children animate within the local coordinate system; the composition itself can be positioned, scaled, and rotated as a unit.
|
|
797
984
|
|
|
985
|
+
**Size, clipping and culling** (measured): a 2D composition does **not** clip its children — a child far outside `width × height` is still drawn. But the rectangle `width × height` is what the renderer uses to decide the group is on screen: when that whole rectangle is off the canvas the group is skipped even if a child is placed back on screen. So keep the rectangle around what it draws (or make it canvas-sized). A `threeD: true` composition (a card) is rendered into a texture of exactly `width × height`, so it **does** clip its children there; size it for soft shadows too.
|
|
986
|
+
|
|
798
987
|
```ts
|
|
799
988
|
{
|
|
800
989
|
type: 'composition',
|
|
@@ -835,10 +1024,21 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
|
|
|
835
1024
|
| `rect` | `width`, `height` | `cornerRadius` |
|
|
836
1025
|
| `circle` | `radius` | — |
|
|
837
1026
|
| `ellipse` | `radiusX`, `radiusY` | — |
|
|
1027
|
+
| `arc` | `radius` | `innerRadius`, `startAngle` (0), `endAngle` (360) |
|
|
838
1028
|
| `line` | `from: [x,y]`, `to: [x,y]` (canvas coordinates; stroke in `initial`) | — |
|
|
839
1029
|
| `polygon` | `points: [[x,y], …]` | `open` (default false) |
|
|
840
1030
|
| `path` | `d` (SVG path data) | — |
|
|
841
1031
|
|
|
1032
|
+
**`arc`** is an arc around its local origin (the centre). Angles are in **degrees: 0° is 3 o'clock and positive is clockwise**, so a progress ring starts at `startAngle: -90` (12 o'clock) and animates `endAngle` from `-90` to `270`. With a stroke and no fill it is an open arc line (`strokeCap: 'round'` for rounded ends); with a fill it is a sector (a pie slice), and with `innerRadius` a ring segment (a donut slice). A sweep of 360° or more is a full circle; `endAngle < startAngle` sweeps counter-clockwise. `radius`, `innerRadius`, `startAngle` and `endAngle` are all animatable.
|
|
1033
|
+
|
|
1034
|
+
```ts
|
|
1035
|
+
{ type: 'shape', shape: 'arc', radius: 120, startAngle: -90, endAngle: -90, strokeCap: 'round',
|
|
1036
|
+
initial: { x: 'GW/2', y: 'GH/2', strokeColor: '#ffd166', strokeWidth: 24 },
|
|
1037
|
+
keyframes: [{ at: 0, to: { endAngle: 270 }, duration: 2, ease: 'power3.out' }] } // a progress ring filling clockwise from the top
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
`strokeCap` (`'butt'`, `'round'`, `'square'`) and `strokeJoin` (`'miter'`, `'round'`, `'bevel'`) work on every shape, at the top level or in `initial`; they are not animated. Sharp corners (the apex of an M in outlined text, a thin polygon) poke out with the default `miter` join: use `'round'`.
|
|
1041
|
+
|
|
842
1042
|
**Style** is set on `initial` and animatable via keyframes:
|
|
843
1043
|
|
|
844
1044
|
| Key | Notes |
|
|
@@ -921,7 +1121,7 @@ Per-shape choice of how colour keyframes are interpolated. Default `'rgb'` (line
|
|
|
921
1121
|
|
|
922
1122
|
**Scalar geometry is animatable.** `width`, `height`, `cornerRadius` (rect), `radius` (circle), `radiusX` / `radiusY` (ellipse) all flow through the same per-frame redraw and can be tweened via keyframes — useful for progress bars (animate `width`), pulsing icons (animate `radius`), or shape-morph callouts. Array geometry (polygon `points`, line endpoints, path `d`) is baked at build time; use `scale` / `scaleX` / `scaleY` for those.
|
|
923
1123
|
|
|
924
|
-
`anchorX` / `anchorY` (default `0.5` each) control which point on the bbox sits at the local origin — `0` is left/top, `1` is right/bottom. They're animatable too. Critical for "grow from one edge" effects:
|
|
1124
|
+
For rect / circle / ellipse, `anchorX` / `anchorY` (default `0.5` each; not available on line / polygon / path, which use `pivotX` / `pivotY`) control which point on the bbox sits at the local origin — `0` is left/top, `1` is right/bottom. They're animatable too. Critical for "grow from one edge" effects:
|
|
925
1125
|
|
|
926
1126
|
```ts
|
|
927
1127
|
// Left-anchored progress bar — width animates 0 → W, left edge stays at x
|
|
@@ -1158,6 +1358,17 @@ When `true`, flip the mask sense: pixels INSIDE the mask shape become transparen
|
|
|
1158
1358
|
|
|
1159
1359
|
Routed through PIXI v8's native `setMask({ inverse: true })`.
|
|
1160
1360
|
|
|
1361
|
+
#### `blendMode`
|
|
1362
|
+
|
|
1363
|
+
How a layer blends with what is behind it: `'normal'` (default), `'add'`, `'screen'` or `'multiply'`. `add` and `screen` brighten, so overlapping glows, halos and light leaks add up instead of covering each other; `multiply` darkens. It works on every layer type, on `threeD` layers, and on a composition (its children inherit the mode, each blending with what is behind it). Any other value warns and is ignored.
|
|
1364
|
+
|
|
1365
|
+
```ts
|
|
1366
|
+
// A soft white glow that brightens whatever it overlaps
|
|
1367
|
+
{ type: 'shape', shape: 'circle', radius: 120, blendMode: 'add',
|
|
1368
|
+
fillGradient: { type: 'radial', stops: [[0, 'rgba(255,255,255,0.9)'], [1, 'rgba(255,255,255,0)']] },
|
|
1369
|
+
initial: { x: 'W/2', y: 'H/2' } }
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1161
1372
|
---
|
|
1162
1373
|
|
|
1163
1374
|
## Assets
|
|
@@ -1722,6 +1933,21 @@ contactSheet(options?: {
|
|
|
1722
1933
|
|
|
1723
1934
|
Many frames on **one** PNG, each labelled `frame N · T s`. The cheapest way to check a whole animation by eye (include the middle of every transition and the last second). Restores the current frame afterwards.
|
|
1724
1935
|
|
|
1936
|
+
### `measureText(text, style?): { width, height, lines }` and `splitText(text, style?, options?): TextPiece[]`
|
|
1937
|
+
|
|
1938
|
+
Text layout helpers, exported from `pixi-effects`. They measure with the renderer's own font metrics (the same numbers a text layer with that `style` has), so a layout built from them matches what is drawn. `style` is a text layer's `style` with numbers (no expressions). A web font must be loaded first.
|
|
1939
|
+
|
|
1940
|
+
```ts
|
|
1941
|
+
measureText('Hello', { fontSize: 72 }); // { width: 187.4, height: 81.3, lines: 1 }
|
|
1942
|
+
|
|
1943
|
+
interface SplitOptions { by?: 'chars' | 'words' | 'lines'; x?: number; y?: number; align?: 'left' | 'center' | 'right' }
|
|
1944
|
+
interface TextPiece { text: string; x: number; y: number; width: number; index: number; line: number }
|
|
1945
|
+
splitText('Hello world', style, { by: 'words', x: 640, y: 300, align: 'center' });
|
|
1946
|
+
// → [{ text: 'Hello', x, y, width, index: 0, line: 0 }, { text: 'world', ... }]
|
|
1947
|
+
```
|
|
1948
|
+
|
|
1949
|
+
`x` is the line's left edge, or its centre / right edge with `align` (each line of a `\n` text is aligned on its own width); `y` is the top of the first line, and further lines go down by the line height. Whitespace takes its room but is not returned as a piece. Use one text layer per piece with the same `style` and the default top-left anchor: `{ type: 'text', text: p.text, style, initial: { x: p.x, y: p.y } }`. Kerning is kept (a piece's x is the measured width of everything before it).
|
|
1950
|
+
|
|
1725
1951
|
### `movie.inspect(frame?, options?): Promise<InspectReport>`
|
|
1726
1952
|
|
|
1727
1953
|
Where every layer is drawn at `frame`, as JSON — for checking layout without eyes. Seeks there and stays there. `options.layers`: `'visible'` (default, only layers drawn at this frame), `'all'`, or `'none'` (just `summary` and `issues`). Faint layers (alpha < 0.3) and text whose x / y is animated (tickers) are not reported.
|
|
@@ -1742,6 +1968,27 @@ interface InspectReport {
|
|
|
1742
1968
|
}
|
|
1743
1969
|
```
|
|
1744
1970
|
|
|
1971
|
+
### `movie.inspectAudio(options?): AudioReport`
|
|
1972
|
+
|
|
1973
|
+
The soundtrack as numbers — for checking sound without listening. Every sound is also measured on its own, so overlaps do not blur it. `options.window`: seconds per row of `windows` (default 0.1).
|
|
1974
|
+
|
|
1975
|
+
```ts
|
|
1976
|
+
interface AudioReport {
|
|
1977
|
+
duration: number; sampleRate: number;
|
|
1978
|
+
peakDb: number; peakAt: number; // the mix before limiting; > 0 dBFS means it was limited
|
|
1979
|
+
sources: Array<{
|
|
1980
|
+
layer: string; source: string; // 'layer "hit"', 'sfx "hit"' | 'asset "bgm"' | 'video "clip"'
|
|
1981
|
+
start: number; end: number; // movie time
|
|
1982
|
+
peakDb: number; // the MIX while it plays
|
|
1983
|
+
sound: { length: number; peakDb: number; loudestAt: number; brightnessHz: number }; // the sound alone
|
|
1984
|
+
}>;
|
|
1985
|
+
windows: Array<{ t: number; rmsDb: number; peakDb: number; brightnessHz: number }>;
|
|
1986
|
+
issues: string[]; // limited mix, inaudible layer, sfx cut off by the end, no audio — read first
|
|
1987
|
+
}
|
|
1988
|
+
```
|
|
1989
|
+
|
|
1990
|
+
`render()` encodes this same mix: AAC in mp4 / mov (the encoder delay is trimmed with an edit list), Opus in webm / mkv.
|
|
1991
|
+
|
|
1745
1992
|
### `movie.render(options?): Promise<Blob>`
|
|
1746
1993
|
|
|
1747
1994
|
Renders the entire timeline to a single video file. Pauses playback first.
|
package/llms.txt
CHANGED
|
@@ -14,7 +14,7 @@ Rules that cause most failures (details in the skill and pitfalls):
|
|
|
14
14
|
|
|
15
15
|
- [Skill: workflow and rules](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/SKILL.md): how to build, check and export a video; read first
|
|
16
16
|
- [Cheatsheet](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/reference/cheatsheet.md): every layer type, prop, default and convention on one page
|
|
17
|
-
- [Recipes](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/reference/recipes.md): tested building blocks (kinetic type, lower-third, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions, three.js)
|
|
17
|
+
- [Recipes](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/reference/recipes.md): tested building blocks (kinetic type, lower-third, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions, sound effects without files, three.js)
|
|
18
18
|
- [Pitfalls](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/reference/pitfalls.md): real mistakes made by AI authors, with fixes
|
|
19
19
|
- [Starter template](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/template.html): copy this file to begin
|
|
20
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixi-effects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"main": "./dist/index.cjs",
|
|
21
21
|
"module": "./dist/index.js",
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
|
+
"bin": { "pixi-effects-check": "ai/tools/check.mjs" },
|
|
23
24
|
"files": ["dist", "ai", "docs/dsl.md", "docs/api.md", "llms.txt", "llms-full.txt", "CHANGELOG.md", "README.md", "LICENSE"],
|
|
24
25
|
"keywords": [
|
|
25
26
|
"pixi", "pixijs", "video", "composition", "compositing", "rendering",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"test:watch": "vitest",
|
|
40
41
|
"typecheck": "tsc --noEmit",
|
|
41
42
|
"build:ai": "node scripts/build-llms.mjs",
|
|
43
|
+
"build:gallery": "node scripts/build-gallery.mjs",
|
|
42
44
|
"prepublishOnly": "npm run build:ai && npm run typecheck && npm test && npm run build"
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunkZL262ZRM_cjs = require('./chunk-ZL262ZRM.cjs');
|
|
4
|
-
require('./chunk-SIRVULFF.cjs');
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Object.defineProperty(exports, "CompositionSequence", {
|
|
9
|
-
enumerable: true,
|
|
10
|
-
get: function () { return chunkZL262ZRM_cjs.CompositionSequence; }
|
|
11
|
-
});
|
|
12
|
-
//# sourceMappingURL=Composition-6FDH5OPM.cjs.map
|
|
13
|
-
//# sourceMappingURL=Composition-6FDH5OPM.cjs.map
|