pixi-effects 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +7 -6
  3. package/ai/reference/cheatsheet.md +3 -1
  4. package/ai/reference/pitfalls.md +1 -0
  5. package/ai/reference/recipes.md +36 -1
  6. package/ai/template.html +3 -3
  7. package/dist/{Base-DnRAxroQ.d.cts → Base-4vNLl4tH.d.cts} +1 -1
  8. package/dist/{Base-CPSFBkPm.d.ts → Base-CD4AoT5-.d.ts} +1 -1
  9. package/dist/Composition-7SO5WYHU.js +4 -0
  10. package/dist/{Composition-TUCRSMJU.js.map → Composition-7SO5WYHU.js.map} +1 -1
  11. package/dist/Composition-Y5VZSDDP.cjs +13 -0
  12. package/dist/{Composition-63EXHNJJ.cjs.map → Composition-Y5VZSDDP.cjs.map} +1 -1
  13. package/dist/Controller.cjs +10 -2
  14. package/dist/Controller.cjs.map +1 -1
  15. package/dist/Controller.d.cts +3 -2
  16. package/dist/Controller.d.ts +3 -2
  17. package/dist/Controller.js +10 -2
  18. package/dist/Controller.js.map +1 -1
  19. package/dist/{Movie-VyDiUYOj.d.ts → Movie-CADDBfpl.d.cts} +3 -1
  20. package/dist/{Movie-CuU_aaCm.d.cts → Movie-Ch2SUI1h.d.ts} +3 -1
  21. package/dist/{chunk-23M5QTWW.cjs → chunk-4AGJF5RK.cjs} +21 -21
  22. package/dist/{chunk-23M5QTWW.cjs.map → chunk-4AGJF5RK.cjs.map} +1 -1
  23. package/dist/{chunk-UBPZLGLU.js → chunk-7IJHJXDI.js} +241 -37
  24. package/dist/chunk-7IJHJXDI.js.map +1 -0
  25. package/dist/{chunk-5Q5XZG7Z.js → chunk-B2FTJLNB.js} +3 -3
  26. package/dist/{chunk-5Q5XZG7Z.js.map → chunk-B2FTJLNB.js.map} +1 -1
  27. package/dist/{chunk-2RR43OIN.cjs → chunk-KQM2ZZMK.cjs} +241 -37
  28. package/dist/chunk-KQM2ZZMK.cjs.map +1 -0
  29. package/dist/index.cjs +58 -19
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +4 -4
  32. package/dist/index.d.ts +4 -4
  33. package/dist/index.js +49 -10
  34. package/dist/index.js.map +1 -1
  35. package/dist/three.cjs +5 -5
  36. package/dist/three.d.cts +2 -2
  37. package/dist/three.d.ts +2 -2
  38. package/dist/three.js +1 -1
  39. package/dist/{types-7VtTEKUW.d.cts → types-DrW9QeKU.d.cts} +22 -6
  40. package/dist/{types-7VtTEKUW.d.ts → types-DrW9QeKU.d.ts} +22 -6
  41. package/docs/api.md +1 -0
  42. package/docs/dsl.md +26 -7
  43. package/llms-full.txt +67 -9
  44. package/package.json +1 -1
  45. package/dist/Composition-63EXHNJJ.cjs +0 -13
  46. package/dist/Composition-TUCRSMJU.js +0 -4
  47. package/dist/chunk-2RR43OIN.cjs.map +0 -1
  48. package/dist/chunk-UBPZLGLU.js.map +0 -1
package/dist/three.cjs CHANGED
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- var chunk2RR43OIN_cjs = require('./chunk-2RR43OIN.cjs');
3
+ var chunkKQM2ZZMK_cjs = require('./chunk-KQM2ZZMK.cjs');
4
4
  var pixi_js = require('pixi.js');
5
5
  var three$1 = require('three');
6
6
 
