pixi-effects 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +5 -5
  3. package/ai/chat-template.html +3 -3
  4. package/ai/tools/render.mjs +19 -2
  5. package/dist/{Base-jRiMHcWT.d.ts → Base-CXsMdmJB.d.ts} +1 -1
  6. package/dist/{Base-BKagtWc0.d.cts → Base-pNq6sVGl.d.cts} +1 -1
  7. package/dist/Composition-LNMKWXSL.cjs +16 -0
  8. package/dist/{Composition-V2XYSW6P.cjs.map → Composition-LNMKWXSL.cjs.map} +1 -1
  9. package/dist/Composition-RF7QOEXT.js +7 -0
  10. package/dist/{Composition-DQCPJPFE.js.map → Composition-RF7QOEXT.js.map} +1 -1
  11. package/dist/Controller.d.cts +2 -2
  12. package/dist/Controller.d.ts +2 -2
  13. package/dist/{Movie-BH81stkb.d.cts → Movie-CL6SsAWe.d.cts} +1 -1
  14. package/dist/{Movie-CfFPItoL.d.ts → Movie-f0tQG9y5.d.ts} +1 -1
  15. package/dist/Spans-NQGKVMR5.cjs +55 -0
  16. package/dist/Spans-NQGKVMR5.cjs.map +1 -0
  17. package/dist/Spans-SDXABOV6.js +53 -0
  18. package/dist/Spans-SDXABOV6.js.map +1 -0
  19. package/dist/{blendModes-RIVZXL5A.js → blendModes-MVRLG3T7.js} +4 -4
  20. package/dist/{blendModes-RIVZXL5A.js.map → blendModes-MVRLG3T7.js.map} +1 -1
  21. package/dist/{blendModes-ETMYWATF.cjs → blendModes-VTKDXWEN.cjs} +5 -5
  22. package/dist/{blendModes-ETMYWATF.cjs.map → blendModes-VTKDXWEN.cjs.map} +1 -1
  23. package/dist/{chunk-MMTESMJ5.js → chunk-AW5NPDE4.js} +1363 -1151
  24. package/dist/chunk-AW5NPDE4.js.map +1 -0
  25. package/dist/{chunk-RCVZX2JP.js → chunk-C5BNHVKK.js} +36 -3
  26. package/dist/chunk-C5BNHVKK.js.map +1 -0
  27. package/dist/{chunk-L73KRFXI.cjs → chunk-CRUDOKSD.cjs} +91 -91
  28. package/dist/chunk-CRUDOKSD.cjs.map +1 -0
  29. package/dist/{chunk-BGMCZA36.js → chunk-D5BFQVKL.js} +6 -6
  30. package/dist/chunk-D5BFQVKL.js.map +1 -0
  31. package/dist/{chunk-LYOKH7KE.cjs → chunk-FCGYJCVO.cjs} +36 -3
  32. package/dist/chunk-FCGYJCVO.cjs.map +1 -0
  33. package/dist/{chunk-UTIBYMIO.js → chunk-FCMOREIS.js} +10 -6
  34. package/dist/chunk-FCMOREIS.js.map +1 -0
  35. package/dist/{chunk-X3WHETQ3.cjs → chunk-K3MAEKFM.cjs} +42 -38
  36. package/dist/chunk-K3MAEKFM.cjs.map +1 -0
  37. package/dist/{chunk-4KB3GUEK.js → chunk-QFNS6BFL.js} +4 -4
  38. package/dist/{chunk-4KB3GUEK.js.map → chunk-QFNS6BFL.js.map} +1 -1
  39. package/dist/{chunk-DJ5B7IKT.cjs → chunk-QHDN6TF5.cjs} +1377 -1161
  40. package/dist/chunk-QHDN6TF5.cjs.map +1 -0
  41. package/dist/{chunk-2DXPTKQZ.cjs → chunk-Z437UWI4.cjs} +5 -5
  42. package/dist/{chunk-2DXPTKQZ.cjs.map → chunk-Z437UWI4.cjs.map} +1 -1
  43. package/dist/index.cjs +33 -33
  44. package/dist/index.d.cts +5 -5
  45. package/dist/index.d.ts +5 -5
  46. package/dist/index.js +5 -5
  47. package/dist/loader.css +1 -1
  48. package/dist/present.cjs +5 -5
  49. package/dist/present.d.cts +2 -2
  50. package/dist/present.d.ts +2 -2
  51. package/dist/present.js +5 -5
  52. package/dist/three.cjs +6 -6
  53. package/dist/three.d.cts +2 -2
  54. package/dist/three.d.ts +2 -2
  55. package/dist/three.js +2 -2
  56. package/dist/{types-CK_crNAI.d.ts → types-CavnUvd5.d.cts} +26 -1
  57. package/dist/{types-CK_crNAI.d.cts → types-CavnUvd5.d.ts} +26 -1
  58. package/docs/dsl.md +14 -4
  59. package/llms-full.txt +19 -8
  60. package/package.json +1 -1
  61. package/skills/pixi-effects/SKILL.md +1 -1
  62. package/skills/pixi-effects/reference/cheatsheet.md +2 -2
  63. package/skills/pixi-effects/reference/pitfalls.md +2 -2
  64. package/skills/pixi-effects/template.html +6 -6
  65. package/dist/Composition-DQCPJPFE.js +0 -7
  66. package/dist/Composition-V2XYSW6P.cjs +0 -16
  67. package/dist/chunk-BGMCZA36.js.map +0 -1
  68. package/dist/chunk-DJ5B7IKT.cjs.map +0 -1
  69. package/dist/chunk-L73KRFXI.cjs.map +0 -1
  70. package/dist/chunk-LYOKH7KE.cjs.map +0 -1
  71. package/dist/chunk-MMTESMJ5.js.map +0 -1
  72. package/dist/chunk-RCVZX2JP.js.map +0 -1
  73. package/dist/chunk-UTIBYMIO.js.map +0 -1
  74. package/dist/chunk-X3WHETQ3.cjs.map +0 -1
