pixi-effects 0.5.0 → 0.7.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 (54) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +17 -10
  3. package/ai/SKILL.md +1 -1
  4. package/ai/reference/cheatsheet.md +8 -2
  5. package/ai/reference/pitfalls.md +1 -0
  6. package/ai/reference/recipes.md +36 -1
  7. package/ai/template.html +4 -3
  8. package/ai/tools/check.mjs +14 -5
  9. package/ai/tools/render.mjs +202 -0
  10. package/ai/tools/view.mjs +269 -0
  11. package/dist/{Base-DnRAxroQ.d.cts → Base-CSIulyIq.d.cts} +1 -1
  12. package/dist/{Base-CPSFBkPm.d.ts → Base-DVgwyIUj.d.ts} +1 -1
  13. package/dist/Composition-3KHQPRBA.cjs +14 -0
  14. package/dist/{Composition-63EXHNJJ.cjs.map → Composition-3KHQPRBA.cjs.map} +1 -1
  15. package/dist/Composition-DT657FO5.js +5 -0
  16. package/dist/{Composition-TUCRSMJU.js.map → Composition-DT657FO5.js.map} +1 -1
  17. package/dist/Controller.cjs +77 -2
  18. package/dist/Controller.cjs.map +1 -1
  19. package/dist/Controller.d.cts +16 -2
  20. package/dist/Controller.d.ts +16 -2
  21. package/dist/Controller.js +77 -2
  22. package/dist/Controller.js.map +1 -1
  23. package/dist/{Movie-CuU_aaCm.d.cts → Movie-B0OUKlZI.d.ts} +54 -2
  24. package/dist/{Movie-VyDiUYOj.d.ts → Movie-BK9daiTt.d.cts} +54 -2
  25. package/dist/{chunk-5Q5XZG7Z.js → chunk-36Z4RIRK.js} +3 -3
  26. package/dist/{chunk-5Q5XZG7Z.js.map → chunk-36Z4RIRK.js.map} +1 -1
  27. package/dist/{chunk-UBPZLGLU.js → chunk-4O2NYDJG.js} +365 -44
  28. package/dist/chunk-4O2NYDJG.js.map +1 -0
  29. package/dist/{chunk-23M5QTWW.cjs → chunk-4VSIJLU3.cjs} +21 -21
  30. package/dist/{chunk-23M5QTWW.cjs.map → chunk-4VSIJLU3.cjs.map} +1 -1
  31. package/dist/{chunk-2RR43OIN.cjs → chunk-6I55T4OH.cjs} +403 -62
  32. package/dist/chunk-6I55T4OH.cjs.map +1 -0
  33. package/dist/index.cjs +234 -24
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +4 -4
  36. package/dist/index.d.ts +4 -4
  37. package/dist/index.js +226 -16
  38. package/dist/index.js.map +1 -1
  39. package/dist/three.cjs +6 -5
  40. package/dist/three.cjs.map +1 -1
  41. package/dist/three.d.cts +2 -2
  42. package/dist/three.d.ts +2 -2
  43. package/dist/three.js +2 -1
  44. package/dist/three.js.map +1 -1
  45. package/dist/{types-7VtTEKUW.d.ts → types-oJijbn-K.d.cts} +36 -7
  46. package/dist/{types-7VtTEKUW.d.cts → types-oJijbn-K.d.ts} +36 -7
  47. package/docs/api.md +7 -0
  48. package/docs/dsl.md +74 -8
  49. package/llms-full.txt +127 -12
  50. package/package.json +7 -4
  51. package/dist/Composition-63EXHNJJ.cjs +0 -13
  52. package/dist/Composition-TUCRSMJU.js +0 -4
  53. package/dist/chunk-2RR43OIN.cjs.map +0 -1
  54. package/dist/chunk-UBPZLGLU.js.map +0 -1
package/llms-full.txt CHANGED
@@ -14,7 +14,7 @@ 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 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.
17
+ 3. **Show the human:** `node ai/tools/view.mjs my-video.html` opens their browser on the page with a scrubbable, zoomable timeline under it (click / drag to seek, + / − or Ctrl/⌘ + wheel to zoom) (`npx pixi-effects-view …` in an installed package); it keeps running until Ctrl-C. **Deliver the file:** `node ai/tools/render.mjs my-video.html -o my-video.mp4` (`npx pixi-effects-render …` in an installed package) renders the page headless to mp4 / webm / mov / mkv and exits 0 only when the file is written; run it AFTER the check below passes. **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 writes `timeline.html` (every layer as a bar on a time axis: ◆ keyframes, shaded transitions; hand it to the human to open) 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
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.
19
19
  4. **Look at it** — you cannot judge motion from code. The library has the tools built in:
20
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.
@@ -91,7 +91,7 @@ Operators `+ - * /`, parentheses, unary `-`. Functions `min max abs floor ceil r
91
91
  name, at, duration,
92
92
  initial: { ...props applied before any keyframe },
93
93
  keyframes: [ { at, duration, ease, set | to | from | (from + to), repeat?, yoyo?, repeatDelay? } ],
94
- filters: [ { type: 'chromaKey', keyColor, threshold, smoothing, spill } | { type: 'custom', name, filter: <Pixi Filter> } ],
94
+ filters: [ { type: 'chromaKey', keyColor, threshold, smoothing, spill } | { type: 'glow', name?, outerStrength: 3, color: '#ffd166' } /* any named filter, see below */ | { type: 'custom', name, filter: <Pixi Filter> } ],
95
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
96
96
  filterArea: { x, y, width, height } // in the layer's OWN coordinates; lets blur/glow draw past the layer's bounds
97
97
  threeD: true // opt into 2.5D (see below)
@@ -104,7 +104,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
104
104
 
105
105
  ## Layer types
106
106
 
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).
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. **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).
108
108
 
109
109
  **image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'`. Default anchor top-left; natural size = `w`,`h`.
110
110
 
@@ -125,6 +125,8 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
125
125
  | polygon | `points: [[x,y],…] open` — canvas coordinates like `line` |
126
126
  | path | `d` (SVG path data) — canvas coordinates like `line` |
127
127
 
128
+ **Draw-on:** every shape but `arc` has `trimStart` / `trimEnd` (0–1 of the outline, default 0 / 1, animatable): `trimEnd: 0` + keyframe `to: { trimEnd: 1 }` draws a line / border / path on; `trimStart` 0 → 1 after it wipes it off; both = a travelling dash. The fill is NOT trimmed. Rect outline starts top-left, circle / ellipse at 12 o'clock, both clockwise. Use `strokeCap: 'round'`.
129
+
128
130
  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).
129
131
 
130
132
  **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.
@@ -137,6 +139,10 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
137
139
 
138
140
  Add `threeD: true` to any visual layer, then use `z`, `rotationX`, `rotationY` (centre rotation with `anchorX/Y: 0.5` or `pivotX/Y`). Add a `{ type: 'camera' }` layer for a view. A `threeD` layer at `z: 0` with the default camera looks identical to a 2D one. Consecutive `threeD` layers are drawn farthest-first (equal `z` keeps array order); non-`threeD` layers ignore the camera and keep array order. Layers at/behind the camera plane are hidden. Not supported on `threeD` layers: masks, transitions, `filterArea`.
139
141
 
142
+ ## Filters by name (no import)
143
+
144
+ `filters: [{ type: 'glow', name: 'halo', outerStrength: 3, color: '#ffd166' }]` — `type` = the pixi / pixi-filters class name without `Filter`, camelCase (`glow dropShadow outline blur noise alpha adjustment hslAdjustment grayscale pixelate crt rgbSplit oldFilm glitch bulgePinch twist zoomBlur kawaseBlur radialBlur motionBlur emboss ascii colorOverlay advancedBloom bevel dot crossHatch tiltShift …`), the other keys are its options. `colorMatrix` takes `preset: 'sepia'|'grayscale'|'negative'|'polaroid'|'technicolor'|'vintage'|'kodachrome'|'browni'`. Name it to animate: `'filters.halo.outerStrength'` (scalars only; `pixelate` via `sizeX`/`sizeY`). Options: `glow` distance outerStrength innerStrength color alpha · `dropShadow` offset{x,y} blur alpha color · `outline` thickness color · `blur` strength · `adjustment` gamma contrast saturation brightness · `hslAdjustment` hue lightness saturation · `pixelate` size · `crt` curvature lineWidth noise vignetting · `rgbSplit` red/green/blue {x,y} · `oldFilm` sepia noise scratch vignetting · `glitch` slices offset · `twist` radius angle offset · `bulgePinch` center radius strength · `zoomBlur` strength center innerRadius · `colorOverlay` color alpha. **Centres/offsets are CANVAS pixels** (default top-left corner): put them on the layer. Glow / shadow / blur draw outside the layer: widen `filterArea` (not for `grayscale` / `oldFilm`, which fill the margin black). Full table: `docs/dsl.md` → Named filters.
145
+
140
146
  ## Transitions (on the parent composition)