7
- var ThreeSequence = class extends chunk2RR43OIN_cjs.Sequence {
7
+ var ThreeSequence = class extends chunkKQM2ZZMK_cjs.Sequence {
8
8
  _renderer = null;
9
9
  _scene = null;
10
10
  _camera = null;
@@ -23,7 +23,7 @@ var ThreeSequence = class extends chunk2RR43OIN_cjs.Sequence {
23
23
  if (this.duration === void 0) {
24
24
  this.duration = this.parent?.duration ?? this.root.duration;
25
25
  }
26
- const scope = chunk2RR43OIN_cjs.buildScope(this, this.parent, this.root);
26
+ const scope = chunkKQM2ZZMK_cjs.buildScope(this, this.parent, this.root);
27
27
  const width = resolveDim(spec.width, scope) ?? this.parent?.width ?? this.root.width;
28
28
  const height = resolveDim(spec.height, scope) ?? this.parent?.height ?? this.root.height;
29
29
  const resolution = spec.resolution ?? 1;
@@ -143,12 +143,12 @@ var ThreeSequence = class extends chunk2RR43OIN_cjs.Sequence {
143
143
  function resolveDim(v, scope) {
144
144
  if (v === void 0) return void 0;
145
145
  if (typeof v === "number") return v;
146
- return chunk2RR43OIN_cjs.evaluateExpr(v, scope);
146
+ return chunkKQM2ZZMK_cjs.evaluateExpr(v, scope);
147
147
  }
148
148
 
149
149
  // src/three/index.ts
150
150
  function registerThree() {
151
- chunk2RR43OIN_cjs.registerSequenceType("three", ThreeSequence);
151
+ chunkKQM2ZZMK_cjs.registerSequenceType("three", ThreeSequence);
152
152
  }
153
153
  function three(spec) {
154
154
  return spec;
package/dist/three.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { b as SequenceCommon, q as PropValue, S as SequenceSpec } from './types-7VtTEKUW.cjs';
1
+ import { b as SequenceCommon, q as PropValue, S as SequenceSpec } from './types-DrW9QeKU.cjs';
2
2
  import { Scene, Camera, WebGLRenderer } from 'three';
3
- import { S as Sequence, P as PathRouters } from './Base-DnRAxroQ.cjs';
3
+ import { S as Sequence, P as PathRouters } from './Base-4vNLl4tH.cjs';
4
4
  import 'pixi.js';
5
5
  import 'gsap';
6
6
 
package/dist/three.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { b as SequenceCommon, q as PropValue, S as SequenceSpec } from './types-7VtTEKUW.js';
1
+ import { b as SequenceCommon, q as PropValue, S as SequenceSpec } from './types-DrW9QeKU.js';
2
2
  import { Scene, Camera, WebGLRenderer } from 'three';
3
- import { S as Sequence, P as PathRouters } from './Base-CPSFBkPm.js';
3
+ import { S as Sequence, P as PathRouters } from './Base-CD4AoT5-.js';
4
4
  import 'pixi.js';
5
5
  import 'gsap';
6
6
 
package/dist/three.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Sequence, buildScope, evaluateExpr, registerSequenceType } from './chunk-UBPZLGLU.js';
1
+ import { Sequence, buildScope, evaluateExpr, registerSequenceType } from './chunk-7IJHJXDI.js';
2
2
  import { Texture, Sprite } from 'pixi.js';
3
3
  import { WebGLRenderer, Scene, PerspectiveCamera } from 'three';
4
4
 
@@ -212,6 +212,10 @@ interface TextSequenceSpec extends SequenceCommon {
212
212
  * The text. May contain `{value}`, replaced by the layer's animatable number
213
213
  * `value` (a counter): `text: '{value} users'`, `initial: { value: 0 }`,
214
214
  * keyframe `to: { value: 2480 }`. Format it with `format`.
215
+ *
216
+ * The text can change over time: a keyframe `set: { text: 'Two' }` swaps the string at that time (and back when
217
+ * you seek back; a string cannot be tweened). `visibleChars` is a number that shows only the first N characters
218
+ * (a typewriter): `initial: { visibleChars: 0 }`, keyframe `to: { visibleChars: 20 }`. It starts at 0 once it is animated.
215
219
  */
216
220
  text?: string;
217
221
  /** How `{value}` is printed: `decimals` (default 0) and thousands `grouping` (default false). */
@@ -336,6 +340,18 @@ interface GradientSpec {
336
340
  /** radial: outer radius where the last stop applies, 0–1. Default 0.5. */
337
341
  radius?: number;
338
342
  }
343
+ /**
344
+ * Stroke trim (draw-on): the part of the outline that is stroked, as fractions 0–1 of its length. Animate `trimEnd`
345
+ * from 0 to 1 to draw a line / path / outline on; animate both for a travelling dash. The outline of a rect starts at
346
+ * its top-left and goes clockwise, of a circle / ellipse at 12 o'clock and goes clockwise; a polygon's closing edge counts.
347
+ * The fill is not trimmed. Not for `arc` (animate its `endAngle`).
348
+ */
349
+ interface Trimmable {
350
+ /** Where the stroke starts, 0–1 of the outline. Default 0. Animatable. */
351
+ trimStart?: PropValue;
352
+ /** Where the stroke ends, 0–1 of the outline. Default 1. Animatable. */
353
+ trimEnd?: PropValue;
354
+ }
339
355
  interface ShapeBase extends SequenceCommon {
340
356
  type: 'shape';
341
357
  /** Fill with a gradient instead of `fillColor` (top level or in `initial`). */
@@ -358,7 +374,7 @@ interface ShapeBase extends SequenceCommon {
358
374
  /** How stroke corners join: `'miter'` (default; sharp points can poke out), `'round'` or `'bevel'`. Top level or in `initial`. */
359
375
  strokeJoin?: 'miter' | 'round' | 'bevel';
360
376
  }
361
- interface RectShapeSpec extends ShapeBase {
377
+ interface RectShapeSpec extends ShapeBase, Trimmable {
362
378
  shape: 'rect';
363
379
  width: PropValue;
364
380
  height: PropValue;
@@ -373,14 +389,14 @@ interface RectShapeSpec extends ShapeBase {
373
389
  anchorX?: PropValue;
374
390
  anchorY?: PropValue;
375
391
  }
376
- interface CircleShapeSpec extends ShapeBase {
392
+ interface CircleShapeSpec extends ShapeBase, Trimmable {
377
393
  shape: 'circle';
378
394
  radius: PropValue;
379
395
  /** See RectShapeSpec.anchorX. Default 0.5 (centre). */
380
396
  anchorX?: PropValue;
381
397
  anchorY?: PropValue;
382
398
  }
383
- interface EllipseShapeSpec extends ShapeBase {
399
+ interface EllipseShapeSpec extends ShapeBase, Trimmable {
384
400
  shape: 'ellipse';
385
401
  radiusX: PropValue;
386
402
  radiusY: PropValue;
@@ -404,20 +420,20 @@ interface ArcShapeSpec extends ShapeBase {
404
420
  /** Degrees. Default 360. Animatable. */
405
421
  endAngle?: PropValue;
406
422
  }
407
- interface LineShapeSpec extends ShapeBase {
423
+ interface LineShapeSpec extends ShapeBase, Trimmable {
408
424
  shape: 'line';
409
425
  /** Line endpoints relative to the shape's local origin. */
410
426
  from: [PropValue, PropValue];
411
427
  to: [PropValue, PropValue];
412
428
  }
413
- interface PolygonShapeSpec extends ShapeBase {
429
+ interface PolygonShapeSpec extends ShapeBase, Trimmable {
414
430
  shape: 'polygon';
415
431
  /** Vertices in local space. The path is auto-closed. */
416
432
  points: Array<[PropValue, PropValue]>;
417
433
  /** When true, draw an open polyline instead of a closed polygon. Default false. */
418
434
  open?: boolean;
419
435
  }
420
- interface PathShapeSpec extends ShapeBase {
436
+ interface PathShapeSpec extends ShapeBase, Trimmable {
421
437
  shape: 'path';
422
438
  /** SVG path data (`d` attribute). Goes through PIXI's GraphicsContext.svg(). */
423
439
  d: string;
@@ -212,6 +212,10 @@ interface TextSequenceSpec extends SequenceCommon {
212
212
  * The text. May contain `{value}`, replaced by the layer's animatable number
213
213
  * `value` (a counter): `text: '{value} users'`, `initial: { value: 0 }`,
214
214
  * keyframe `to: { value: 2480 }`. Format it with `format`.
215
+ *
216
+ * The text can change over time: a keyframe `set: { text: 'Two' }` swaps the string at that time (and back when
217
+ * you seek back; a string cannot be tweened). `visibleChars` is a number that shows only the first N characters
218
+ * (a typewriter): `initial: { visibleChars: 0 }`, keyframe `to: { visibleChars: 20 }`. It starts at 0 once it is animated.
215
219
  */
216
220
  text?: string;
217
221
  /** How `{value}` is printed: `decimals` (default 0) and thousands `grouping` (default false). */
@@ -336,6 +340,18 @@ interface GradientSpec {
336
340
  /** radial: outer radius where the last stop applies, 0–1. Default 0.5. */
337
341
  radius?: number;
338
342
  }
343
+ /**
344
+ * Stroke trim (draw-on): the part of the outline that is stroked, as fractions 0–1 of its length. Animate `trimEnd`
345
+ * from 0 to 1 to draw a line / path / outline on; animate both for a travelling dash. The outline of a rect starts at
346
+ * its top-left and goes clockwise, of a circle / ellipse at 12 o'clock and goes clockwise; a polygon's closing edge counts.
347
+ * The fill is not trimmed. Not for `arc` (animate its `endAngle`).
348
+ */
349
+ interface Trimmable {
350
+ /** Where the stroke starts, 0–1 of the outline. Default 0. Animatable. */
351
+ trimStart?: PropValue;
352
+ /** Where the stroke ends, 0–1 of the outline. Default 1. Animatable. */
353
+ trimEnd?: PropValue;
354
+ }
339
355
  interface ShapeBase extends SequenceCommon {
340
356
  type: 'shape';
341
357
  /** Fill with a gradient instead of `fillColor` (top level or in `initial`). */
@@ -358,7 +374,7 @@ interface ShapeBase extends SequenceCommon {
358
374
  /** How stroke corners join: `'miter'` (default; sharp points can poke out), `'round'` or `'bevel'`. Top level or in `initial`. */
359
375
  strokeJoin?: 'miter' | 'round' | 'bevel';
360
376
  }
361
- interface RectShapeSpec extends ShapeBase {
377
+ interface RectShapeSpec extends ShapeBase, Trimmable {
362
378
  shape: 'rect';
363
379
  width: PropValue;
364
380
  height: PropValue;
@@ -373,14 +389,14 @@ interface RectShapeSpec extends ShapeBase {
373
389
  anchorX?: PropValue;
374
390
  anchorY?: PropValue;
375
391
  }
376
- interface CircleShapeSpec extends ShapeBase {
392
+ interface CircleShapeSpec extends ShapeBase, Trimmable {
377
393
  shape: 'circle';
378
394
  radius: PropValue;
379
395
  /** See RectShapeSpec.anchorX. Default 0.5 (centre). */
380
396
  anchorX?: PropValue;
381
397
  anchorY?: PropValue;
382
398
  }
383
- interface EllipseShapeSpec extends ShapeBase {
399
+ interface EllipseShapeSpec extends ShapeBase, Trimmable {
384
400
  shape: 'ellipse';
385
401
  radiusX: PropValue;
386
402
  radiusY: PropValue;
@@ -404,20 +420,20 @@ interface ArcShapeSpec extends ShapeBase {
404
420
  /** Degrees. Default 360. Animatable. */
405
421
  endAngle?: PropValue;
406
422
  }
407
- interface LineShapeSpec extends ShapeBase {
423
+ interface LineShapeSpec extends ShapeBase, Trimmable {
408
424
  shape: 'line';
409
425
  /** Line endpoints relative to the shape's local origin. */
410
426
  from: [PropValue, PropValue];
411
427
  to: [PropValue, PropValue];
412
428
  }
413
- interface PolygonShapeSpec extends ShapeBase {
429
+ interface PolygonShapeSpec extends ShapeBase, Trimmable {
414
430
  shape: 'polygon';
415
431
  /** Vertices in local space. The path is auto-closed. */
416
432
  points: Array<[PropValue, PropValue]>;
417
433
  /** When true, draw an open polyline instead of a closed polygon. Default false. */
418
434
  open?: boolean;
419
435
  }
420
- interface PathShapeSpec extends ShapeBase {
436
+ interface PathShapeSpec extends ShapeBase, Trimmable {
421
437
  shape: 'path';
422
438
  /** SVG path data (`d` attribute). Goes through PIXI's GraphicsContext.svg(). */
423
439
  d: string;
package/docs/api.md CHANGED
@@ -182,6 +182,7 @@ movie.off(event, fn): this
182
182
  | ----------- | ------------------------------------------------- | ----------------------------------------------------- |
183
183
  | `'ready'` | none | once, after `init()` resolves |
184
184
  | `'frame'` | `{ frame: number; totalFrames: number }` | every `gotoFrame` (so once per playback frame too) |
185
+ | `'play'` | none | when `play()` actually transitions from paused |
185
186
  | `'pause'` | none | when `pause()` actually transitions from playing |
186
187
  | `'progress'`| `{ progress: number; frame: number; totalFrames: number }` | during `render()`, once per encoded frame |
187
188
 
package/docs/dsl.md CHANGED
@@ -71,7 +71,7 @@ A layer is **not removed** when its lifespan `[at, at + duration)` ends: it is o
71
71
 
72
72
  ### `text`
73
73
 
74
- Renders a [PIXI.Text](https://pixijs.com/8.x/guides/components/scene-objects/text/text). Animatable position, rotation, scale, opacity and `fill`. `style` accepts any PixiJS `TextStyle` field and is applied once at build time — notably `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding` (numbers may be expressions). A `dropShadow` glow is clipped unless `padding` is at least twice its `blur`. Text content cannot change over time, with one exception: a `{value}` placeholder in `text` prints an animatable number (`text: '{value} users'`, `initial: { value: 0 }`, a keyframe `to: { value: 2480 }`; `format: { decimals, grouping }`) — that is how counters work. Any other change of wording is a second layer (stack layers with `at` / `duration`). Multi-line text is centre-aligned unless `style.align` says otherwise. In expressions `w` / `h` are the size of the styled text, so `x: '-w'` places it just off the left edge.
74
+ 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.
75
75
 
76
76
  ```ts
77
77
  {
@@ -103,6 +103,17 @@ The text's `fill` is also animatable via keyframes (under the `to`/`from`/`set`
103
103
  }
104
104
  ```
105
105
 
106
+ #### Changing text: `set: { text }` and `visibleChars`
107
+
108
+ 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:
109
+
110
+ ```ts
111
+ { type: 'text', text: '3', duration: 4, style: { fontSize: 150 }, initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
112
+ keyframes: [{ at: 1, set: { text: '2' } }, { at: 2, set: { text: '1' } }, { at: 3, set: { text: 'Go!' } }] }
113
+ ```
114
+
115
+ `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.
116
+
106
117
  #### Counters (`{value}`)
107
118
 
108
119
  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`):
@@ -250,13 +261,13 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
250
261
 
251
262
  | `shape` | Required props | Optional |
252
263
  |-----------|------------------------------------------|------------------------|
253
- | `rect` | `width`, `height` | `cornerRadius` |
254
- | `circle` | `radius` | — |
255
- | `ellipse` | `radiusX`, `radiusY` | — |
264
+ | `rect` | `width`, `height` | `cornerRadius`, `trimStart`, `trimEnd` |
265
+ | `circle` | `radius` | `trimStart`, `trimEnd` |
266
+ | `ellipse` | `radiusX`, `radiusY` | `trimStart`, `trimEnd` |
256
267
  | `arc` | `radius` | `innerRadius`, `startAngle` (0), `endAngle` (360) |
257
- | `line` | `from: [x,y]`, `to: [x,y]` (canvas coordinates; stroke in `initial`) | — |
258
- | `polygon` | `points: [[x,y], …]` | `open` (default false) |
259
- | `path` | `d` (SVG path data) | — |
268
+ | `line` | `from: [x,y]`, `to: [x,y]` (canvas coordinates; stroke in `initial`) | `trimStart`, `trimEnd` |
269
+ | `polygon` | `points: [[x,y], …]` | `open` (default false), `trimStart`, `trimEnd` |
270
+ | `path` | `d` (SVG path data) | `trimStart`, `trimEnd` |
260
271
 
261
272
  **`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.
262
273
 
@@ -266,6 +277,14 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
266
277
  keyframes: [{ at: 0, to: { endAngle: 270 }, duration: 2, ease: 'power3.out' }] } // a progress ring filling clockwise from the top
267
278
  ```
268
279
 
280
+ **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).
281
+
282
+ ```ts
283
+ { type: 'shape', shape: 'path', d: 'M 0 50 L 45 95 L 130 0', trimEnd: 0, strokeCap: 'round', strokeJoin: 'round',
284
+ initial: { x: 1040, y: 270, strokeColor: '#7bd88f', strokeWidth: 16 },
285
+ keyframes: [{ at: 0, to: { trimEnd: 1 }, duration: 1, ease: 'power2.inOut' }] } // a check mark drawing itself
286
+ ```
287
+
269
288
  `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'`.
270
289
 
271
290
  **Style** is set on `initial` and animatable via keyframes:
package/llms-full.txt CHANGED
@@ -104,7 +104,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
104
104
 
105
105
  ## Layer types
106
106
 
107
- **text** — `text`, `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */ }`) — that is how counters work. Other content cannot change over time. Multi-line text is centre-aligned by default (`style.align: 'left'` to change).
107
+ **text** — `text`, `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */ }`) — that is how counters work. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change).
108
108
 
109
109
  **image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'`. Default anchor top-left; natural size = `w`,`h`.
110
110
 
@@ -125,6 +125,8 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
125
125
  | polygon | `points: [[x,y],…] open` — canvas coordinates like `line` |
126
126
  | path | `d` (SVG path data) — canvas coordinates like `line` |
127
127
 
128
+ **Draw-on:** every shape but `arc` has `trimStart` / `trimEnd` (0–1 of the outline, default 0 / 1, animatable): `trimEnd: 0` + keyframe `to: { trimEnd: 1 }` draws a line / border / path on; `trimStart` 0 → 1 after it wipes it off; both = a travelling dash. The fill is NOT trimmed. Rect outline starts top-left, circle / ellipse at 12 o'clock, both clockwise. Use `strokeCap: 'round'`.
129
+
128
130
  Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha strokeWidth`; `colorSpace: 'oklab'|'oklch'` for clean colour tweens. rect/circle/ellipse are **centred on `x,y` by default** (`anchorX/anchorY` default 0.5): for a bar growing from its base use `anchorY: 1` (or `anchorX: 0` for left-to-right). **`fillGradient`** (top level or `initial`; instead of `fillColor`): `{ type?: 'linear'|'radial', stops: [[0, '#000'], [1, 'rgba(0,0,0,.6)']], angle? /* linear, deg, 90 = top→bottom */, center?, innerRadius?, radius? /* radial, 0–1 of bounds */ }` — stops may have alpha, so a radial transparent→dark is a vignette. Stroke ends and corners: `strokeCap: 'butt'|'round'|'square'`, `strokeJoin: 'miter'|'round'|'bevel'` (top level or in `initial`; not animated — `round` keeps sharp corners like an M from poking out).
129
131
 
130
132
  **composition** — `width height duration sequences transitions`. With `threeD: true` it is a **card**: its children are drawn into one texture (content outside its `width × height` is clipped: size it for a soft shadow too; a plain 2D composition does not clip, but is skipped when its whole rectangle is off screen) and move / rotate in depth together. Children use the composition's local coordinates and times. Position/rotate/scale it as a unit; to rotate/scale about its centre set `pivotX/pivotY` to the centre and `x/y` to where that point should sit.
@@ -269,6 +271,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
269
271
  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
272
  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
273
  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)*
274
+ 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
275
 
273
276
 
274
277
  <!-- ===== ai/reference/recipes.md ===== -->
@@ -527,7 +530,7 @@ return [
527
530
 
528
531
  ## Slideshow with transitions and background music
529
532
 
530
- Scene `i` starts at `i × (L − T)` and overlaps the next by the transition length `T`; the transition's `at` is the next scene's start; total = `n × (L − T) + T`. Transitions and `kenBurns` combine freely. Captions are top-level layers (transitions never touch layers they do not name); keep them above the bottom ~60 px. `bgm.mp3` is only 6 s: **`loop: true`** or it falls silent.
533
+ 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
534
 
532
535
  ```js
533
536
  // @recipe slideshow
@@ -647,6 +650,41 @@ return [
647
650
 
648
651
  ---
649
652
 
653
+ ## Draw a line, border or check mark on, and type text out
654
+
655
+ `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.
656
+
657
+ ```js
658
+ // @recipe draw-on-and-type
659
+ const ACCENT = '#ffd166', TEAL = '#4cc9f0', GREEN = '#7bd88f';
660
+ const drawOn = (at, duration = 1.2) => ({ at, to: { trimEnd: 1 }, duration, ease: 'power2.inOut' });
661
+ return [
662
+ { type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0a0e1a' } },
663
+ // an underline that draws itself
664
+ { type: 'shape', shape: 'line', from: [340, 120], to: [940, 120], trimEnd: 0, strokeCap: 'round',
665
+ initial: { strokeColor: ACCENT, strokeWidth: 6 }, keyframes: [drawOn(0.2, 1)] },
666
+ // a card border (clockwise from the top-left), then its fill fades in
667
+ { type: 'shape', shape: 'rect', width: 360, height: 200, cornerRadius: 28, trimEnd: 0, strokeJoin: 'round',
668
+ initial: { x: 400, y: 360, strokeColor: TEAL, strokeWidth: 8, fillColor: TEAL, fillAlpha: 0 },
669
+ keyframes: [drawOn(0.6, 1.4), { at: 2, to: { fillAlpha: 0.2 }, duration: 0.6 }] },
670
+ // an SVG check mark
671
+ { type: 'shape', shape: 'path', d: 'M 0 50 L 45 95 L 130 0', trimEnd: 0, strokeCap: 'round', strokeJoin: 'round',
672
+ initial: { x: 880, y: 360, strokeColor: GREEN, strokeWidth: 16 }, keyframes: [drawOn(1.8, 0.9)] },
673
+ // a typewriter: left-aligned so it grows rightward; 30 characters over 2.4 s
674
+ { type: 'text', text: 'No mask, no per-letter layers.', at: 3, duration: 5,
675
+ style: { fontSize: 40, fill: '#e8eefc', fontFamily: 'ui-monospace, Menlo, monospace' },
676
+ initial: { x: 120, y: 560, anchorX: 0, anchorY: 0.5, visibleChars: 0 },
677
+ keyframes: [{ at: 0, to: { visibleChars: 30 }, duration: 2.4, ease: 'none' }] },
678
+ // a countdown: one layer whose string changes
679
+ { type: 'text', text: '3', at: 3, duration: 5,
680
+ style: { fontSize: 150, fontWeight: '900', fill: ACCENT },
681
+ initial: { x: 1060, y: 540, anchorX: 0.5, anchorY: 0.5 },
682
+ keyframes: [{ at: 1, set: { text: '2' } }, { at: 2, set: { text: '1' } }, { at: 3, set: { text: 'Go!' } }] },
683
+ ];
684
+ ```
685
+
686
+ ---
687
+
650
688
  ## Letters and words that animate one by one (`splitText`)
651
689
 
652
690
  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 +880,7 @@ A layer is **not removed** when its lifespan `[at, at + duration)` ends: it is o
842
880
 
843
881
  ### `text`
844
882
 
845
- Renders a [PIXI.Text](https://pixijs.com/8.x/guides/components/scene-objects/text/text). Animatable position, rotation, scale, opacity and `fill`. `style` accepts any PixiJS `TextStyle` field and is applied once at build time — notably `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding` (numbers may be expressions). A `dropShadow` glow is clipped unless `padding` is at least twice its `blur`. Text content cannot change over time, with one exception: a `{value}` placeholder in `text` prints an animatable number (`text: '{value} users'`, `initial: { value: 0 }`, a keyframe `to: { value: 2480 }`; `format: { decimals, grouping }`) — that is how counters work. Any other change of wording is a second layer (stack layers with `at` / `duration`). Multi-line text is centre-aligned unless `style.align` says otherwise. In expressions `w` / `h` are the size of the styled text, so `x: '-w'` places it just off the left edge.
883
+ 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
884
 
847
885
  ```ts
848
886
  {
@@ -874,6 +912,17 @@ The text's `fill` is also animatable via keyframes (under the `to`/`from`/`set`
874
912
  }
875
913
  ```
876
914
 
915
+ #### Changing text: `set: { text }` and `visibleChars`
916
+
917
+ 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:
918
+
919
+ ```ts
920
+ { type: 'text', text: '3', duration: 4, style: { fontSize: 150 }, initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
921
+ keyframes: [{ at: 1, set: { text: '2' } }, { at: 2, set: { text: '1' } }, { at: 3, set: { text: 'Go!' } }] }
922
+ ```
923
+
924
+ `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.
925
+
877
926
  #### Counters (`{value}`)
878
927
 
879
928
  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 +1070,13 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
1021
1070
 
1022
1071
  | `shape` | Required props | Optional |
1023
1072
  |-----------|------------------------------------------|------------------------|
1024
- | `rect` | `width`, `height` | `cornerRadius` |
1025
- | `circle` | `radius` | — |
1026
- | `ellipse` | `radiusX`, `radiusY` | — |
1073
+ | `rect` | `width`, `height` | `cornerRadius`, `trimStart`, `trimEnd` |
1074
+ | `circle` | `radius` | `trimStart`, `trimEnd` |
1075
+ | `ellipse` | `radiusX`, `radiusY` | `trimStart`, `trimEnd` |
1027
1076
  | `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) | — |
1077
+ | `line` | `from: [x,y]`, `to: [x,y]` (canvas coordinates; stroke in `initial`) | `trimStart`, `trimEnd` |
1078
+ | `polygon` | `points: [[x,y], …]` | `open` (default false), `trimStart`, `trimEnd` |
1079
+ | `path` | `d` (SVG path data) | `trimStart`, `trimEnd` |
1031
1080
 
1032
1081
  **`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
1082
 
@@ -1037,6 +1086,14 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
1037
1086
  keyframes: [{ at: 0, to: { endAngle: 270 }, duration: 2, ease: 'power3.out' }] } // a progress ring filling clockwise from the top
1038
1087
  ```
1039
1088
 
1089
+ **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).
1090
+
1091
+ ```ts
1092
+ { type: 'shape', shape: 'path', d: 'M 0 50 L 45 95 L 130 0', trimEnd: 0, strokeCap: 'round', strokeJoin: 'round',
1093
+ initial: { x: 1040, y: 270, strokeColor: '#7bd88f', strokeWidth: 16 },
1094
+ keyframes: [{ at: 0, to: { trimEnd: 1 }, duration: 1, ease: 'power2.inOut' }] } // a check mark drawing itself
1095
+ ```
1096
+
1040
1097
  `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
1098
 
1042
1099
  **Style** is set on `initial` and animatable via keyframes:
@@ -2026,6 +2083,7 @@ movie.off(event, fn): this
2026
2083
  | ----------- | ------------------------------------------------- | ----------------------------------------------------- |
2027
2084
  | `'ready'` | none | once, after `init()` resolves |
2028
2085
  | `'frame'` | `{ frame: number; totalFrames: number }` | every `gotoFrame` (so once per playback frame too) |
2086
+ | `'play'` | none | when `play()` actually transitions from paused |
2029
2087
  | `'pause'` | none | when `pause()` actually transitions from playing |
2030
2088
  | `'progress'`| `{ progress: number; frame: number; totalFrames: number }` | during `render()`, once per encoded frame |
2031
2089
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-effects",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,13 +0,0 @@
1
- 'use strict';
2
-
3
- var chunk23M5QTWW_cjs = require('./chunk-23M5QTWW.cjs');
4
- require('./chunk-2RR43OIN.cjs');
5
-
6
-
7
-
8
- Object.defineProperty(exports, "CompositionSequence", {
9
- enumerable: true,
10
- get: function () { return chunk23M5QTWW_cjs.CompositionSequence; }
11
- });
12
- //# sourceMappingURL=Composition-63EXHNJJ.cjs.map
13
- //# sourceMappingURL=Composition-63EXHNJJ.cjs.map
@@ -1,4 +0,0 @@
1
- export { CompositionSequence } from './chunk-5Q5XZG7Z.js';
2
- import './chunk-UBPZLGLU.js';
3
- //# sourceMappingURL=Composition-TUCRSMJU.js.map
4
- //# sourceMappingURL=Composition-TUCRSMJU.js.map