package/llms-full.txt CHANGED
@@ -6,7 +6,7 @@ Concatenation of the skill, cheatsheet, pitfalls, recipes and the DSL / API refe
6
6
 
7
7
  <!-- ===== skills/pixi-effects/SKILL.md ===== -->
8
8
 
9
- > Written for pixi-effects 0.31.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
9
+ > Written for pixi-effects 0.32.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
10
10
 
11
11
  # pixi-effects — write a video as data
12
12
 
@@ -159,7 +159,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
159
159
 
160
160
  ## Layer types
161
161
 
162
- **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. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change).
162
+ **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. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change). **Colour inside one line:** `spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'a'", fill: '#ffd166', fontWeight: '700' }, ',\n next line']` instead of `text` (strings use the layer's `style`; a span changes only fill / fontWeight / fontStyle / fontVariant / letterSpacing / dropShadow, never a size, font or stroke (those are the layer's `style`); `\n` starts a line; `style.align: 'left'` for code; `visibleChars` types over all the spans; it becomes a group of text layers `<name>.0…`).
163
163
 
164
164
  **image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'`. Default anchor top-left; natural size = `w`,`h`.
165
165
 
@@ -264,7 +264,7 @@ measureText('Hello', style) // → { width, height,
264
264
  splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
265
265
  // → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
