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.
- package/CHANGELOG.md +30 -0
- package/README.md +17 -10
- package/ai/SKILL.md +1 -1
- package/ai/reference/cheatsheet.md +8 -2
- package/ai/reference/pitfalls.md +1 -0
- package/ai/reference/recipes.md +36 -1
- package/ai/template.html +4 -3
- package/ai/tools/check.mjs +14 -5
- package/ai/tools/render.mjs +202 -0
- package/ai/tools/view.mjs +269 -0
- package/dist/{Base-DnRAxroQ.d.cts → Base-CSIulyIq.d.cts} +1 -1
- package/dist/{Base-CPSFBkPm.d.ts → Base-DVgwyIUj.d.ts} +1 -1
- package/dist/Composition-3KHQPRBA.cjs +14 -0
- package/dist/{Composition-63EXHNJJ.cjs.map → Composition-3KHQPRBA.cjs.map} +1 -1
- package/dist/Composition-DT657FO5.js +5 -0
- package/dist/{Composition-TUCRSMJU.js.map → Composition-DT657FO5.js.map} +1 -1
- package/dist/Controller.cjs +77 -2
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +16 -2
- package/dist/Controller.d.ts +16 -2
- package/dist/Controller.js +77 -2
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-CuU_aaCm.d.cts → Movie-B0OUKlZI.d.ts} +54 -2
- package/dist/{Movie-VyDiUYOj.d.ts → Movie-BK9daiTt.d.cts} +54 -2
- package/dist/{chunk-5Q5XZG7Z.js → chunk-36Z4RIRK.js} +3 -3
- package/dist/{chunk-5Q5XZG7Z.js.map → chunk-36Z4RIRK.js.map} +1 -1
- package/dist/{chunk-UBPZLGLU.js → chunk-4O2NYDJG.js} +365 -44
- package/dist/chunk-4O2NYDJG.js.map +1 -0
- package/dist/{chunk-23M5QTWW.cjs → chunk-4VSIJLU3.cjs} +21 -21
- package/dist/{chunk-23M5QTWW.cjs.map → chunk-4VSIJLU3.cjs.map} +1 -1
- package/dist/{chunk-2RR43OIN.cjs → chunk-6I55T4OH.cjs} +403 -62
- package/dist/chunk-6I55T4OH.cjs.map +1 -0
- package/dist/index.cjs +234 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +226 -16
- package/dist/index.js.map +1 -1
- package/dist/three.cjs +6 -5
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +2 -1
- package/dist/three.js.map +1 -1
- package/dist/{types-7VtTEKUW.d.ts → types-oJijbn-K.d.cts} +36 -7
- package/dist/{types-7VtTEKUW.d.cts → types-oJijbn-K.d.ts} +36 -7
- package/docs/api.md +7 -0
- package/docs/dsl.md +74 -8
- package/llms-full.txt +127 -12
- package/package.json +7 -4
- package/dist/Composition-63EXHNJJ.cjs +0 -13
- package/dist/Composition-TUCRSMJU.js +0 -4
- package/dist/chunk-2RR43OIN.cjs.map +0 -1
- 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.
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
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.
|
|
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
|