pixi-effects 0.26.3 → 0.27.1

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 (73) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +12 -10
  3. package/ai/CHAT.md +1 -1
  4. package/ai/chat-template.html +4 -6
  5. package/dist/{Base-CpdDs4HT.d.ts → Base-CKGl6jCo.d.ts} +2 -4
  6. package/dist/{Base-TbBhHPrG.d.cts → Base-DjhdL8B1.d.cts} +2 -4
  7. package/dist/Composition-BJVY5HP7.js +7 -0
  8. package/dist/{Composition-3O7L2VL5.js.map → Composition-BJVY5HP7.js.map} +1 -1
  9. package/dist/Composition-YW2G6EXL.cjs +16 -0
  10. package/dist/{Composition-EE3KEE2L.cjs.map → Composition-YW2G6EXL.cjs.map} +1 -1
  11. package/dist/Controller.d.cts +2 -3
  12. package/dist/Controller.d.ts +2 -3
  13. package/dist/{Movie-BkP82KXS.d.ts → Movie-CbYwTs6U.d.cts} +2 -3
  14. package/dist/{Movie-BWLLWRRe.d.cts → Movie-ILKq2fw_.d.ts} +2 -3
  15. package/dist/Presenter.d.cts +2 -3
  16. package/dist/Presenter.d.ts +2 -3
  17. package/dist/{blendModes-YV43K6E7.cjs → blendModes-HDOP6R7T.cjs} +5 -5
  18. package/dist/{blendModes-YV43K6E7.cjs.map → blendModes-HDOP6R7T.cjs.map} +1 -1
  19. package/dist/{blendModes-G3NOAU3T.js → blendModes-PAVTW3BO.js} +4 -4
  20. package/dist/{blendModes-G3NOAU3T.js.map → blendModes-PAVTW3BO.js.map} +1 -1
  21. package/dist/{chunk-TLYTYBWX.js → chunk-45QKLD6M.js} +9 -11
  22. package/dist/chunk-45QKLD6M.js.map +1 -0
  23. package/dist/{chunk-WMMZDPWK.js → chunk-AR3HVHKF.js} +7 -11
  24. package/dist/chunk-AR3HVHKF.js.map +1 -0
  25. package/dist/{chunk-A225HYSY.cjs → chunk-ETKTRQL2.cjs} +970 -258
  26. package/dist/chunk-ETKTRQL2.cjs.map +1 -0
  27. package/dist/{chunk-3DLYAPK7.cjs → chunk-P4GY3Z4G.cjs} +5 -5
  28. package/dist/{chunk-3DLYAPK7.cjs.map → chunk-P4GY3Z4G.cjs.map} +1 -1
  29. package/dist/{chunk-AXZUKNXF.js → chunk-TYWFKCPY.js} +4 -4
  30. package/dist/{chunk-AXZUKNXF.js.map → chunk-TYWFKCPY.js.map} +1 -1
  31. package/dist/{chunk-IIDBEYQH.cjs → chunk-VH5R473P.cjs} +91 -95
  32. package/dist/chunk-VH5R473P.cjs.map +1 -0
  33. package/dist/{chunk-FXAHNPBP.js → chunk-X42BGR6P.js} +950 -241
  34. package/dist/chunk-X42BGR6P.js.map +1 -0
  35. package/dist/{chunk-ECA2WNRX.cjs → chunk-ZMAD3KJU.cjs} +9 -11
  36. package/dist/chunk-ZMAD3KJU.cjs.map +1 -0
  37. package/dist/index.cjs +102 -105
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +16 -14
  40. package/dist/index.d.ts +16 -14
  41. package/dist/index.js +48 -33
  42. package/dist/index.js.map +1 -1
  43. package/dist/loader.css +1 -1
  44. package/dist/{music-3O5MDQXY.js → music-JVRNXHQ5.js} +34 -7
  45. package/dist/music-JVRNXHQ5.js.map +1 -0
  46. package/dist/{music-4O367SPR.cjs → music-YPUZTWTN.cjs} +34 -6
  47. package/dist/music-YPUZTWTN.cjs.map +1 -0
  48. package/dist/three.cjs +6 -6
  49. package/dist/three.d.cts +2 -3
  50. package/dist/three.d.ts +2 -3
  51. package/dist/three.js +2 -2
  52. package/dist/{types-DSGwN5w1.d.cts → types-BNVsBBZf.d.cts} +130 -3
  53. package/dist/{types-DSGwN5w1.d.ts → types-BNVsBBZf.d.ts} +130 -3
  54. package/docs/api.md +5 -1
  55. package/docs/dsl.md +13 -15
  56. package/llms-full.txt +24 -22
  57. package/llms.txt +1 -1
  58. package/package.json +4 -5
  59. package/skills/pixi-effects/SKILL.md +1 -1
  60. package/skills/pixi-effects/reference/cheatsheet.md +1 -1
  61. package/skills/pixi-effects/reference/pitfalls.md +3 -3
  62. package/skills/pixi-effects/reference/recipes.md +1 -1
  63. package/skills/pixi-effects/template.html +6 -8
  64. package/dist/Composition-3O7L2VL5.js +0 -7
  65. package/dist/Composition-EE3KEE2L.cjs +0 -16
  66. package/dist/chunk-A225HYSY.cjs.map +0 -1
  67. package/dist/chunk-ECA2WNRX.cjs.map +0 -1
  68. package/dist/chunk-FXAHNPBP.js.map +0 -1
  69. package/dist/chunk-IIDBEYQH.cjs.map +0 -1
  70. package/dist/chunk-TLYTYBWX.js.map +0 -1
  71. package/dist/chunk-WMMZDPWK.js.map +0 -1
  72. package/dist/music-3O5MDQXY.js.map +0 -1
  73. package/dist/music-4O367SPR.cjs.map +0 -1
@@ -1,3 +1,128 @@
1
+ type EaseFn = (p: number) => number;
2
+
3
+ /**
4
+ * A timeline that is a pure function of time (spike: see docs/superpowers/specs/2026-10-10-pure-timeline-design.md).
5
+ *
6
+ * For every (target, property) it keeps the segments that write it, ordered by start time. The value at `t`
7
+ * is the value of the last segment that has started, computed from `t` alone; nothing is remembered between
8
+ * two calls except the start values captured when the segment was built. GSAP is the oracle: the rules below
9
+ * were taken from black-box probes of `gsap.timeline` and are checked against it by tests/core/pure/timelineParity.test.ts.
10
+ */
11
+ interface PureVars {
12
+ duration?: number;
13
+ ease?: string | EaseFn;
14
+ repeat?: number;
15
+ yoyo?: boolean;
16
+ repeatDelay?: number;
17
+ onUpdate?: () => void;
18
+ /** The Pixi plugin's shorthands: `scale`, `anchor`, `rotation` (degrees), `tint`, `autoAlpha` ... */
19
+ pixi?: Record<string, unknown>;
20
+ [prop: string]: unknown;
21
+ }
22
+ /** One thing a timeline writes: see `segments()`. */
23
+ interface SegmentInfo {
24
+ target: object;
25
+ /** The property (`'alpha'`, `'scale.x'`, `'fill'`). */
26
+ id: string;
27
+ start: number;
28
+ duration: number;
29
+ repeat: number;
30
+ yoyo: boolean;
31
+ repeatDelay: number;
32
+ kind: 'set' | 'to' | 'from' | 'fromTo';
33
+ }
34
+ /** A marker that only lengthens a timeline (what `gsap.to({}, { duration })` is used for). */
35
+ interface Spacer {
36
+ readonly spacer: true;
37
+ readonly duration: number;
38
+ }
39
+ declare class PureTimeline {
40
+ private readonly defaultEase;
41
+ private readonly channels;
42
+ private readonly channelList;
43
+ private readonly children;
44
+ private spans;
45
+ private now;
46
+ private order;
47
+ private killed;
48
+ private prepared;
49
+ constructor(opts?: {
50
+ paused?: boolean;
51
+ defaults?: {
52
+ ease?: string | EaseFn;
53
+ };
54
+ });
55
+ /** Adds a child timeline (played with its local time) or a spacer that lengthens this one. */
56
+ add(child: PureTimeline | Spacer, at?: number): this;
57
+ set(target: object, vars: PureVars, at?: number): this;
58
+ to(target: object, vars: PureVars, at?: number): this;
59
+ from(target: object, vars: PureVars, at?: number): this;
60
+ fromTo(target: object, fromVars: PureVars, toVars: PureVars, at?: number): this;
61
+ private build;
62
+ private channel;
63
+ /**
64
+ * A property moved by a function of the caller (a colour in OKLab, a gradient): `to` and `from` say how to get the two ends from
65
+ * the value before the segment, `make` the interpolator between them. The value is only written while the segment is the last
66
+ * one that has started, so nothing is read or kept between two frames.
67
+ */
68
+ tweenValue<T>(spec: {
69
+ holder: object;
70
+ id: string;
71
+ get(): T;
72
+ set(v: T): void;
73
+ from?: (prev: T) => T;
74
+ to: (start: T, prev: T, base: T) => T;
75
+ make: (a: T, b: T) => (p: number) => T;
76
+ duration: number;
77
+ ease: string | EaseFn;
78
+ at: number;
79
+ repeat?: number;
80
+ yoyo?: boolean;
81
+ repeatDelay?: number;
82
+ }): this;
83
+ /**
84
+ * A write that is undone when time goes back before it: the property is `value` from `at` on, and what it was before otherwise.
85
+ * `holder` and `id` name the property, so a set and the tweens of the same property are one channel; `write` is the way the caller
86
+ * writes it (it may do more than assign: redraw), and runs again whenever this set is what the property shows.
87
+ */
88
+ setValue<T>(holder: object, id: string, at: number, read: () => T, write: (v: T) => void, value: T): this;
89
+ /** What the timeline will write, for tools and tests: every segment of this timeline and of the ones under it, starts in this timeline's time. */
90
+ segments(offset?: number): SegmentInfo[];
91
+ /** How many timelines are directly under this one. */
92
+ timelines(): number;
93
+ duration(): number;
94
+ time(): number;
95
+ time(value: number): this;
96
+ /** GSAP's name for `time(t)`. */
97
+ seek(t: number): this;
98
+ progress(): number;
99
+ progress(p: number): this;
100
+ kill(): void;
101
+ /**
102
+ * What GSAP ended up with once it had been played to the end and back (what Movie used to do at build), as rules that depend
103
+ * on the segments of a property only:
104
+ * - a `from` / `fromTo` shows its start from the moment it is built, so a `to` that comes first starts at the start of the
105
+ * `from` built last, else at the property's own value;
106
+ * - a `to` (and a `set`) starts where the segment before it ended;
107
+ * - a `from` ends where the segment before it ended, or, with none before, where the property was when it was built;
108
+ * - before the first segment the property shows that segment's start.
109
+ */
110
+ private prepare;
111
+ /** Every channel of this timeline and of its children reads the property it starts from before any of them writes. */
112
+ private prepareDeep;
113
+ /** How many channels write each (target, property) in this timeline and everything under it. */
114
+ private countChannels;
115
+ private isExclusive;
116
+ private markExclusive;
117
+ private evaluate;
118
+ }
119
+
120
+ /**
121
+ * The timeline of the library and the few things around it: a timeline is a pure function of time (a value at a time is computed from the
122
+ * script and the time alone, never from the way the playhead came), see docs/superpowers/specs/2026-10-10-pure-timeline-design.md.
123
+ */
124
+ type Timeline = PureTimeline;
125
+
1
126
  /** What an author writes in `composition.stops`: a time in seconds, or an object. */