266
266
  ```
267
- 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.
267
+ 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 (leave `out` out and the pieces stay until the layer ends); 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.
268
268
 
269
269
  ## Assets
270
270
 
@@ -421,7 +421,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
421
421
  93. **A `threeD` layer's shadow budget is four casters per receiver, counted in layer order, not geometry.** Five letters as five `castsShadows` layers over one floor: the fifth casts nothing (the warning names it). Make the word ONE text layer (the light is per pixel, each letter is still lit on its own) or give only the four that matter `castsShadows`. Splitting the floor does not help: each half takes the first four.
422
422
  94. **`resolution` below 1 softens crisp lines in a shader.** `fwidth` anti-aliased edges come out a little blurry after the upscale: keep `resolution: 1` for line art and use 0.5 only for soft fields (flowing colour, glow).
423
423
  95. **A `color-dodge` leak over a dark picture is a muddy red disc.** Dodge divides each channel of near-black separately, red first. Cut the dodge to the bright subject with a matte (`mask: 'letters'`, a matte-only copy of the word) and put the warm haze in a separate `screen` radial.
424
- 96. **A filter option that is a point (`rgbSplit.red`, `glitch` offsets) cannot be tweened as a whole**, but a scalar can: `set: { 'filters.tear.seed': n }` steps `glitch.seed` (a `set` keyframe works on filter options; no warning, visible tearing). To switch between looks on one subject, put the subject in time-windowed `composition` copies, each carrying the filters of its look.
424
+ 96. **A filter option that is a point (`rgbSplit.red`, `glitch` offsets) is tweened one leaf at a time** (`'filters.s.red.x'`; the whole point as a value switches, it does not tween), and a scalar too: `set: { 'filters.tear.seed': n }` steps `glitch.seed` (a `set` keyframe works on filter options; no warning, visible tearing). To switch between looks on one subject, put the subject in time-windowed `composition` copies, each carrying the filters of its look.
425
425
  97. **A deck inside your own composition loses its `transitions` and `stops`** unless you take them across: `const d = deck({ width: 1920, height: 1080, pages })`, then `{ type: 'composition', sequences: d.composition.sequences, transitions: d.composition.transitions }`. Pass `width` / `height` yourself for a page that is not the movie's own size.
426
426
  98. **A matte layer needs a fill.** A named matte (`mask: 'glass'`) that is a shape with no `fillColor` / `fillGradient` (or a text with no fill) cuts everything away and nothing warns: the layers it should show simply vanish. Give the matte a fill (white for `alpha`, a gradient for `luma`).
427
427
  99. **In a `music` score a capital letter is a chord and a lowercase one a note.** `G7` is G dominant seventh, `C5` a power chord, `C6` a sixth chord; `g7`, `c5`, `c6` are notes (octave 7 is a very high note: it used to be what `G7` meant, silently). A bare note in capitals (`Bb2`) is refused with the lowercase spelling (`bb2`); inside brackets (`[C4 E4 G4]`) any case is a note. An unknown chord kind suggests the nearest (`Cmin7` → `Cm7`, `Csus` → `Csus4`, `CM7` → `Cmaj7`).
@@ -432,6 +432,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
432
432
  104. **A variable moves values, not timing, and it is read when the composition is built.** `at` and `duration` are plain numbers; `vars: { shift }` works in `x: 'W/2 + shift'` and `'{title}'` inside any string, not in `at: 'shift'`. `setVars` builds everything again (about 20 ms, plus the sound only if it changed): for a continuous input call it as often as you like, calls that overlap are joined, but do not use it for something that should move every frame (that is a keyframe).
433
433
  105. **Layout that follows the screen is written in `W` / `H` / `GW` / `GH`, never in pixels.** After `movie.resize(w, h)` an expression sees the new size; a literal `x: 960` stays at 960. A string variable cannot be read in an expression (`x: 'title'` is a number context): use `{title}` in a string, or declare the number. A name that is declared but never read, and a `{name}` that was not declared, are both said at `init` (usually a typo). Load a web font BEFORE the first `setVars` that changes a text (see 103): the text is measured again each rebuild.
434
434
  106. **A text outline used to spike; it is round-joined now.** `style: { stroke: { color: '#000', width: 12 } }` was drawn with PixiJS's default `miter` join, so the sharp corners of W, V, A, k and 4 grew spikes ("the outline sticks out"). The library now adds `join: 'round'` to an outline that names no join; say `join: 'miter'` or `'bevel'` if you want one. The stroke is centred on the glyph edge, so half of it eats into the letter: keep `width` to about a tenth of `fontSize`, or for a thick outline put a second copy of the text with the stroke UNDER the plain one.
435
+ 107. **`spans` places pieces by measuring, so the font must be loaded first, and a span cannot change size.** A web font that is not loaded yet is measured as the fallback font and the pieces drift apart: `await document.fonts.load('30px "Name"')` before `movie.init`. `fontSize` / `fontFamily` / `lineHeight` / `align` on a span are ignored (warned): they belong to the layer's `style`. `fill`, `text` and `value` keyframes do not reach a spans layer (it is a group of text layers).
435
436
 
436
437
 
437
438
  <!-- ===== skills/pixi-effects/reference/recipes.md ===== -->
@@ -1870,7 +1871,7 @@ interface SequenceCommon {
1870
1871
  | Layer | Where `x, y` land by default | To change it |
1871
1872
  |---|---|---|
1872
1873
  | `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` |
1873
- | `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.** |
1874
+ | `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.** A `path` whose `d` is already written in canvas coordinates takes no `x` / `y` at all (they would move its centre to that point); to move it, change the coordinates or wrap it in a composition. |
1874
1875
  | `text`, `image`, `video` | the **top-left** corner | `anchorX` / `anchorY: 0.5` to centre |
1875
1876
  | `composition` | its top-left corner | `pivotX` / `pivotY` = the point that sits at `x, y` and that rotation / scale turn about |
1876
1877
 
@@ -1940,6 +1941,16 @@ A text layer has one animatable number, `value`, printed wherever the text conta
1940
1941
 
1941
1942
  `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.