141
147
 
142
148
  ```js
@@ -269,6 +275,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
269
275
  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
276
  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
277
  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)*
278
+ 53. **Draw-on and typewriter need no masks or per-letter layers.** `trimStart` / `trimEnd` (0–1 of the outline, animatable, every shape but `arc`) draw a stroke on: `trimEnd: 0` plus a keyframe `to: { trimEnd: 1 }`. The fill is not trimmed, so fade `fillAlpha` in afterwards; use `strokeCap: 'round'`. Text changes over time with `set: { text }` (swap at a time) and `visibleChars` (typewriter; left-align it so it grows rightward). A `to: { text }` cannot tween a string and warns. *(fixed)*
272
279
 
273
280
 
274
281
  <!-- ===== ai/reference/recipes.md ===== -->
@@ -527,7 +534,7 @@ return [
527
534
 
528
535
  ## Slideshow with transitions and background music
529
536
 
530
- Scene `i` starts at `i × (L − T)` and overlaps the next by the transition length `T`; the transition's `at` is the next scene's start; total = `n × (L − T) + T`. Transitions and `kenBurns` combine freely. Captions are top-level layers (transitions never touch layers they do not name); keep them above the bottom ~60 px. `bgm.mp3` is only 6 s: **`loop: true`** or it falls silent.
537
+ Scene `i` starts at `i × (L − T)` and overlaps the next by the transition length `T`; the transition's `at` is the next scene's start; total = `n × (L − T) + T`. Transitions and `kenBurns` combine freely. Captions are top-level layers (transitions never touch layers they do not name); keep them above the bottom ~60 px. A music file shorter than the movie (`bgm.mp3` is a 16 s loop) needs **`loop: true`** or it falls silent.
531
538
 
532
539
  ```js
533
540
  // @recipe slideshow