2
127
  interface StopSpec {
3
128
  /** Where playback stops, in seconds (negative counts back from the end). */
@@ -146,7 +271,7 @@ interface TransitionCommon {
146
271
  at: number;
147
272
  /** Length of the transition in seconds. Must be > 0. */
148
273
  duration: number;
149
- /** GSAP easing name. Default `'none'` (linear). */
274
+ /** Ease name (`'power2.out'`, `'back.out(1.7)'`, `'spring.bouncy'`, `'cubic-bezier(.4,0,.2,1)'`, `'steps(8)'`). Default `'none'` (linear). */
150
275
  ease?: string;
151
276
  }
152
277
  interface CrossfadeTransition extends TransitionCommon {
@@ -591,7 +716,7 @@ interface ShapeBase extends SequenceCommon {
591
716
  /**
592
717
  * Colour space used to interpolate `fillColor` / `strokeColor` keyframes.
593
718
  *
594
- * - `'rgb'` (default): linear RGB tween via `gsap.utils.interpolate`.
719
+ * - `'rgb'` (default): a straight lerp of the red, green and blue channels.
595
720
  * Fast, but a red → green ramp passes through muddy brown / olive
596
721
  * greys at the midpoint because intermediate sRGB values are
597
722
  * perceptually unbalanced.
@@ -721,6 +846,8 @@ interface AudioDescriptor {
721
846
  synth?: {
722
847
  key: string;
723
848
  render: (sampleRate: number) => [Float32Array, Float32Array];
849
+ /** The same samples in slices: `pace(done)` (0..1) runs between them and may wait, so a long piece does not freeze the page. */
850
+ renderAsync?: (sampleRate: number, pace?: (done: number) => Promise<void> | void) => Promise<[Float32Array, Float32Array]>;
724
851
  };
725
852
  /** Who made it, for warnings and inspectAudio: `layer "pop-1"`. */
726
853
  layer?: string;
@@ -743,4 +870,4 @@ interface AudioDescriptor {
743
870
  sourceMap?: (u: number) => number;
744
871
  }
745
872
 
746
- export { type StopSpec as $, type ArcShapeSpec as A, type BlendModeName as B, type CompositionShape as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type PathShapeSpec as H, type ImageSequenceSpec as I, type PolygonShapeSpec as J, type Keyframe as K, type LightSequenceSpec as L, type MusicOptions as M, type NullSequenceSpec as N, type PropValue as O, type Props as P, type SfxKnobs as Q, type RectShapeSpec as R, type SequenceSpec as S, type TextSequenceSpec as T, type SfxOptions as U, type SfxPreset as V, type SfxVoice as W, type ShaderSequenceSpec as X, type ShapeSequenceSpec as Y, type SlideTransition as Z, type Stop as _, type CameraSequenceSpec as a, type TransitionCommon as a0, type VideoSequenceSpec as a1, type WipeTransition as a2, type ZoomTransition as a3, normalizeStops as a4, pictureStops as a5, type AudioDescriptor as a6, type TransitionSpec as b, type CompositionSpec as c, type SequenceCommon as d, type AssetSpec as e, type AudioAssetSpec as f, type AudioMusicSpec as g, type AudioSequenceSpec as h, type AudioSfxSpec as i, type ChromaKeyFilterSpec as j, type CircleShapeSpec as k, type CompositionSequenceSpec as l, type CrossfadeTransition as m, type CustomFilterSpec as n, type DissolveTransition as o, type Expr as p, type IrisTransition as q, type LineShapeSpec as r, type LumaTransition as s, type MaskRefSpec as t, type MatteRefSpec as u, type MusicDrum as v, type MusicDrums as w, type MusicInstrument as x, type MusicLevel as y, type MusicTrack as z };
873
+ export { type StopSpec as $, type ArcShapeSpec as A, type BlendModeName as B, type CompositionShape as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type PathShapeSpec as H, type ImageSequenceSpec as I, type PolygonShapeSpec as J, type Keyframe as K, type LightSequenceSpec as L, type MusicOptions as M, type NullSequenceSpec as N, type PropValue as O, type Props as P, type SfxKnobs as Q, type RectShapeSpec as R, type SequenceSpec as S, type TextSequenceSpec as T, type SfxOptions as U, type SfxPreset as V, type SfxVoice as W, type ShaderSequenceSpec as X, type ShapeSequenceSpec as Y, type SlideTransition as Z, type Stop as _, type CameraSequenceSpec as a, type TransitionCommon as a0, type VideoSequenceSpec as a1, type WipeTransition as a2, type ZoomTransition as a3, normalizeStops as a4, pictureStops as a5, type Timeline as a6, type AudioDescriptor as a7, type TransitionSpec as b, type CompositionSpec as c, type SequenceCommon as d, type AssetSpec as e, type AudioAssetSpec as f, type AudioMusicSpec as g, type AudioSequenceSpec as h, type AudioSfxSpec as i, type ChromaKeyFilterSpec as j, type CircleShapeSpec as k, type CompositionSequenceSpec as l, type CrossfadeTransition as m, type CustomFilterSpec as n, type DissolveTransition as o, type Expr as p, type IrisTransition as q, type LineShapeSpec as r, type LumaTransition as s, type MaskRefSpec as t, type MatteRefSpec as u, type MusicDrum as v, type MusicDrums as w, type MusicInstrument as x, type MusicLevel as y, type MusicTrack as z };
package/docs/api.md CHANGED
@@ -211,6 +211,10 @@ The whole movie as rows on a time axis, for a person to read at a glance. `timel
211
211
 
212
212
  `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.
213
213
 
214
+ ### `ease(name): (p: number) => number`
215
+
216
+ The function behind an ease name (`ease('power2.out')(0.5)`, `ease('spring.bouncy')`, `ease('cubic-bezier(.4, 0, .2, 1)')`), for a preset or a check of your own. A name nobody knows warns once and runs as `power1.out`.
217
+
214
218
  ### `movie.destroy(): Promise<void>`
215
219
 
216
220
  Pauses playback, destroys the underlying PIXI Application, releases audio buffers and AudioContext, and marks the instance unusable.
@@ -254,7 +258,7 @@ The names are the ones an HTML5 `<video>` uses, so a player written for `<video>
254
258
  | `volume` | number | 0..1 getter/setter; immediate. Setter clamps and applies to active audio |
255
259
  | `muted` | boolean | getter/setter; immediate |
256
260
  | `app` | `pixi.js Application \| null` | PIXI Application instance (advanced/escape hatch) |
257
- | `timeline` | GSAP Timeline `\| null` | underlying GSAP timeline (advanced) |
261
+ | `timeline` | `Timeline \| null` | the movie's timeline (advanced): `time(t)`, `duration()`, `segments()` |
258
262
  | `stops` | `Stop[]` | the composition's stops (see Presentations); empty for an ordinary movie |
259
263
  | `stopIndex`, `pageIndex`, `pageCount`, `currentStop` | number, number, number, `Stop \| null` | where the playhead is among the stops and pages (`-1` / `null` before the first stop) |
260
264
 
package/docs/dsl.md CHANGED
@@ -401,7 +401,7 @@ Fill a shape with a gradient instead of `fillColor` (top level or in `initial`).
401
401
 
402
402
  #### `colorSpace`
403
403
 
404
- Per-shape choice of how colour keyframes are interpolated. Default `'rgb'` (linear sRGB lerp via `gsap.utils.interpolate`) is fast but classic — a red → green ramp passes through muddy olive at the midpoint. The two perceptually uniform options keep saturation through the transition:
404
+ Per-shape choice of how colour keyframes are interpolated. Default `'rgb'` (a straight lerp of the red, green and blue channels) is fast but classic — a red → green ramp passes through muddy olive at the midpoint. The two perceptually uniform options keep saturation through the transition:
405
405
 
406
406
  | Value | Behaviour |
407
407
  |-----------|---------------------------------------------------------------------------------------------------|
@@ -924,7 +924,7 @@ Supported formats are whatever PixiJS Assets and the browser's audio/video decod
924
924
  interface Keyframe {
925
925
  at?: number; // start, in seconds from the start of THIS sequence; negative = back from the sequence's end (-0.5 = 0.5 s before it ends)
926
926
  duration?: number | 'auto'; // seconds (default 0 — instantaneous); 'auto' = the time a spring ease takes to settle
927
- ease?: string; // GSAP easing name (default 'none'), or a spring: 'spring(mass, stiffness, damping)', 'spring.gentle|snappy|bouncy|wobbly|slow'
927
+ ease?: string; // Ease name (default 'none'), or a spring: 'spring(mass, stiffness, damping)', 'spring.gentle|snappy|bouncy|wobbly|slow'
928
928
  set?: Props; // jump to these values at `at`
929
929
  to?: Props; // animate from current to these values over `duration`
930
930
  from?: Props; // animate from these values to current
@@ -932,7 +932,7 @@ interface Keyframe {
932
932
  }
933
933
  ```
934
934
 
935
- **Relative values.** A number in `to`, `from` or `set` may be written `'+=36'` or `'-=36'`: it is measured from where the property stands when the keyframe starts (GSAP's own relative values), and the right side is an expression (`'-=GW*0.1'`). `from: { x: '-=36' }` starts 36 px to the left of where the layer is and arrives there (the usual slide-in); `to: { y: '+=40' }` moves 40 px further. Several keyframes one after another stack, whatever order you seek in; with `repeat` the same move plays again from the same start (it does not stack). A relative value in `initial`, a style, or the start side of a `from` + `to` keyframe has nothing to be measured from: it warns and is read as the number.
935
+ **Relative values.** A number in `to`, `from` or `set` may be written `'+=36'` or `'-=36'`: it is measured from where the property stands when the keyframe starts (the way GSAP writes them), and the right side is an expression (`'-=GW*0.1'`). `from: { x: '-=36' }` starts 36 px to the left of where the layer is and arrives there (the usual slide-in); `to: { y: '+=40' }` moves 40 px further. Several keyframes one after another stack, whatever order you seek in; with `repeat` the same move plays again from the same start (it does not stack). A relative value in `initial`, a style, or the start side of a `from` + `to` keyframe has nothing to be measured from: it warns and is read as the number.
936
936
 
937
937
  The four kinds are mutually exclusive per keyframe:
938
938
 
@@ -996,13 +996,13 @@ If `at < 0`, it's interpreted as `duration + at` — measured back from the end
996
996
 
997
997
  ### Easing
998
998
 
999
- `'cubic-bezier(x1, y1, x2, y2)'` is the CSS curve exactly (x1 and x2 between 0 and 1, y1 and y2 may overshoot): `'cubic-bezier(.4, 0, .2, 1)'` is a gentle in-out, `'cubic-bezier(.2, .8, .2, 1)'` is front-loaded, not slow motion (half of the change in the first 13 % of the time, 99 % by 74 %, then a long settle). GSAP's `power2` is a cubic: `power2.in` has done only 12.5 % at half time. A malformed one warns once and runs as `'none'`.
999
+ `'cubic-bezier(x1, y1, x2, y2)'` is the CSS curve exactly (x1 and x2 between 0 and 1, y1 and y2 may overshoot): `'cubic-bezier(.4, 0, .2, 1)'` is a gentle in-out, `'cubic-bezier(.2, .8, .2, 1)'` is front-loaded, not slow motion (half of the change in the first 13 % of the time, 99 % by 74 %, then a long settle). `power2` is a cubic: `power2.in` has done only 12.5 % at half time. A malformed one warns once and runs as `'none'`.
1000
1000
 
1001
- Standard GSAP easing strings: `'none'`, `'linear'`, `'power1.in'` ... `'power4.inOut'`, `'sine.in/out/inOut'`, `'expo.in/out/inOut'`, `'circ.in/out/inOut'`, `'back.in/out/inOut(overshoot)'`, `'elastic.in/out/inOut(amplitude, period)'`, `'bounce.in/out/inOut'`. See [GSAP easing docs](https://gsap.com/docs/v3/Eases/).
1001
+ Ease strings (the names GSAP made familiar): `'none'`, `'linear'`, `'power1.in'` ... `'power4.inOut'`, `'sine.in/out/inOut'`, `'expo.in/out/inOut'`, `'circ.in/out/inOut'`, `'back.in/out/inOut(overshoot)'`, `'elastic.in/out/inOut(amplitude, period)'`, `'bounce.in/out/inOut'`. `'steps(n)'` is n equal steps.
1002
1002
 
1003
1003
  ### PIXI shorthands
1004
1004
 
1005
- These keys are auto-routed through GSAP's PixiPlugin when used in `initial` / `set` / `to` / `from` / `keyframes`:
1005
+ These keys are shorthands the timeline understands when used in `initial` / `set` / `to` / `from` / `keyframes`:
1006
1006
 
1007
1007
  ```
1008
1008
  scale, scaleX, scaleY
@@ -1013,12 +1013,10 @@ position, positionX, positionY
1013
1013
  tilePosition, tilePositionX, tilePositionY
1014
1014
  tileScale, tileScaleX, tileScaleY
1015
1015
  tint, autoAlpha
1016
- colorize, colorizeAmount, colorMatrixFilter
1017
- blur, blurX, blurY, blurPadding
1018
- lineColor, lineAlpha, fillColor, fillAlpha
1016
+ fillColor, fillAlpha (a shape's own colour keys)
1019
1017
  ```
1020
1018
 
1021
- (In addition to plain DisplayObject props like `x`, `y`, `rotation`, `alpha`, `width`, `height`, `visible`.) **Angles are in degrees**: `rotation`, `skew` / `skewX` / `skewY` (PixiPlugin converts them), and `rotationX` / `rotationY` for 3D layers.
1019
+ (In addition to plain DisplayObject props like `x`, `y`, `rotation`, `alpha`, `width`, `height`, `visible`.) `blur`, `colorize` and the other colour-matrix shorthands of GSAP's PixiPlugin are gone: use a named filter and animate `filters.<name>.<prop>` (below). **Angles are in degrees**: `rotation`, `skew` / `skewX` / `skewY`, and `rotationX` / `rotationY` for 3D layers.
1022
1020
 
1023
1021
  ### Filter keyframe paths
1024
1022
 
@@ -1145,7 +1143,7 @@ Common fields (`TransitionCommon`):
1145
1143
  | `to` | string | sibling sequence's `name`. Must be declared **after** `from` in `sequences[]`. |
1146
1144
  | `at` | number | start of the transition, in the **parent composition's** time (like a sequence's `at`, not sequence-local); negative = back from the parent's end. |
1147
1145
  | `duration` | number | seconds, must be > 0. |
1148
- | `ease` | string? | GSAP easing name. Default `'none'` (linear). |
1146
+ | `ease` | string? | Ease name. Default `'none'` (linear). |
1149
1147
 
1150
1148
  Validation runs at composition build time. Errors throw with the offending `transitions[<index>]` quoted in the message: missing names, `to` before `from`, transition window outside either sequence's lifespan, duplicate use of one sequence as `from`, `from === to`, `duration <= 0`.
1151
1149
 
@@ -1412,7 +1410,7 @@ The image is centred on the canvas; `fit` (default `'cover'`) controls how the t
1412
1410
  | `name?` | string | sequence name so transitions can reference it |
1413
1411
  | `at?` | number | start time, parent-relative seconds |
1414
1412
  | `fit?` | `'cover'` \| `'contain'` | how the texture fills the canvas. Default `'cover'`. |
1415
- | `ease?` | string | GSAP easing name. Default `'sine.inOut'`. |
1413
+ | `ease?` | string | Ease name. Default `'sine.inOut'`. |
1416
1414
 
1417
1415
  #### `motion: 'still'`
1418
1416
 
@@ -1548,7 +1546,7 @@ import { cameraPath, wiggle } from 'pixi-effects';
1548
1546
  | `points` | `[[x, y, z], …]`, at least two, the route in order |
1549
1547
  | `duration` | seconds; required |
1550
1548
  | `at` | start, seconds from the start of the camera layer (default 0) |
1551
- | `ease` | GSAP ease for the progress along the route (default `'none'`: constant speed; `'power2.in'` an accelerating rush) |
1549
+ | `ease` | Ease for the progress along the route (default `'none'`: constant speed; `'power2.in'` an accelerating rush) |
1552
1550
  | `look` | `'ahead'` (default: face where it flies) or a fixed `[x, y, z]` |
1553
1551
  | `lookAhead` | with `'ahead'`: how far ahead, as a fraction 0–1 of the route (default 0.08); past the end it continues along the last direction |
1554
1552
  | `smooth` | `true` (default): a smooth curve through the points (Catmull-Rom); `false`: straight lines |
@@ -1574,7 +1572,7 @@ import { followPath } from 'pixi-effects';
1574
1572
  | `d` | the route: SVG path data in canvas coordinates; several sub-paths are walked in order; a closed one comes back to its start |
1575
1573
  | `duration` | seconds; required |
1576
1574
  | `at` | start, seconds from the start of the layer (default 0): give the layer its own `at` so it is not drawn at the origin before the trip |
1577
- | `ease` | GSAP ease for the progress along the path (default `'none'`: constant speed) |
1575
+ | `ease` | Ease for the progress along the path (default `'none'`: constant speed) |
1578
1576
  | `from` / `to` | fractions 0–1 of the path to start and stop at (default 0 → 1; `from: 1, to: 0` goes backwards) |
1579
1577
  | `orient` / `rotate` | `orient: true` also keyframes `rotation` (degrees) to face the way it is going, without ever jumping by a turn; `rotate` adds an offset (`-90` for an image that points up) |
1580
1578
  | `frameRate` | samples per second (default 30) |
@@ -1643,7 +1641,7 @@ const ripple = stagger(18, { each: 0.12, grid: [6, 3], from: 'center', ease: 'si
1643
1641
  | `amount` | seconds from the first to the last, however many there are |
1644
1642
  | `from` | `'start'` (default), `'end'`, `'center'` (outward), `'edges'` (inward), `'random'` (a seeded shuffle of the same delays) or an item index |
1645
1643
  | `grid` | `[columns, rows]`: items fill the grid row by row, and the wave travels by straight-line distance (a ripple) |
1646
- | `ease` | a GSAP ease that reshapes the spread (default `'none'`); the first stays at 0, the last at the full spread |
1644
+ | `ease` | an ease that reshapes the spread (default `'none'`); the first stays at 0, the last at the full spread |
1647
1645
  | `seed` | for `from: 'random'`: another seed, another order |
1648
1646
 
1649
1647
  With a layer array each layer's own `at` is kept and the delay is added to it. Keyframes inside the layers stay as they are (they count from the layer's start).
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.26.3. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
9
+ > Written for pixi-effects 0.27.1. 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
 
@@ -111,7 +111,7 @@ blendMode: 'normal' | 'add' | 'screen' | 'multiply' | 'overlay' | 'soft-light' |
111
111
 
112
112
  Animatable props (`initial` / keyframes): `x y alpha rotation scale scaleX scaleY pivotX pivotY anchorX anchorY skewX skewY tint width height visible autoAlpha`, plus for shapes their style/geometry, for text `fill`, for audio `volume`, for 3D `z rotationX rotationY`, filter params as `'filters.<name>.<param>'`, three objects as `'three.<obj>.<path>'`. **These go in `initial` / keyframes, never at the top level of a shape or text layer** (top-level `x`, `rotation`, `strokeColor`, `fillAlpha` each warn "is not a … key"): the top level of a shape holds its geometry (`width height radius d startAngle endAngle cornerRadius anchorX anchorY`), `trimStart` / `trimEnd`, `strokeCap` / `strokeJoin`, `fillGradient`.
113
113
 
114
- Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate to), `from` (animate from), `from`+`to`; a value may be relative to where the layer stands (`from: { x: '-=36' }` slides in from 36 px to the left of its place). `from` / `from+to` show their start value from the layer's start, so a delayed fade-in needs no `initial.alpha`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed). `ease` = any GSAP ease (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`, **`'cubic-bezier(.4, 0, .2, 1)'`** (CSS syntax, x1 and x2 in 0…1; `.4, 0, .2, 1` is a gentle in-out; `.2, .8, .2, 1` is FRONT-LOADED, not slow motion: half of the change in the first 13 % of the time, 90 % by 39 %, 99 % by 74 %, then a long settle, so what sits at the END of the content gets the glide; `.2, .8, .5, 1` has a shorter tail). GSAP's `power1…4` are the powers 2…5: `power2` is a cubic, `power2.in` has done only 12.5 % at half time (42 % at three quarters)… and springs: `'spring(mass, stiffness, damping)'` or `'spring.gentle' | 'snappy' | 'bouncy' | 'wobbly' | 'slow'`; **`duration: 'auto'` = the time the spring takes to settle** (presets, settle time / highest value reached as a multiple of the target: `snappy` 0.42 s / 1.01, `gentle` 0.72 s / 1.02, `bouncy` 1.08 s / 1.28, `slow` 1.29 s / 1.00, `wobbly` 1.61 s / 1.40: time the next beat from these) (real physics; with a number `duration` is the settle time and only the damping ratio shapes it; a damping ratio of 1 or more never overshoots, so `spring(1,170,26)` does not bounce: use damping below 20 at stiffness 170; `'auto'` works on keyframes only, not in `animateText` / `orbit` tweens; a `stagger` ease may be a spring, its delays are held inside the spread). Default ease is linear.
114
+ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate to), `from` (animate from), `from`+`to`; a value may be relative to where the layer stands (`from: { x: '-=36' }` slides in from 36 px to the left of its place). `from` / `from+to` show their start value from the layer's start, so a delayed fade-in needs no `initial.alpha`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed). `ease` = an ease name (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`, **`'cubic-bezier(.4, 0, .2, 1)'`** (CSS syntax, x1 and x2 in 0…1; `.4, 0, .2, 1` is a gentle in-out; `.2, .8, .2, 1` is FRONT-LOADED, not slow motion: half of the change in the first 13 % of the time, 90 % by 39 %, 99 % by 74 %, then a long settle, so what sits at the END of the content gets the glide; `.2, .8, .5, 1` has a shorter tail). `power1…4` are the powers 2…5: `power2` is a cubic, `power2.in` has done only 12.5 % at half time (42 % at three quarters)… and springs: `'spring(mass, stiffness, damping)'` or `'spring.gentle' | 'snappy' | 'bouncy' | 'wobbly' | 'slow'`; **`duration: 'auto'` = the time the spring takes to settle** (presets, settle time / highest value reached as a multiple of the target: `snappy` 0.42 s / 1.01, `gentle` 0.72 s / 1.02, `bouncy` 1.08 s / 1.28, `slow` 1.29 s / 1.00, `wobbly` 1.61 s / 1.40: time the next beat from these) (real physics; with a number `duration` is the settle time and only the damping ratio shapes it; a damping ratio of 1 or more never overshoots, so `spring(1,170,26)` does not bounce: use damping below 20 at stiffness 170; `'auto'` works on keyframes only, not in `animateText` / `orbit` tweens; a `stagger` ease may be a spring, its delays are held inside the spread). Default ease is linear.
115
115
 
116
116
  ## Layer types
117
117
 
@@ -305,14 +305,14 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
305
305
  30c. **Look with the built-in tools, not OS screenshots**: `await movie.contactSheet({ count: 6, as: 'dataURL' })` returns one labelled image of the whole animation; `await movie.inspect(frame)` returns every layer's canvas bounds and `issues`. If you do use a browser tool, `screenshot` needs an **absolute path**, macOS `sed -i` needs `''`, and a static server may redirect `/x.html` → `/x` and drop `?query`. Keep the template's `try/catch` around `init` so failures land in `window.__logs`. Check ≥ 5 timestamps including mid-transition. *(fixed: tools built in)*
306
306
  31. **Check audio with `movie.inspectAudio()`**: every sound measured on its own (`sound.length`, `peakDb`, `loudestAt`, `brightnessHz`), when it plays, loudness and brightness per 0.1 s window, and `issues` (the mix was limited, a layer is inaudible, an sfx is cut off by the end). Decoding a render by hand is no longer needed: `render()` encodes the same mix, on time in mp4 / mov / webm / mkv. *(fixed)*
307
307
 
308
- 31b. **A mistyped `ease` now warns** (`ease: 'power3.outt'` → did you mean `power3.out`). It used to run silently as GSAP's default (`power1.out`), so the motion changed and nothing said so.
308
+ 31b. **A mistyped `ease` now warns** (`ease: 'power3.outt'` → did you mean `power3.out`). It used to run silently as the default ease (`power1.out`), so the motion changed and nothing said so.
309
309
  31c. **`draft` and `scale` make the file smaller, not the drawing faster** (measured: a 48 s 1080p film exports in about the same time as a draft; the file is 7× smaller). To save time, export a part (`range` / `--range` / `--scene`), look at single moments (`check --no-export --at …`), and keep motion blur off while you iterate.
310
310
 
311
311
  ## Round 2 — found by building 16 portfolio pieces (examples/gallery)
312
312
 
313
313
  32. **Seeking is safe now** *(fixed)*: a `set` keyframe (counter value, text fill, image tint, shape style) is undone when you seek back before it, and shapes are redrawn before they are culled, so a rect growing from `width: 0` at the left edge draws after any jump seek (contact sheet, snapshot, scrubbing, export). The old workarounds (`width: 0.01`, a one-frame `to` instead of `set`) are not needed.
314
314
  33. **A mask starts and ends with the layer it masks** *(fixed)*, so its keyframes count from that layer's start. A mask with an explicit `at` keeps composition time. Before, a mask `at: 0` reveal had finished before a layer that appeared at 8 s, and the layer stayed invisible without any warning.
315
- 34. **`from` and `from+to` show their start value from the LAYER's start**, not from the keyframe's `at` (GSAP behaviour): a delayed fade-in `{ at: 2, from: { alpha: 0 }, to: { alpha: 1 } }` needs no `initial.alpha`. To keep the current value until `at` and then jump, use `set`. *(docs)*
315
+ 34. **`from` and `from+to` show their start value from the LAYER's start**, not from the keyframe's `at` (as in GSAP and After Effects): a delayed fade-in `{ at: 2, from: { alpha: 0 }, to: { alpha: 1 } }` needs no `initial.alpha`. To keep the current value until `at` and then jump, use `set`. *(docs)*
316
316
  35. **One `Movie` per page.** Creating a second Movie (for an isolated repro) and destroying it breaks the first (`null.clear`). Reload instead. *(docs)*
317
317
  36. **Multi-line text is centre-aligned by default** (`style.align: 'left'` for left-aligned paragraphs). *(docs)*
318
318
  37. **Composition size.** A 2D composition does not clip its children (a child outside `width × height` still draws), but its rectangle is the culling area: if the whole rectangle is off the canvas the group is skipped even when a child is on screen — keep it near what it draws, or canvas-sized. A `threeD` composition (a card) IS clipped at its `width × height` (it is rendered into a texture that size): size it for the soft shadow too. A growing `threeD` shape or text is fine: its texture is resized in place (it used to warn "destroyed while still bound"). *(measured; docs / fixed)*
@@ -352,7 +352,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
352
352
  70. **`speed` is a setting, `time` is the property.** `speed: 2` on the layer; to change the speed over time animate `time` (a curve of seconds in the file), never `keyframes: [{ to: { speed: 3 } }]` (warns). `speed: 0` is not allowed (hold a moment with `time` to the same value twice); `speed` together with keyframed `time`: `time` wins. Other names (`playbackRate`, `timeScale`, `rate`, `reverse: true`, `timeRemap`) are warned about with what to write. `particles({ speed })` is something else: launch speed in px per second.
353
353
  71. **Sound follows `speed` like a tape: the pitch moves with it.** An audio file or a video's sound at `speed: 2` is an octave up; backward plays reversed. Not for `sfx` / `music` layers (warns): change an sfx with `pitch` (semitones), or put the sfx **inside a remapped composition** and it follows that clock (slower and lower, or backward).
354
354
  72. **A video at `speed: 2` with no `duration` is half as long; with a `duration` longer than the file it holds the last frame** (add `loop: true` to repeat). A negative `time` counts back from the end of the file (`initial: { time: -2 }` = two seconds before the end).
355
- 73. **Know the shape of the curve before you time a beat to it.** `cubic-bezier(.2, .8, .2, 1)` does half of its change in the first 13 % of the time and 99 % by 74 % (not slow motion: over 6 s the first 2 s of content pass in 0.6 s); `.2, .8, .5, 1` has a shorter tail. GSAP's `power2` is a cubic (`power1` = t², `power3` = t⁴): `power2.in` has done 12.5 % at half time and 42 % at three quarters, so a rewind with it dismantles most of the title in the last third.
355
+ 73. **Know the shape of the curve before you time a beat to it.** `cubic-bezier(.2, .8, .2, 1)` does half of its change in the first 13 % of the time and 99 % by 74 % (not slow motion: over 6 s the first 2 s of content pass in 0.6 s); `.2, .8, .5, 1` has a shorter tail. `power2` is a cubic (`power1` = t², `power3` = t⁴): `power2.in` has done 12.5 % at half time and 42 % at three quarters, so a rewind with it dismantles most of the title in the last third.
356
356
  74. **`focus` as a layer name means that layer's FIRST `z`, and the name must be unique.** (Two layers with one name: the first `threeD` one is used and it warns.) The camera does not follow a layer whose `z` moves (it warns): to follow it, animate `focus` itself with numbers. A name no layer has, or a layer that is not `threeD`, warns and the `z = 0` plane is used (did-you-mean for typos).
357
357
  75. **Depth of field blurs `threeD` layers only, and the layer's own depth decides it.** A 2D layer never blurs; a `threeD` layer at the focus depth stays sharp; the depth is measured along the camera's view direction, so with an orbiting or tilted camera a layer off to the side of the look-at point is a little deeper than its `z` says and blurs a little even at the focus `z`; one blur for the whole layer, so a tilted floor is one blur (split it into layers).
358
358
  76. **`aperture` is the lens diameter in px, not a 0–1 strength.** 30 is the default (clearly shallow), 0 turns it off, 60 is strong, 100 is extreme (small bright objects speckle); the blur never exceeds 32 px. 20 or more blurred layers at once warns (one filter pass each, more on WebGPU).
@@ -480,7 +480,7 @@ return [
480
480
 
481
481
  ## Rewind and play again: a composition whose own time you control
482
482
 
483
- Put the part you want to rewind, slow down or freeze **in a composition**, then give that composition `speed` (one straight speed: `0.5` slow motion, `-1` backward) or `time` keyframes (any curve of its own time: play, hold, rewind, play slowly). The layers inside write `at` / `duration` / `keyframes` in the composition's own seconds; the composition's own `at` / `duration` stay in the outer time. A sound or `sfx` inside follows the clock (backward and lower when it rewinds, like a tape). `cubic-bezier(.2, .8, .2, 1)` is front-loaded, not slow motion: half of the replay passes in the first 13 % of its time and 99 % by 74 %, so the replay opens quickly and what sits at the END of the content (the ring, the settle) gets the long glide. GSAP's `power2` is a cubic: `power2.in` has done only 12.5 % at half time, so the dismantling bunches up in the last third of the rewind.
483
+ Put the part you want to rewind, slow down or freeze **in a composition**, then give that composition `speed` (one straight speed: `0.5` slow motion, `-1` backward) or `time` keyframes (any curve of its own time: play, hold, rewind, play slowly). The layers inside write `at` / `duration` / `keyframes` in the composition's own seconds; the composition's own `at` / `duration` stay in the outer time. A sound or `sfx` inside follows the clock (backward and lower when it rewinds, like a tape). `cubic-bezier(.2, .8, .2, 1)` is front-loaded, not slow motion: half of the replay passes in the first 13 % of its time and 99 % by 74 %, so the replay opens quickly and what sits at the END of the content (the ring, the settle) gets the long glide. `power2` is a cubic: `power2.in` has done only 12.5 % at half time, so the dismantling bunches up in the last third of the rewind.
484
484
 
485
485
  ```js
486
486
  // @recipe rewind-and-replay
@@ -1922,7 +1922,7 @@ Fill a shape with a gradient instead of `fillColor` (top level or in `initial`).
1922
1922
 
1923
1923
  #### `colorSpace`
1924
1924
 
1925
- Per-shape choice of how colour keyframes are interpolated. Default `'rgb'` (linear sRGB lerp via `gsap.utils.interpolate`) is fast but classic — a red → green ramp passes through muddy olive at the midpoint. The two perceptually uniform options keep saturation through the transition:
1925
+ Per-shape choice of how colour keyframes are interpolated. Default `'rgb'` (a straight lerp of the red, green and blue channels) is fast but classic — a red → green ramp passes through muddy olive at the midpoint. The two perceptually uniform options keep saturation through the transition:
1926
1926
 
1927
1927
  | Value | Behaviour |
1928
1928
  |-----------|---------------------------------------------------------------------------------------------------|
@@ -2445,7 +2445,7 @@ Supported formats are whatever PixiJS Assets and the browser's audio/video decod
2445
2445
  interface Keyframe {
2446
2446
  at?: number; // start, in seconds from the start of THIS sequence; negative = back from the sequence's end (-0.5 = 0.5 s before it ends)
2447
2447
  duration?: number | 'auto'; // seconds (default 0 — instantaneous); 'auto' = the time a spring ease takes to settle
2448
- ease?: string; // GSAP easing name (default 'none'), or a spring: 'spring(mass, stiffness, damping)', 'spring.gentle|snappy|bouncy|wobbly|slow'
2448
+ ease?: string; // Ease name (default 'none'), or a spring: 'spring(mass, stiffness, damping)', 'spring.gentle|snappy|bouncy|wobbly|slow'
2449
2449
  set?: Props; // jump to these values at `at`
2450
2450
  to?: Props; // animate from current to these values over `duration`
2451
2451
  from?: Props; // animate from these values to current
@@ -2453,7 +2453,7 @@ interface Keyframe {
2453
2453
  }
2454
2454
  ```
2455
2455
 
2456
- **Relative values.** A number in `to`, `from` or `set` may be written `'+=36'` or `'-=36'`: it is measured from where the property stands when the keyframe starts (GSAP's own relative values), and the right side is an expression (`'-=GW*0.1'`). `from: { x: '-=36' }` starts 36 px to the left of where the layer is and arrives there (the usual slide-in); `to: { y: '+=40' }` moves 40 px further. Several keyframes one after another stack, whatever order you seek in; with `repeat` the same move plays again from the same start (it does not stack). A relative value in `initial`, a style, or the start side of a `from` + `to` keyframe has nothing to be measured from: it warns and is read as the number.
2456
+ **Relative values.** A number in `to`, `from` or `set` may be written `'+=36'` or `'-=36'`: it is measured from where the property stands when the keyframe starts (the way GSAP writes them), and the right side is an expression (`'-=GW*0.1'`). `from: { x: '-=36' }` starts 36 px to the left of where the layer is and arrives there (the usual slide-in); `to: { y: '+=40' }` moves 40 px further. Several keyframes one after another stack, whatever order you seek in; with `repeat` the same move plays again from the same start (it does not stack). A relative value in `initial`, a style, or the start side of a `from` + `to` keyframe has nothing to be measured from: it warns and is read as the number.
2457
2457
 
2458
2458
  The four kinds are mutually exclusive per keyframe:
2459
2459
 
@@ -2517,13 +2517,13 @@ If `at < 0`, it's interpreted as `duration + at` — measured back from the end
2517
2517
 
2518
2518
  ### Easing
2519
2519
 
2520
- `'cubic-bezier(x1, y1, x2, y2)'` is the CSS curve exactly (x1 and x2 between 0 and 1, y1 and y2 may overshoot): `'cubic-bezier(.4, 0, .2, 1)'` is a gentle in-out, `'cubic-bezier(.2, .8, .2, 1)'` is front-loaded, not slow motion (half of the change in the first 13 % of the time, 99 % by 74 %, then a long settle). GSAP's `power2` is a cubic: `power2.in` has done only 12.5 % at half time. A malformed one warns once and runs as `'none'`.
2520
+ `'cubic-bezier(x1, y1, x2, y2)'` is the CSS curve exactly (x1 and x2 between 0 and 1, y1 and y2 may overshoot): `'cubic-bezier(.4, 0, .2, 1)'` is a gentle in-out, `'cubic-bezier(.2, .8, .2, 1)'` is front-loaded, not slow motion (half of the change in the first 13 % of the time, 99 % by 74 %, then a long settle). `power2` is a cubic: `power2.in` has done only 12.5 % at half time. A malformed one warns once and runs as `'none'`.
2521
2521
 
2522
- Standard GSAP easing strings: `'none'`, `'linear'`, `'power1.in'` ... `'power4.inOut'`, `'sine.in/out/inOut'`, `'expo.in/out/inOut'`, `'circ.in/out/inOut'`, `'back.in/out/inOut(overshoot)'`, `'elastic.in/out/inOut(amplitude, period)'`, `'bounce.in/out/inOut'`. See [GSAP easing docs](https://gsap.com/docs/v3/Eases/).
2522
+ Ease strings (the names GSAP made familiar): `'none'`, `'linear'`, `'power1.in'` ... `'power4.inOut'`, `'sine.in/out/inOut'`, `'expo.in/out/inOut'`, `'circ.in/out/inOut'`, `'back.in/out/inOut(overshoot)'`, `'elastic.in/out/inOut(amplitude, period)'`, `'bounce.in/out/inOut'`. `'steps(n)'` is n equal steps.
2523
2523
 
2524
2524
  ### PIXI shorthands
2525
2525
 
2526
- These keys are auto-routed through GSAP's PixiPlugin when used in `initial` / `set` / `to` / `from` / `keyframes`:
2526
+ These keys are shorthands the timeline understands when used in `initial` / `set` / `to` / `from` / `keyframes`:
2527
2527
 
2528
2528
  ```
2529
2529
  scale, scaleX, scaleY
@@ -2534,12 +2534,10 @@ position, positionX, positionY
2534
2534
  tilePosition, tilePositionX, tilePositionY
2535
2535
  tileScale, tileScaleX, tileScaleY
2536
2536
  tint, autoAlpha
2537
- colorize, colorizeAmount, colorMatrixFilter
2538
- blur, blurX, blurY, blurPadding
2539
- lineColor, lineAlpha, fillColor, fillAlpha
2537
+ fillColor, fillAlpha (a shape's own colour keys)
2540
2538
  ```
2541
2539
 
2542
- (In addition to plain DisplayObject props like `x`, `y`, `rotation`, `alpha`, `width`, `height`, `visible`.) **Angles are in degrees**: `rotation`, `skew` / `skewX` / `skewY` (PixiPlugin converts them), and `rotationX` / `rotationY` for 3D layers.
2540
+ (In addition to plain DisplayObject props like `x`, `y`, `rotation`, `alpha`, `width`, `height`, `visible`.) `blur`, `colorize` and the other colour-matrix shorthands of GSAP's PixiPlugin are gone: use a named filter and animate `filters.<name>.<prop>` (below). **Angles are in degrees**: `rotation`, `skew` / `skewX` / `skewY`, and `rotationX` / `rotationY` for 3D layers.
2543
2541
 
2544
2542
  ### Filter keyframe paths
2545
2543
 
@@ -2666,7 +2664,7 @@ Common fields (`TransitionCommon`):
2666
2664
  | `to` | string | sibling sequence's `name`. Must be declared **after** `from` in `sequences[]`. |
2667
2665
  | `at` | number | start of the transition, in the **parent composition's** time (like a sequence's `at`, not sequence-local); negative = back from the parent's end. |
2668
2666
  | `duration` | number | seconds, must be > 0. |
2669
- | `ease` | string? | GSAP easing name. Default `'none'` (linear). |
2667
+ | `ease` | string? | Ease name. Default `'none'` (linear). |
2670
2668
 
2671
2669
  Validation runs at composition build time. Errors throw with the offending `transitions[<index>]` quoted in the message: missing names, `to` before `from`, transition window outside either sequence's lifespan, duplicate use of one sequence as `from`, `from === to`, `duration <= 0`.
2672
2670
 
@@ -2933,7 +2931,7 @@ The image is centred on the canvas; `fit` (default `'cover'`) controls how the t
2933
2931
  | `name?` | string | sequence name so transitions can reference it |
2934
2932
  | `at?` | number | start time, parent-relative seconds |
2935
2933
  | `fit?` | `'cover'` \| `'contain'` | how the texture fills the canvas. Default `'cover'`. |
2936
- | `ease?` | string | GSAP easing name. Default `'sine.inOut'`. |
2934
+ | `ease?` | string | Ease name. Default `'sine.inOut'`. |
2937
2935
 
2938
2936
  #### `motion: 'still'`
2939
2937
 
@@ -3069,7 +3067,7 @@ import { cameraPath, wiggle } from 'pixi-effects';
3069
3067
  | `points` | `[[x, y, z], …]`, at least two, the route in order |
3070
3068
  | `duration` | seconds; required |
3071
3069
  | `at` | start, seconds from the start of the camera layer (default 0) |
3072
- | `ease` | GSAP ease for the progress along the route (default `'none'`: constant speed; `'power2.in'` an accelerating rush) |
3070
+ | `ease` | Ease for the progress along the route (default `'none'`: constant speed; `'power2.in'` an accelerating rush) |
3073
3071
  | `look` | `'ahead'` (default: face where it flies) or a fixed `[x, y, z]` |
3074
3072
  | `lookAhead` | with `'ahead'`: how far ahead, as a fraction 0–1 of the route (default 0.08); past the end it continues along the last direction |
3075
3073
  | `smooth` | `true` (default): a smooth curve through the points (Catmull-Rom); `false`: straight lines |
@@ -3095,7 +3093,7 @@ import { followPath } from 'pixi-effects';
3095
3093
  | `d` | the route: SVG path data in canvas coordinates; several sub-paths are walked in order; a closed one comes back to its start |
3096
3094
  | `duration` | seconds; required |
3097
3095
  | `at` | start, seconds from the start of the layer (default 0): give the layer its own `at` so it is not drawn at the origin before the trip |
3098
- | `ease` | GSAP ease for the progress along the path (default `'none'`: constant speed) |
3096
+ | `ease` | Ease for the progress along the path (default `'none'`: constant speed) |
3099
3097
  | `from` / `to` | fractions 0–1 of the path to start and stop at (default 0 → 1; `from: 1, to: 0` goes backwards) |
3100
3098
  | `orient` / `rotate` | `orient: true` also keyframes `rotation` (degrees) to face the way it is going, without ever jumping by a turn; `rotate` adds an offset (`-90` for an image that points up) |
3101
3099
  | `frameRate` | samples per second (default 30) |
@@ -3164,7 +3162,7 @@ const ripple = stagger(18, { each: 0.12, grid: [6, 3], from: 'center', ease: 'si
3164
3162
  | `amount` | seconds from the first to the last, however many there are |
3165
3163
  | `from` | `'start'` (default), `'end'`, `'center'` (outward), `'edges'` (inward), `'random'` (a seeded shuffle of the same delays) or an item index |
3166
3164
  | `grid` | `[columns, rows]`: items fill the grid row by row, and the wave travels by straight-line distance (a ripple) |
3167
- | `ease` | a GSAP ease that reshapes the spread (default `'none'`); the first stays at 0, the last at the full spread |
3165
+ | `ease` | an ease that reshapes the spread (default `'none'`); the first stays at 0, the last at the full spread |
3168
3166
  | `seed` | for `from: 'random'`: another seed, another order |
3169
3167
 
3170
3168
  With a layer array each layer's own `at` is kept and the delay is added to it. Keyframes inside the layers stay as they are (they count from the layer's start).
@@ -3405,6 +3403,10 @@ The whole movie as rows on a time axis, for a person to read at a glance. `timel
3405
3403
 
3406
3404
  `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.
3407
3405
 
3406
+ ### `ease(name): (p: number) => number`
3407
+
3408
+ The function behind an ease name (`ease('power2.out')(0.5)`, `ease('spring.bouncy')`, `ease('cubic-bezier(.4, 0, .2, 1)')`), for a preset or a check of your own. A name nobody knows warns once and runs as `power1.out`.
3409
+
3408
3410
  ### `movie.destroy(): Promise<void>`
3409
3411
 
3410
3412
  Pauses playback, destroys the underlying PIXI Application, releases audio buffers and AudioContext, and marks the instance unusable.
@@ -3448,7 +3450,7 @@ The names are the ones an HTML5 `<video>` uses, so a player written for `<video>
3448
3450
  | `volume` | number | 0..1 getter/setter; immediate. Setter clamps and applies to active audio |
3449
3451
  | `muted` | boolean | getter/setter; immediate |
3450
3452
  | `app` | `pixi.js Application \| null` | PIXI Application instance (advanced/escape hatch) |
3451
- | `timeline` | GSAP Timeline `\| null` | underlying GSAP timeline (advanced) |
3453
+ | `timeline` | `Timeline \| null` | the movie's timeline (advanced): `time(t)`, `duration()`, `segments()` |
3452
3454
  | `stops` | `Stop[]` | the composition's stops (see Presentations); empty for an ordinary movie |
3453
3455
  | `stopIndex`, `pageIndex`, `pageCount`, `currentStop` | number, number, number, `Stop \| null` | where the playhead is among the stops and pages (`-1` / `null` before the first stop) |
3454
3456
 
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # pixi-effects
2
2
 
3
- > Declarative motion graphics and video for the web. Describe a video as plain objects (text, shapes, images, video, audio, nested compositions, a 2.5D camera, optional three.js), play it in a canvas, export MP4 / WebM / MOV. Built on PixiJS v8, GSAP and mediabunny. Written to be authored by AI: no per-frame code.
3
+ > Declarative motion graphics and video for the web. Describe a video as plain objects (text, shapes, images, video, audio, nested compositions, a 2.5D camera, optional three.js), play it in a canvas, export MP4 / WebM / MOV. Built on PixiJS v8 and mediabunny. Written to be authored by AI: no per-frame code.
4
4
 
5
5
  Rules that cause most failures (details in the skill and pitfalls):
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-effects",
3
- "version": "0.26.3",
3
+ "version": "0.27.1",
4
4
  "description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,11 +29,11 @@
29
29
  "files": ["dist", "ai", "skills", "docs/dsl.md", "docs/api.md", "llms.txt", "llms-full.txt", "CHANGELOG.md", "README.md", "LICENSE"],
30
30
  "keywords": [
31
31
  "pixi", "pixijs", "video", "composition", "compositing", "rendering",
32
- "after-effects", "motion-graphics", "timeline", "webgl", "gsap", "keyframe", "typescript",
32
+ "after-effects", "motion-graphics", "timeline", "webgl", "keyframe", "typescript",
33
33
  "three", "threejs"
34
34
  ],
35
35
  "license": "MIT",
36
- "author": "tefista <tefista.dev@gmail.com>",
36
+ "author": { "name": "tefista", "email": "tefista.dev@gmail.com", "url": "https://x.com/t_yjm" },
37
37
  "repository": { "type": "git", "url": "git+https://github.com/yjmtmtk/pixi-effects.git" },
38
38
  "homepage": "https://github.com/yjmtmtk/pixi-effects#readme",
39
39
  "bugs": { "url": "https://github.com/yjmtmtk/pixi-effects/issues" },
@@ -62,7 +62,6 @@
62
62
  "mediabunny": "^1.61.3"
63
63
  },
64
64
  "peerDependencies": {
65
- "gsap": "^3.12.0",
66
65
  "pixi.js": "^8.0.0",
67
66
  "three": ">=0.150.0",
68
67
  "pixi-filters": "^6.1.0"
@@ -76,7 +75,7 @@
76
75
  "typescript": "^5.5.0",
77
76
  "vitest": "^2.1.0",
78
77
  "happy-dom": "^15.0.0",
79
- "gsap": "^3.12.0",
78
+ "gsap": "3.15.0",
80
79
  "marked": "^18.0.14",
81
80
  "pixi.js": "^8.22.0",
82
81
  "pixi-filters": "^6.1.5",
@@ -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.26.3. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
6
+ > Written for pixi-effects 0.27.1. 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
 
@@ -45,7 +45,7 @@ blendMode: 'normal' | 'add' | 'screen' | 'multiply' | 'overlay' | 'soft-light' |
45
45
 
46
46
  Animatable props (`initial` / keyframes): `x y alpha rotation scale scaleX scaleY pivotX pivotY anchorX anchorY skewX skewY tint width height visible autoAlpha`, plus for shapes their style/geometry, for text `fill`, for audio `volume`, for 3D `z rotationX rotationY`, filter params as `'filters.<name>.<param>'`, three objects as `'three.<obj>.<path>'`. **These go in `initial` / keyframes, never at the top level of a shape or text layer** (top-level `x`, `rotation`, `strokeColor`, `fillAlpha` each warn "is not a … key"): the top level of a shape holds its geometry (`width height radius d startAngle endAngle cornerRadius anchorX anchorY`), `trimStart` / `trimEnd`, `strokeCap` / `strokeJoin`, `fillGradient`.
47
47
 
48
- Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate to), `from` (animate from), `from`+`to`; a value may be relative to where the layer stands (`from: { x: '-=36' }` slides in from 36 px to the left of its place). `from` / `from+to` show their start value from the layer's start, so a delayed fade-in needs no `initial.alpha`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed). `ease` = any GSAP ease (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`, **`'cubic-bezier(.4, 0, .2, 1)'`** (CSS syntax, x1 and x2 in 0…1; `.4, 0, .2, 1` is a gentle in-out; `.2, .8, .2, 1` is FRONT-LOADED, not slow motion: half of the change in the first 13 % of the time, 90 % by 39 %, 99 % by 74 %, then a long settle, so what sits at the END of the content gets the glide; `.2, .8, .5, 1` has a shorter tail). GSAP's `power1…4` are the powers 2…5: `power2` is a cubic, `power2.in` has done only 12.5 % at half time (42 % at three quarters)… and springs: `'spring(mass, stiffness, damping)'` or `'spring.gentle' | 'snappy' | 'bouncy' | 'wobbly' | 'slow'`; **`duration: 'auto'` = the time the spring takes to settle** (presets, settle time / highest value reached as a multiple of the target: `snappy` 0.42 s / 1.01, `gentle` 0.72 s / 1.02, `bouncy` 1.08 s / 1.28, `slow` 1.29 s / 1.00, `wobbly` 1.61 s / 1.40: time the next beat from these) (real physics; with a number `duration` is the settle time and only the damping ratio shapes it; a damping ratio of 1 or more never overshoots, so `spring(1,170,26)` does not bounce: use damping below 20 at stiffness 170; `'auto'` works on keyframes only, not in `animateText` / `orbit` tweens; a `stagger` ease may be a spring, its delays are held inside the spread). Default ease is linear.
48
+ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate to), `from` (animate from), `from`+`to`; a value may be relative to where the layer stands (`from: { x: '-=36' }` slides in from 36 px to the left of its place). `from` / `from+to` show their start value from the layer's start, so a delayed fade-in needs no `initial.alpha`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed). `ease` = an ease name (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`, **`'cubic-bezier(.4, 0, .2, 1)'`** (CSS syntax, x1 and x2 in 0…1; `.4, 0, .2, 1` is a gentle in-out; `.2, .8, .2, 1` is FRONT-LOADED, not slow motion: half of the change in the first 13 % of the time, 90 % by 39 %, 99 % by 74 %, then a long settle, so what sits at the END of the content gets the glide; `.2, .8, .5, 1` has a shorter tail). `power1…4` are the powers 2…5: `power2` is a cubic, `power2.in` has done only 12.5 % at half time (42 % at three quarters)… and springs: `'spring(mass, stiffness, damping)'` or `'spring.gentle' | 'snappy' | 'bouncy' | 'wobbly' | 'slow'`; **`duration: 'auto'` = the time the spring takes to settle** (presets, settle time / highest value reached as a multiple of the target: `snappy` 0.42 s / 1.01, `gentle` 0.72 s / 1.02, `bouncy` 1.08 s / 1.28, `slow` 1.29 s / 1.00, `wobbly` 1.61 s / 1.40: time the next beat from these) (real physics; with a number `duration` is the settle time and only the damping ratio shapes it; a damping ratio of 1 or more never overshoots, so `spring(1,170,26)` does not bounce: use damping below 20 at stiffness 170; `'auto'` works on keyframes only, not in `animateText` / `orbit` tweens; a `stagger` ease may be a spring, its delays are held inside the spread). Default ease is linear.
49
49
 
50
50
  ## Layer types
51
51
 
@@ -53,14 +53,14 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
53
53
  30c. **Look with the built-in tools, not OS screenshots**: `await movie.contactSheet({ count: 6, as: 'dataURL' })` returns one labelled image of the whole animation; `await movie.inspect(frame)` returns every layer's canvas bounds and `issues`. If you do use a browser tool, `screenshot` needs an **absolute path**, macOS `sed -i` needs `''`, and a static server may redirect `/x.html` → `/x` and drop `?query`. Keep the template's `try/catch` around `init` so failures land in `window.__logs`. Check ≥ 5 timestamps including mid-transition. *(fixed: tools built in)*
54
54
  31. **Check audio with `movie.inspectAudio()`**: every sound measured on its own (`sound.length`, `peakDb`, `loudestAt`, `brightnessHz`), when it plays, loudness and brightness per 0.1 s window, and `issues` (the mix was limited, a layer is inaudible, an sfx is cut off by the end). Decoding a render by hand is no longer needed: `render()` encodes the same mix, on time in mp4 / mov / webm / mkv. *(fixed)*
55
55
 
56
- 31b. **A mistyped `ease` now warns** (`ease: 'power3.outt'` → did you mean `power3.out`). It used to run silently as GSAP's default (`power1.out`), so the motion changed and nothing said so.
56
+ 31b. **A mistyped `ease` now warns** (`ease: 'power3.outt'` → did you mean `power3.out`). It used to run silently as the default ease (`power1.out`), so the motion changed and nothing said so.
57
57
  31c. **`draft` and `scale` make the file smaller, not the drawing faster** (measured: a 48 s 1080p film exports in about the same time as a draft; the file is 7× smaller). To save time, export a part (`range` / `--range` / `--scene`), look at single moments (`check --no-export --at …`), and keep motion blur off while you iterate.
58
58
 
59
59
  ## Round 2 — found by building 16 portfolio pieces (examples/gallery)
60
60
 
61
61
  32. **Seeking is safe now** *(fixed)*: a `set` keyframe (counter value, text fill, image tint, shape style) is undone when you seek back before it, and shapes are redrawn before they are culled, so a rect growing from `width: 0` at the left edge draws after any jump seek (contact sheet, snapshot, scrubbing, export). The old workarounds (`width: 0.01`, a one-frame `to` instead of `set`) are not needed.
62
62
  33. **A mask starts and ends with the layer it masks** *(fixed)*, so its keyframes count from that layer's start. A mask with an explicit `at` keeps composition time. Before, a mask `at: 0` reveal had finished before a layer that appeared at 8 s, and the layer stayed invisible without any warning.
63
- 34. **`from` and `from+to` show their start value from the LAYER's start**, not from the keyframe's `at` (GSAP behaviour): a delayed fade-in `{ at: 2, from: { alpha: 0 }, to: { alpha: 1 } }` needs no `initial.alpha`. To keep the current value until `at` and then jump, use `set`. *(docs)*
63
+ 34. **`from` and `from+to` show their start value from the LAYER's start**, not from the keyframe's `at` (as in GSAP and After Effects): a delayed fade-in `{ at: 2, from: { alpha: 0 }, to: { alpha: 1 } }` needs no `initial.alpha`. To keep the current value until `at` and then jump, use `set`. *(docs)*
64
64
  35. **One `Movie` per page.** Creating a second Movie (for an isolated repro) and destroying it breaks the first (`null.clear`). Reload instead. *(docs)*
65
65
  36. **Multi-line text is centre-aligned by default** (`style.align: 'left'` for left-aligned paragraphs). *(docs)*
66
66
  37. **Composition size.** A 2D composition does not clip its children (a child outside `width × height` still draws), but its rectangle is the culling area: if the whole rectangle is off the canvas the group is skipped even when a child is on screen — keep it near what it draws, or canvas-sized. A `threeD` composition (a card) IS clipped at its `width × height` (it is rendered into a texture that size): size it for the soft shadow too. A growing `threeD` shape or text is fine: its texture is resized in place (it used to warn "destroyed while still bound"). *(measured; docs / fixed)*
@@ -100,7 +100,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
100
100
  70. **`speed` is a setting, `time` is the property.** `speed: 2` on the layer; to change the speed over time animate `time` (a curve of seconds in the file), never `keyframes: [{ to: { speed: 3 } }]` (warns). `speed: 0` is not allowed (hold a moment with `time` to the same value twice); `speed` together with keyframed `time`: `time` wins. Other names (`playbackRate`, `timeScale`, `rate`, `reverse: true`, `timeRemap`) are warned about with what to write. `particles({ speed })` is something else: launch speed in px per second.
101
101
  71. **Sound follows `speed` like a tape: the pitch moves with it.** An audio file or a video's sound at `speed: 2` is an octave up; backward plays reversed. Not for `sfx` / `music` layers (warns): change an sfx with `pitch` (semitones), or put the sfx **inside a remapped composition** and it follows that clock (slower and lower, or backward).
102
102
  72. **A video at `speed: 2` with no `duration` is half as long; with a `duration` longer than the file it holds the last frame** (add `loop: true` to repeat). A negative `time` counts back from the end of the file (`initial: { time: -2 }` = two seconds before the end).
103
- 73. **Know the shape of the curve before you time a beat to it.** `cubic-bezier(.2, .8, .2, 1)` does half of its change in the first 13 % of the time and 99 % by 74 % (not slow motion: over 6 s the first 2 s of content pass in 0.6 s); `.2, .8, .5, 1` has a shorter tail. GSAP's `power2` is a cubic (`power1` = t², `power3` = t⁴): `power2.in` has done 12.5 % at half time and 42 % at three quarters, so a rewind with it dismantles most of the title in the last third.
103
+ 73. **Know the shape of the curve before you time a beat to it.** `cubic-bezier(.2, .8, .2, 1)` does half of its change in the first 13 % of the time and 99 % by 74 % (not slow motion: over 6 s the first 2 s of content pass in 0.6 s); `.2, .8, .5, 1` has a shorter tail. `power2` is a cubic (`power1` = t², `power3` = t⁴): `power2.in` has done 12.5 % at half time and 42 % at three quarters, so a rewind with it dismantles most of the title in the last third.
104
104
  74. **`focus` as a layer name means that layer's FIRST `z`, and the name must be unique.** (Two layers with one name: the first `threeD` one is used and it warns.) The camera does not follow a layer whose `z` moves (it warns): to follow it, animate `focus` itself with numbers. A name no layer has, or a layer that is not `threeD`, warns and the `z = 0` plane is used (did-you-mean for typos).
105
105
  75. **Depth of field blurs `threeD` layers only, and the layer's own depth decides it.** A 2D layer never blurs; a `threeD` layer at the focus depth stays sharp; the depth is measured along the camera's view direction, so with an orbiting or tilted camera a layer off to the side of the look-at point is a little deeper than its `z` says and blurs a little even at the focus `z`; one blur for the whole layer, so a tilted floor is one blur (split it into layers).
106
106
  76. **`aperture` is the lens diameter in px, not a 0–1 strength.** 30 is the default (clearly shallow), 0 turns it off, 60 is strong, 100 is extreme (small bright objects speckle); the blur never exceeds 32 px. 20 or more blurred layers at once warns (one filter pass each, more on WebGPU).