1942
1943
 
1944
+ **Colour inside one text layer: `spans`.** Instead of `text`, write an array of strings (drawn in the layer's own `style`) and `{ text, fill, … }` objects; they are shown one after the other, and `\n` inside a span starts a new line:
1945
+
1946
+ ```ts
1947
+ { type: 'text', name: 'code', style: { fontFamily: 'Menlo', fontSize: 30, fill: '#e8f5e8', align: 'left' },
1948
+ spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'text'", fill: '#ffd166', fontWeight: '700' }, ',\n at: 0.5'],
1949
+ initial: { x: 120, y: 200 } }
1950
+ ```
1951
+
1952
+ A span may change `fill`, `fontWeight`, `fontStyle`, `fontVariant`, `letterSpacing` and `dropShadow`; a size, a font, `lineHeight`, `align` or a `stroke` belongs to the layer's `style` (one outline per layer: a stroke shifts a piece by half its width; a span that sets one of these is warned about once and the key is ignored; `style.wordWrap` and `style.leading` do not apply either: the lines are the `\n` of the spans). `style.align` places the lines on the width of the block (`'left'`, `'center'` — the default, as for every text — or `'right'`). At build time the layer becomes a group (a composition) of ordinary text layers, one per piece of a line, named `<name>.0`, `<name>.1` …, each at the x it has when the pieces are measured one after the other (the same measuring as `splitText`), so `inspect`, the timeline view and `check` see them. The group carries the layer's `name`, `at`, `duration`, `initial` (`anchorX` / `anchorY` are numbers, 0.5 centres the whole block, and cannot be keyframed), `keyframes` (`x`, `y`, `alpha`, `scale`, `rotation`, …), `filters`, `mask` and `blendMode`. A **typewriter** works over all the spans: `initial: { visibleChars: 0 }` and a keyframe `to: { visibleChars: N }` count every character in order, the newline included. `fill`, `text`, `value`, `fillGradient`, `format` and `colorSpace` do not apply to a group (a warning says so): use a separate text layer, or put the colour in a span. Measuring needs the font loaded (`await document.fonts.load('30px "Name"')` for a web font), as for `splitText`; the size and spacing in `style` may be numbers, expressions (`'GH * 0.05'`) or declared variables: they are resolved before the pieces are measured, and a missing `fontSize` is measured as the text layer's own default (Arial, 36).
1953
+
1943
1954
  ### `image`
1944
1955
 
1945
1956
  Renders a [PIXI.Sprite](https://pixijs.com/8.x/guides/components/scene-objects/sprite/sprite) from a registered asset. Intrinsic `w`/`h` come from the loaded texture.
@@ -3076,7 +3087,7 @@ A scaled punch-in / punch-out. By default `B` opens up: it starts large and zoom
3076
3087
 
3077
3088
  ### `dissolve`
3078
3089
 
3079
- Pixel-grain noise reveal driven by deterministic 2D Perlin noise. Pixels with a low noise value reveal first; as `uProgress` advances, more pixels reveal. The same `seed` always produces the same dissolve pattern, so a render is bit-exact reproducible.
3090
+ A noise reveal driven by deterministic 2D Perlin noise. Pixels with a low noise value reveal first; as `uProgress` advances, more pixels reveal. At the default `scale` of 30 it reads as large blotches (about 40 px each), not as grain: `scale: 200` or more gives a fine grain. The same `seed` always produces the same dissolve pattern, so a render is bit-exact reproducible.
3080
3091
 
3081
3092
  ```ts
3082
3093
  {
@@ -3186,7 +3197,7 @@ Gotchas, all measured:
3186
3197
  - **Distortion:** `warp` (`wave`: a flag or the surface of water, `haze`: heat shimmer; moves with the frame's time, no keyframes needed, and pads the layer by `strength` so a shifted edge is not cut), `twist` (a swirl), `bulgePinch` (a lens), `shockwave` (a ring that spreads), `rgbSplit` (colour fringes), `glitch` (slices). `warp`'s time is the time of the frame (inside a time-remapped composition, that composition's clock), `speed: 0` stands still, `strength` is in px and is capped at 200. It bends the layer's picture; to draw something that does not exist yet, use a `shader` layer.
3187
3198
  - **Centres and offsets are in canvas pixels, not the layer's:** `twist.offset`, `bulgePinch.center`, `zoomBlur.center`, `radialBlur.center`, `shockwave.center` default to the canvas's top-left corner, so a layer in the middle looks unaffected. Put the centre on the layer: `{ type: 'twist', offset: { x: 640, y: 360 } }` for a layer centred on a 1280×720 canvas.
3188
3199
  - **A filter works inside the layer's bounds.** Glow, dropShadow, outline and blur draw outside it and are clipped unless you widen the area (`filterArea: { x: -24, y: -24, width: w + 48, height: h + 48 }`, in the layer's own coordinates). Do not widen it for `grayscale`, `oldFilm` or other filters that paint the whole area: they fill the extra margin with black.
3189
- - **Animate scalars by name** (`'filters.halo.outerStrength'`). Options that are points (`rgbSplit.red`, `dropShadow.offset`) do not tween as a whole; animate `'filters.<name>.red.x'` if the filter exposes it, or swap the filter. `pixelate` animates through `sizeX` / `sizeY`, not `size`.
3200
+ - **Animate scalars by name** (`'filters.halo.outerStrength'`). Options that are points (`rgbSplit.red`, `dropShadow.offset`) are animated one leaf at a time: `'filters.<name>.red.x'` / `'filters.<name>.red.y'` tween like any number (the whole point `red: { x, y }` as a keyframe value switches at its keyframe and does not tween; a path that leads nowhere is warned about once and ignored). `pixelate` animates through `sizeX` / `sizeY`, not `size`.
3190
3201
  - `shockwave` and `godray` are driven by `time` (a still frame at `time: 0` looks like nothing): animate it. `radialBlur`, `shockwave` and `tiltShift` positions are canvas pixels too.
3191
3202
  - Some filters take their value as one argument; the DSL hides that (`{ type: 'pixelate', size: 8 }`, `{ type: 'emboss', strength: 6 }`).
3192
3203
 
@@ -3217,7 +3228,7 @@ import { GlowFilter, OldFilmFilter } from 'pixi-filters';
3217
3228
  Notes:
3218
3229
 
3219
3230
  - `filter` must be a `Filter` instance (constructor must have run on the consumer side).
3220
- - Animation paths use scalar property setters. Filters whose properties are PointData (e.g. `pixi-filters` `RGBSplitFilter` exposes `red: { x, y }`) currently don't propagate the change to the GPU uniform when only `.x` is mutated — replace the whole point in an `onUpdate` callback or use a scalar-API filter instead.
3231
+ - A dotted key (`'filters.s.red.x'`) is a path into the filter: the leaf of the point is written in place, which `pixi-filters` reads when it draws (checked on `RGBSplitFilter` in a real browser, `tests/tools/pointOptions.test.ts`). Before 0.32 such a key was written as a property literally named `red.x` and nothing moved.
3221
3232
  - Without a `name`, the filter still applies but cannot be addressed via `filters.<name>.<prop>` keyframe paths.
3222
3233
 
3223
3234
  Notes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-effects",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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.31.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
6
+ > Written for pixi-effects 0.32.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
 
@@ -50,7 +50,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
50
50
 
51
51
  ## Layer types
52
52
 
53
- **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. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change).
53
+ **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. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change). **Colour inside one line:** `spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'a'", fill: '#ffd166', fontWeight: '700' }, ',\n next line']` instead of `text` (strings use the layer's `style`; a span changes only fill / fontWeight / fontStyle / fontVariant / letterSpacing / dropShadow, never a size, font or stroke (those are the layer's `style`); `\n` starts a line; `style.align: 'left'` for code; `visibleChars` types over all the spans; it becomes a group of text layers `<name>.0…`).
54
54
 
55
55
  **image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'`. Default anchor top-left; natural size = `w`,`h`.
56
56
 
@@ -155,7 +155,7 @@ measureText('Hello', style) // → { width, height,
155
155
  splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
156
156
  // → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
157
157
  ```
158
- 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
+ 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 (leave `out` out and the pieces stay until the layer ends); 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.
159
159
 
160
160
  ## Assets
161
161
 
@@ -123,7 +123,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
123
123
  93. **A `threeD` layer's shadow budget is four casters per receiver, counted in layer order, not geometry.** Five letters as five `castsShadows` layers over one floor: the fifth casts nothing (the warning names it). Make the word ONE text layer (the light is per pixel, each letter is still lit on its own) or give only the four that matter `castsShadows`. Splitting the floor does not help: each half takes the first four.
124
124
  94. **`resolution` below 1 softens crisp lines in a shader.** `fwidth` anti-aliased edges come out a little blurry after the upscale: keep `resolution: 1` for line art and use 0.5 only for soft fields (flowing colour, glow).
125
125
  95. **A `color-dodge` leak over a dark picture is a muddy red disc.** Dodge divides each channel of near-black separately, red first. Cut the dodge to the bright subject with a matte (`mask: 'letters'`, a matte-only copy of the word) and put the warm haze in a separate `screen` radial.
126
- 96. **A filter option that is a point (`rgbSplit.red`, `glitch` offsets) cannot be tweened as a whole**, but a scalar can: `set: { 'filters.tear.seed': n }` steps `glitch.seed` (a `set` keyframe works on filter options; no warning, visible tearing). To switch between looks on one subject, put the subject in time-windowed `composition` copies, each carrying the filters of its look.
126
+ 96. **A filter option that is a point (`rgbSplit.red`, `glitch` offsets) is tweened one leaf at a time** (`'filters.s.red.x'`; the whole point as a value switches, it does not tween), and a scalar too: `set: { 'filters.tear.seed': n }` steps `glitch.seed` (a `set` keyframe works on filter options; no warning, visible tearing). To switch between looks on one subject, put the subject in time-windowed `composition` copies, each carrying the filters of its look.
127
127
  97. **A deck inside your own composition loses its `transitions` and `stops`** unless you take them across: `const d = deck({ width: 1920, height: 1080, pages })`, then `{ type: 'composition', sequences: d.composition.sequences, transitions: d.composition.transitions }`. Pass `width` / `height` yourself for a page that is not the movie's own size.
128
128
  98. **A matte layer needs a fill.** A named matte (`mask: 'glass'`) that is a shape with no `fillColor` / `fillGradient` (or a text with no fill) cuts everything away and nothing warns: the layers it should show simply vanish. Give the matte a fill (white for `alpha`, a gradient for `luma`).
129
129
  99. **In a `music` score a capital letter is a chord and a lowercase one a note.** `G7` is G dominant seventh, `C5` a power chord, `C6` a sixth chord; `g7`, `c5`, `c6` are notes (octave 7 is a very high note: it used to be what `G7` meant, silently). A bare note in capitals (`Bb2`) is refused with the lowercase spelling (`bb2`); inside brackets (`[C4 E4 G4]`) any case is a note. An unknown chord kind suggests the nearest (`Cmin7` → `Cm7`, `Csus` → `Csus4`, `CM7` → `Cmaj7`).
@@ -134,4 +134,4 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
134
134
  104. **A variable moves values, not timing, and it is read when the composition is built.** `at` and `duration` are plain numbers; `vars: { shift }` works in `x: 'W/2 + shift'` and `'{title}'` inside any string, not in `at: 'shift'`. `setVars` builds everything again (about 20 ms, plus the sound only if it changed): for a continuous input call it as often as you like, calls that overlap are joined, but do not use it for something that should move every frame (that is a keyframe).
135
135
  105. **Layout that follows the screen is written in `W` / `H` / `GW` / `GH`, never in pixels.** After `movie.resize(w, h)` an expression sees the new size; a literal `x: 960` stays at 960. A string variable cannot be read in an expression (`x: 'title'` is a number context): use `{title}` in a string, or declare the number. A name that is declared but never read, and a `{name}` that was not declared, are both said at `init` (usually a typo). Load a web font BEFORE the first `setVars` that changes a text (see 103): the text is measured again each rebuild.
136
136
  106. **A text outline used to spike; it is round-joined now.** `style: { stroke: { color: '#000', width: 12 } }` was drawn with PixiJS's default `miter` join, so the sharp corners of W, V, A, k and 4 grew spikes ("the outline sticks out"). The library now adds `join: 'round'` to an outline that names no join; say `join: 'miter'` or `'bevel'` if you want one. The stroke is centred on the glyph edge, so half of it eats into the letter: keep `width` to about a tenth of `fontSize`, or for a thick outline put a second copy of the text with the stroke UNDER the plain one.
137
-
137
+ 107. **`spans` places pieces by measuring, so the font must be loaded first, and a span cannot change size.** A web font that is not loaded yet is measured as the fallback font and the pieces drift apart: `await document.fonts.load('30px "Name"')` before `movie.init`. `fontSize` / `fontFamily` / `lineHeight` / `align` on a span are ignored (warned): they belong to the layer's `style`. `fill`, `text` and `value` keyframes do not reach a spans layer (it is a group of text layers).
@@ -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.31.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 -->
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.32.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,16 +37,16 @@
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.31.0/dist/index.js",
41
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.31.0/dist/Controller.js",
42
- "pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.31.0/dist/present.js",
43
- "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.31.0/dist/three.js",
40
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.32.0/dist/index.js",
41
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.32.0/dist/Controller.js",
42
+ "pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.32.0/dist/present.js",
43
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.32.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.31.0/dist/index.js" /> <!-- after the import map: the library downloads while the page is parsed -->
49
+ <link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.32.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';
@@ -1,7 +0,0 @@
1
- export { CompositionSequence } from './chunk-BGMCZA36.js';
2
- import './chunk-MMTESMJ5.js';
3
- import './chunk-4KB3GUEK.js';
4
- import './chunk-RCVZX2JP.js';
5
- import './chunk-BXOCEZ4U.js';
6
- //# sourceMappingURL=Composition-DQCPJPFE.js.map
7
- //# sourceMappingURL=Composition-DQCPJPFE.js.map
@@ -1,16 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkL73KRFXI_cjs = require('./chunk-L73KRFXI.cjs');
4
- require('./chunk-DJ5B7IKT.cjs');
5
- require('./chunk-2DXPTKQZ.cjs');
6
- require('./chunk-LYOKH7KE.cjs');
7
- require('./chunk-TWZNOUM7.cjs');
8
-
9
-
10
-
11
- Object.defineProperty(exports, "CompositionSequence", {
12
- enumerable: true,
13
- get: function () { return chunkL73KRFXI_cjs.CompositionSequence; }
14
- });
15
- //# sourceMappingURL=Composition-V2XYSW6P.cjs.map
16
- //# sourceMappingURL=Composition-V2XYSW6P.cjs.map