@@ -647,6 +654,41 @@ return [
647
654
 
648
655
  ---
649
656
 
657
+ ## Draw a line, border or check mark on, and type text out
658
+
659
+ `trimEnd` 0 → 1 strokes a shape's outline on (`trimStart` 0 → 1 afterwards wipes it off); the fill is NOT trimmed, so fade `fillAlpha` in once the outline is done. A rect's outline starts top-left and goes clockwise, a circle's at 12 o'clock. `strokeCap: 'round'` rounds the ends. For text, `visibleChars` types it out and `set: { text }` swaps the string at a time.
660
+
661
+ ```js
662
+ // @recipe draw-on-and-type
663
+ const ACCENT = '#ffd166', TEAL = '#4cc9f0', GREEN = '#7bd88f';
664
+ const drawOn = (at, duration = 1.2) => ({ at, to: { trimEnd: 1 }, duration, ease: 'power2.inOut' });
665
+ return [
666
+ { type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0a0e1a' } },
667
+ // an underline that draws itself
668
+ { type: 'shape', shape: 'line', from: [340, 120], to: [940, 120], trimEnd: 0, strokeCap: 'round',
669
+ initial: { strokeColor: ACCENT, strokeWidth: 6 }, keyframes: [drawOn(0.2, 1)] },
670
+ // a card border (clockwise from the top-left), then its fill fades in
671
+ { type: 'shape', shape: 'rect', width: 360, height: 200, cornerRadius: 28, trimEnd: 0, strokeJoin: 'round',
672
+ initial: { x: 400, y: 360, strokeColor: TEAL, strokeWidth: 8, fillColor: TEAL, fillAlpha: 0 },
673
+ keyframes: [drawOn(0.6, 1.4), { at: 2, to: { fillAlpha: 0.2 }, duration: 0.6 }] },
674
+ // an SVG check mark
675
+ { type: 'shape', shape: 'path', d: 'M 0 50 L 45 95 L 130 0', trimEnd: 0, strokeCap: 'round', strokeJoin: 'round',
676
+ initial: { x: 880, y: 360, strokeColor: GREEN, strokeWidth: 16 }, keyframes: [drawOn(1.8, 0.9)] },
677
+ // a typewriter: left-aligned so it grows rightward; 30 characters over 2.4 s
678
+ { type: 'text', text: 'No mask, no per-letter layers.', at: 3, duration: 5,
679
+ style: { fontSize: 40, fill: '#e8eefc', fontFamily: 'ui-monospace, Menlo, monospace' },
680
+ initial: { x: 120, y: 560, anchorX: 0, anchorY: 0.5, visibleChars: 0 },
681
+ keyframes: [{ at: 0, to: { visibleChars: 30 }, duration: 2.4, ease: 'none' }] },
682
+ // a countdown: one layer whose string changes
683
+ { type: 'text', text: '3', at: 3, duration: 5,
684
+ style: { fontSize: 150, fontWeight: '900', fill: ACCENT },
685
+ initial: { x: 1060, y: 540, anchorX: 0.5, anchorY: 0.5 },
686
+ keyframes: [{ at: 1, set: { text: '2' } }, { at: 2, set: { text: '1' } }, { at: 3, set: { text: 'Go!' } }] },
687
+ ];
688
+ ```
689
+
690
+ ---
691
+
650
692
  ## Letters and words that animate one by one (`splitText`)
651
693
 
652
694
  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.
@@ -842,7 +884,7 @@ A layer is **not removed** when its lifespan `[at, at + duration)` ends: it is o
842
884
 
843
885
  ### `text`
844
886
 
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.
887
+ 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 can change over time (see *Changing text* below): a `{value}` placeholder prints an animatable number (counters), a keyframe `set: { text: '…' }` swaps the string, and `visibleChars` types it out. 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.
846
888
 
847
889
  ```ts
848
890
  {
@@ -874,6 +916,17 @@ The text's `fill` is also animatable via keyframes (under the `to`/`from`/`set`
874
916
  }
875
917
  ```
876
918
 
919
+ #### Changing text: `set: { text }` and `visibleChars`
920
+
921
+ A keyframe `set: { text: 'Two' }` swaps the string at that time (layer-local `at`, undone when you seek back; a string cannot be tweened, so `to` / `from` with `text` warn). A countdown is one layer:
922
+
923
+ ```ts
924
+ { type: 'text', text: '3', duration: 4, style: { fontSize: 150 }, initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
925
+ keyframes: [{ at: 1, set: { text: '2' } }, { at: 2, set: { text: '1' } }, { at: 3, set: { text: 'Go!' } }] }
926
+ ```
927
+
928
+ `visibleChars` is a number that shows only the first N characters (counted as you see them: an emoji is one): a typewriter. `initial: { visibleChars: 0 }` and a keyframe `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` types 30 characters over 2.4 s (it starts at 0 as soon as it is animated). Use a left-aligned layer (`anchorX: 0`, `style.align: 'left'`) so the text grows to the right instead of re-centring each letter; the layer's `w` / `h` are those of the whole text. `set: { text }` and `visibleChars` combine (swap the string, `set: { visibleChars: 0 }`, type it again), and so does a `{value}` placeholder in the string.
929
+
877
930
  #### Counters (`{value}`)
878
931
 
879
932
  A text layer has one animatable number, `value`, printed wherever the text contains `{value}`. Animate it with the normal keyframes (any ease, `from`/`to`/`set`, `repeat`):
@@ -1021,13 +1074,13 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
1021
1074
 
1022
1075
  | `shape` | Required props | Optional |
1023
1076
  |-----------|------------------------------------------|------------------------|
1024
- | `rect` | `width`, `height` | `cornerRadius` |
1025
- | `circle` | `radius` | — |
1026
- | `ellipse` | `radiusX`, `radiusY` | — |
1077
+ | `rect` | `width`, `height` | `cornerRadius`, `trimStart`, `trimEnd` |
1078
+ | `circle` | `radius` | `trimStart`, `trimEnd` |
1079
+ | `ellipse` | `radiusX`, `radiusY` | `trimStart`, `trimEnd` |
1027
1080
  | `arc` | `radius` | `innerRadius`, `startAngle` (0), `endAngle` (360) |
1028
- | `line` | `from: [x,y]`, `to: [x,y]` (canvas coordinates; stroke in `initial`) | — |
1029
- | `polygon` | `points: [[x,y], …]` | `open` (default false) |
1030
- | `path` | `d` (SVG path data) | — |
1081
+ | `line` | `from: [x,y]`, `to: [x,y]` (canvas coordinates; stroke in `initial`) | `trimStart`, `trimEnd` |
1082
+ | `polygon` | `points: [[x,y], …]` | `open` (default false), `trimStart`, `trimEnd` |
1083
+ | `path` | `d` (SVG path data) | `trimStart`, `trimEnd` |
1031
1084
 
1032
1085
  **`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
1086
 
@@ -1037,6 +1090,14 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
1037
1090
  keyframes: [{ at: 0, to: { endAngle: 270 }, duration: 2, ease: 'power3.out' }] } // a progress ring filling clockwise from the top
1038
1091
  ```
1039
1092
 
1093
+ **Draw-on (`trimStart` / `trimEnd`).** On every shape except `arc`, `trimStart` and `trimEnd` (0–1, default 0 and 1, both animatable) are the part of the outline that is stroked, as fractions of its length. Animate `trimEnd` from 0 to 1 to draw a line, border or SVG path on; animate `trimStart` after it to wipe it off the same way; animate both for a travelling dash. A rect's outline starts at its top-left and goes clockwise, a circle / ellipse's at 12 o'clock and goes clockwise, a polygon's includes its closing edge, and a path's several sub-paths count as one length in order. **The fill is not trimmed** (the whole shape stays filled): fade `fillAlpha` in after the draw-on. Use `strokeCap: 'round'` for a friendly end. For `arc`, animate `endAngle` instead (a `trim…` on an arc warns).
1094
+
1095
+ ```ts
1096
+ { type: 'shape', shape: 'path', d: 'M 0 50 L 45 95 L 130 0', trimEnd: 0, strokeCap: 'round', strokeJoin: 'round',
1097
+ initial: { x: 1040, y: 270, strokeColor: '#7bd88f', strokeWidth: 16 },
1098
+ keyframes: [{ at: 0, to: { trimEnd: 1 }, duration: 1, ease: 'power2.inOut' }] } // a check mark drawing itself
1099
+ ```
1100
+
1040
1101
  `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
1102
 
1042
1103
  **Style** is set on `initial` and animatable via keyframes:
@@ -1686,6 +1747,53 @@ Removes a key color from the source. Works on video, image, or composition layer
1686
1747
 
1687
1748
  Animatable: `threshold`, `smoothing`, `spill`. `keyColor` is set at build time.
1688
1749
 
1750
+ ### Named filters (`{ type: 'glow', … }`)
1751
+
1752
+ A filter by name, as plain data. `type` is the filter's class name without `Filter`, in camelCase (case does not matter), and every other key is that filter's own option. No import, and the layer stays JSON:
1753
+
1754
+ ```ts
1755
+ { type: 'text', text: 'NEON', style: { fontSize: 140, fill: '#fff' }, initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
1756
+ filters: [{ type: 'glow', name: 'halo', outerStrength: 0, color: '#ff4d9d', distance: 20 }],
1757
+ keyframes: [{ at: 0, to: { 'filters.halo.outerStrength': 6 }, duration: 1 }] } // animate any scalar option by name
1758
+ ```
1759
+
1760
+ Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `blur`, `noise`, `alpha` and `colorMatrix` come from pixi.js. All the others come from the **`pixi-filters`** package, which is loaded the first time a layer uses one (add `"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js"` to the import map, or `npm i pixi-filters`; `ai/template.html` already has it). A misspelt type says what you probably meant. Verified rendering and exporting in a real browser (`examples/_checks/named-filters.html`, 24 filters):
1761
+
1762
+ | `type` | Options (the useful ones) | What it does |
1763
+ |---|---|---|
1764
+ | `blur` | `strength`, `quality` | Gaussian blur (pixi.js) |
1765
+ | `noise` | `noise` (0–1), `seed` | Film grain (pixi.js) |
1766
+ | `alpha` | `alpha` | Whole-layer opacity (pixi.js) |
1767
+ | `colorMatrix` | `preset`: `'sepia' 'grayscale' 'negative' 'polaroid' 'technicolor' 'vintage' 'kodachrome' 'browni'`, or `matrix` (20 numbers) | Colour grade (pixi.js) |
1768
+ | `adjustment` | `gamma contrast saturation brightness red green blue alpha` (1 = unchanged) | Colour grading |
1769
+ | `hslAdjustment` | `hue` (degrees) `saturation lightness colorize alpha` | Hue shift |
1770
+ | `grayscale` | – | Black and white |
1771
+ | `glow` | `distance outerStrength innerStrength color alpha quality knockout` | Glow around the shape |
1772
+ | `dropShadow` | `offset: { x, y }` `blur alpha color shadowOnly` | Shadow |
1773
+ | `outline` | `thickness color alpha quality knockout` | Stroke around the shape |
1774
+ | `pixelate` | `size` (a number, or `[w, h]`); animate `sizeX` / `sizeY` | Mosaic |
1775
+ | `crt` | `curvature lineWidth lineContrast noise vignetting vignettingAlpha time` | Old monitor |
1776
+ | `rgbSplit` | `red green blue` (each `{ x, y }` offset) | Chromatic aberration |
1777
+ | `oldFilm` | `sepia noise scratch scratchDensity vignetting seed` | Aged film |
1778
+ | `glitch` | `slices offset direction seed average` | Digital glitch |
1779
+ | `bulgePinch` | `center radius strength` | Bulge (+) / pinch (−) |
1780
+ | `twist` | `radius angle offset: { x, y }` | Swirl |
1781
+ | `zoomBlur` | `strength center innerRadius radius` | Radial zoom streaks |
1782
+ | `kawaseBlur` | `strength quality` | Cheap, wide blur |
1783
+ | `radialBlur` / `motionBlur` | `angle center radius` / `velocity kernelSize` | Spin / directional blur |
1784
+ | `emboss` | `strength` | Relief |
1785
+ | `ascii` | `size color replaceColor` | Text-art |
1786
+ | `colorOverlay` | `color alpha` | Tint over everything |
1787
+ | `advancedBloom` / `bloom` | `threshold bloomScale brightness blur` | Bloom |
1788
+ | `bevel`, `dot`, `crossHatch`, `tiltShift`, `reflection`, `shockwave`, `godray`, `simplexNoise`, `colorGradient`, `colorMap`, `colorReplace`, `multiColorReplace`, `convolution`, `backdropBlur`, `simpleLightmap`, `tiltShiftAxis` | see the [pixi-filters docs](https://pixijs.io/filters/docs/) | Other pixi-filters |
1789
+
1790
+ Gotchas, all measured:
1791
+
1792
+ - **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.
1793
+ - **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.
1794
+ - **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`.
1795
+ - Some filters take their value as one argument; the DSL hides that (`{ type: 'pixelate', size: 8 }`, `{ type: 'emboss', strength: 6 }`).
1796
+
1689
1797
  ### `custom`
1690
1798
 
1691
1799
  Escape hatch for any PIXI `Filter` instance — including PIXI's own built-ins (`BlurFilter`, `ColorMatrixFilter`, `NoiseFilter`, etc.), [pixi-filters](https://github.com/pixijs/filters), community packages, or your own `Filter` subclass. The instance is used as-is; animation works the same way as for `chromaKey` as long as the filter has writable scalar properties at the addressed paths.
@@ -1719,7 +1827,7 @@ Notes:
1719
1827
  Notes:
1720
1828
 
1721
1829
  - `filter` must be a PIXI `Filter` instance (constructor must have run on the consumer side). Plain object literals throw.
1722
- - `pixi-filters` is **not** a dependency of pixi-effects — install it on your side if you want to use it.
1830
+ - Use `custom` for a filter that has no name above (a community filter, or your own `Filter`). For everything in pixi.js and `pixi-filters`, prefer the named form.
1723
1831
  - Without a `name`, the filter still applies but cannot be addressed via `filters.<name>.<prop>` keyframe paths.
1724
1832
 
1725
1833
  ---
@@ -2011,6 +2119,12 @@ Returns a `Blob` whose `type` is the container's MIME (e.g. `video/mp4`). Emits
2011
2119
 
2012
2120
  The renderer also forces a keyframe every ~2 seconds so the resulting file scrubs efficiently in standard players.
2013
2121
 
2122
+ ### `movie.timelineData(): TimelineData` and `movie.timelineChart(options?): string`
2123
+
2124
+ The whole movie as rows on a time axis, for a person to read at a glance. `timelineData()` is the data: `{ duration, rows, transitions }`, a row per layer with `name`, `type`, `depth` (1 inside a composition), absolute `start` / `end` in seconds, `keys` (when each keyframe starts) and, for a family of generated layers, `parts` (one span per layer). Layers of one type whose names differ only by numbers (`ring10-0`, `ring9-1`, `pop-3` …), four or more, become ONE row (`ring#-# ×19`), so a movie with 700 generated layers is a screenful. `timelineChart({ title })` returns one self-contained HTML page (an inline SVG, no scripts): bars coloured by type, ◆ at each keyframe start, a shaded band per transition, a tooltip per bar. `pixi-effects-check` writes it next to the contact sheet as `timeline.html`.
2125
+
2126
+ `movie.timelineSvg()` is the chart alone (one `<svg>`, geometry in `data-*` attributes, each row `<g class="row" data-start>`): `pixi-effects-view` puts a playhead on it and turns a click into a seek.
2127
+
2014
2128
  ### `movie.destroy(): Promise<void>`
2015
2129
 
2016
2130
  Pauses playback, destroys the underlying PIXI Application, releases audio buffers and AudioContext, and marks the instance unusable.
@@ -2026,6 +2140,7 @@ movie.off(event, fn): this
2026
2140
  | ----------- | ------------------------------------------------- | ----------------------------------------------------- |
2027
2141
  | `'ready'` | none | once, after `init()` resolves |
2028
2142
  | `'frame'` | `{ frame: number; totalFrames: number }` | every `gotoFrame` (so once per playback frame too) |
2143
+ | `'play'` | none | when `play()` actually transitions from paused |
2029
2144
  | `'pause'` | none | when `pause()` actually transitions from playing |
2030
2145
  | `'progress'`| `{ progress: number; frame: number; totalFrames: number }` | during `render()`, once per encoded frame |
2031
2146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-effects",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,7 +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
+ "bin": { "pixi-effects-check": "ai/tools/check.mjs", "pixi-effects-render": "ai/tools/render.mjs", "pixi-effects-view": "ai/tools/view.mjs" },
24
24
  "files": ["dist", "ai", "docs/dsl.md", "docs/api.md", "llms.txt", "llms-full.txt", "CHANGELOG.md", "README.md", "LICENSE"],
25
25
  "keywords": [
26
26
  "pixi", "pixijs", "video", "composition", "compositing", "rendering",
@@ -49,10 +49,12 @@
49
49
  "peerDependencies": {
50
50
  "gsap": "^3.12.0",
51
51
  "pixi.js": "^8.0.0",
52
- "three": ">=0.150.0"
52
+ "three": ">=0.150.0",
53
+ "pixi-filters": "^6.1.0"
53
54
  },
54
55
  "peerDependenciesMeta": {
55
- "three": { "optional": true }
56
+ "three": { "optional": true },
57
+ "pixi-filters": { "optional": true }
56
58
  },
57
59
  "devDependencies": {
58
60
  "tsup": "^8.0.0",
@@ -61,6 +63,7 @@
61
63
  "happy-dom": "^15.0.0",
62
64
  "gsap": "^3.12.0",
63
65
  "pixi.js": "^8.22.0",
66
+ "pixi-filters": "^6.1.5",
64
67
  "three": "^0.185.1",
65
68
  "@types/three": "^0.185.0"
66
69
  },
@@ -1,13 +0,0 @@
1
- 'use strict';
2
-
3
- var chunk23M5QTWW_cjs = require('./chunk-23M5QTWW.cjs');
4
- require('./chunk-2RR43OIN.cjs');
5
-
6
-
7
-
8
- Object.defineProperty(exports, "CompositionSequence", {
9
- enumerable: true,
10
- get: function () { return chunk23M5QTWW_cjs.CompositionSequence; }
11
- });
12
- //# sourceMappingURL=Composition-63EXHNJJ.cjs.map
13
- //# sourceMappingURL=Composition-63EXHNJJ.cjs.map
@@ -1,4 +0,0 @@
1
- export { CompositionSequence } from './chunk-5Q5XZG7Z.js';
2
- import './chunk-UBPZLGLU.js';
3
- //# sourceMappingURL=Composition-TUCRSMJU.js.map
4
- //# sourceMappingURL=Composition-TUCRSMJU.js.map