pixi-effects 0.4.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 (61) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.md +11 -6
  3. package/ai/SKILL.md +7 -4
  4. package/ai/reference/cheatsheet.md +18 -5
  5. package/ai/reference/pitfalls.md +16 -11
  6. package/ai/reference/recipes.md +153 -1
  7. package/ai/template.html +6 -3
  8. package/ai/tools/check.mjs +385 -0
  9. package/dist/{Base-DhY9oMxr.d.ts → Base-4vNLl4tH.d.cts} +6 -1
  10. package/dist/{Base-5R7C5pHR.d.cts → Base-CD4AoT5-.d.ts} +6 -1
  11. package/dist/Composition-7SO5WYHU.js +4 -0
  12. package/dist/{Composition-RGTRZ2JX.js.map → Composition-7SO5WYHU.js.map} +1 -1
  13. package/dist/Composition-Y5VZSDDP.cjs +13 -0
  14. package/dist/{Composition-D7RCAGEA.cjs.map → Composition-Y5VZSDDP.cjs.map} +1 -1
  15. package/dist/Controller.cjs +47 -15
  16. package/dist/Controller.cjs.map +1 -1
  17. package/dist/Controller.d.cts +5 -2
  18. package/dist/Controller.d.ts +5 -2
  19. package/dist/Controller.js +47 -15
  20. package/dist/Controller.js.map +1 -1
  21. package/dist/{Movie-DRZtIGU7.d.ts → Movie-CADDBfpl.d.cts} +58 -2
  22. package/dist/{Movie-CghFXprP.d.cts → Movie-Ch2SUI1h.d.ts} +58 -2
  23. package/dist/{chunk-QGBOC5YL.cjs → chunk-4AGJF5RK.cjs} +33 -21
  24. package/dist/chunk-4AGJF5RK.cjs.map +1 -0
  25. package/dist/{chunk-ZQX7WIYL.js → chunk-7IJHJXDI.js} +380 -64
  26. package/dist/chunk-7IJHJXDI.js.map +1 -0
  27. package/dist/{chunk-A574IA4F.js → chunk-B2FTJLNB.js} +18 -6
  28. package/dist/chunk-B2FTJLNB.js.map +1 -0
  29. package/dist/chunk-DKGG7WD4.js +61 -0
  30. package/dist/chunk-DKGG7WD4.js.map +1 -0
  31. package/dist/{chunk-6BQ6IEW2.cjs → chunk-KQM2ZZMK.cjs} +380 -63
  32. package/dist/chunk-KQM2ZZMK.cjs.map +1 -0
  33. package/dist/chunk-UQBGW5DO.cjs +64 -0
  34. package/dist/chunk-UQBGW5DO.cjs.map +1 -0
  35. package/dist/index.cjs +333 -89
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +54 -5
  38. package/dist/index.d.ts +54 -5
  39. package/dist/index.js +317 -75
  40. package/dist/index.js.map +1 -1
  41. package/dist/sfx-MZJZXYY3.cjs +446 -0
  42. package/dist/sfx-MZJZXYY3.cjs.map +1 -0
  43. package/dist/sfx-S37DDC66.js +434 -0
  44. package/dist/sfx-S37DDC66.js.map +1 -0
  45. package/dist/three.cjs +5 -5
  46. package/dist/three.d.cts +2 -2
  47. package/dist/three.d.ts +2 -2
  48. package/dist/three.js +1 -1
  49. package/dist/{types-CRE9WKd4.d.ts → types-DrW9QeKU.d.cts} +118 -12
  50. package/dist/{types-CRE9WKd4.d.cts → types-DrW9QeKU.d.ts} +118 -12
  51. package/docs/api.md +37 -0
  52. package/docs/dsl.md +70 -10
  53. package/llms-full.txt +300 -30
  54. package/llms.txt +1 -1
  55. package/package.json +2 -1
  56. package/dist/Composition-D7RCAGEA.cjs +0 -13
  57. package/dist/Composition-RGTRZ2JX.js +0 -4
  58. package/dist/chunk-6BQ6IEW2.cjs.map +0 -1
  59. package/dist/chunk-A574IA4F.js.map +0 -1
  60. package/dist/chunk-QGBOC5YL.cjs.map +0 -1
  61. package/dist/chunk-ZQX7WIYL.js.map +0 -1
@@ -212,12 +212,17 @@ 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). */
218
222
  format?: {
219
223
  decimals?: number;
220
- grouping?: boolean;
224
+ grouping?: boolean; /** Zero-pad the whole part to this many digits (`pad: 2` → `05`; a clock `18:42:05`). */
225
+ pad?: number;
221
226
  };
222
227
  /** Subset of PIXI v8 TextStyleOptions. String values may be exprs (e.g. fontSize: 'GW * 0.05'). */
223
228
  style?: Record<string, PropValue | {
@@ -232,12 +237,71 @@ interface TextSequenceSpec extends SequenceCommon {
232
237
  */
233
238
  colorSpace?: 'rgb' | 'oklab' | 'oklch';
234
239
  }
235
- interface AudioSequenceSpec extends SequenceCommon {
240
+ type SfxPreset = 'click' | 'pop' | 'swoosh' | 'swipe' | 'hit' | 'riser' | 'chime' | 'beep' | 'coin' | 'glitch' | 'typewriter';
241
+ /** The intent-level knobs shared by presets and custom sounds. */
242
+ interface SfxKnobs {
243
+ /** Semitones up (+) or down (−); 12 = one octave. Default 0, range ±24. */
244
+ pitch?: number;
245
+ /** −1 = dark / muffled … 0 = as designed … 1 = bright / crisp. Default 0. */
246
+ brightness?: number;
247
+ /** Another take of the random parts (noise grain, glitch pattern, typewriter key). Same seed ⇒ same sound. Default 0. */
248
+ seed?: number;
249
+ }
250
+ /**
251
+ * One layer of a custom sound. Most sounds are one or two voices: noise through a moving band-pass
252
+ * (whooshes, air, impacts) or a tone with a pitch sweep (blips, zaps, drops).
253
+ */
254
+ interface SfxVoice {
255
+ /** `'noise'` for air, hiss and impacts; a waveform for tones. */
256
+ wave: 'sine' | 'triangle' | 'square' | 'saw' | 'noise';
257
+ /** Tone frequency in Hz, or `[from, to]` to sweep (exponentially). Ignored for noise. Default 440. */
258
+ freq?: number | [number, number];
259
+ /** Keeps part of the spectrum; `freq` may sweep: `{ type: 'bandpass', freq: [500, 5000] }` = a band of noise rising. `q` default 0.7 (≥ 0.7). */
260
+ filter?: {
261
+ type: 'lowpass' | 'highpass' | 'bandpass';
262
+ freq: number | [number, number];
263
+ q?: number;
264
+ };
265
+ /** Level over the voice: `'fall'` (default) hit then decay · `'bell'` swell then fade · `'swell'` grow to the end · `'hold'` steady. */
266
+ envelope?: 'fall' | 'bell' | 'swell' | 'hold';
267
+ /** −1 left … 1 right, or `[from, to]` to move. Default 0. */
268
+ pan?: number | [number, number];
269
+ /** The part of the sound this voice plays, as fractions 0–1 of its length. Default 0 and 1. */
270
+ from?: number;
271
+ to?: number;
272
+ /** Level relative to the other voices. Default 1. */
273
+ gain?: number;
274
+ }
275
+ /** A preset with knobs, or a custom sound made of `voices` (length = the layer's duration, default 0.5 s). */
276
+ type SfxOptions = (SfxKnobs & {
277
+ preset: SfxPreset;
278
+ voices?: never;
279
+ }) | (SfxKnobs & {
280
+ voices: SfxVoice[];
281
+ preset?: never;
282
+ });
283
+ interface AudioBase extends SequenceCommon {
236
284
  type: 'audio';
285
+ /** Level. 1 = the file as recorded, or the sfx preset's standard level (peaks at −12 dBFS). Animatable. */
286
+ volume?: number;
287
+ }
288
+ /** Plays an audio file from `assets`. `duration` defaults to the file's length (with `loop`: to the composition's end). */
289
+ interface AudioAssetSpec extends AudioBase {
237
290
  asset: string;
291
+ sfx?: never;
292
+ /** Repeat the file for the whole `duration`. Without it the layer goes silent when the file ends. */
238
293
  loop?: boolean;
239
- volume?: number;
240
294
  }
295
+ /**
296
+ * Plays a synthesised sound effect. `duration` IS the sound's length: omitted, it is the preset's own
297
+ * length; given, the sound is stretched or shortened to it. `at` is where the sound starts.
298
+ */
299
+ interface AudioSfxSpec extends AudioBase {
300
+ sfx: SfxPreset | SfxOptions;
301
+ asset?: never;
302
+ loop?: never;
303
+ }
304
+ type AudioSequenceSpec = AudioAssetSpec | AudioSfxSpec;
241
305
  interface CompositionSequenceSpec extends SequenceCommon {
242
306
  type: 'composition';
243
307
  width?: number;
@@ -276,6 +340,18 @@ interface GradientSpec {
276
340
  /** radial: outer radius where the last stop applies, 0–1. Default 0.5. */
277
341
  radius?: number;
278
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
+ }
279
355
  interface ShapeBase extends SequenceCommon {
280
356
  type: 'shape';
281
357
  /** Fill with a gradient instead of `fillColor` (top level or in `initial`). */
@@ -293,8 +369,12 @@ interface ShapeBase extends SequenceCommon {
293
369
  * sweeps; `oklab` is straight-line in the chromaticity plane.
294
370
  */
295
371
  colorSpace?: 'rgb' | 'oklab' | 'oklch';
372
+ /** How an open stroke ends: `'butt'` (default), `'round'` (a friendly progress-ring end) or `'square'`. Top level or in `initial`. */
373
+ strokeCap?: 'butt' | 'round' | 'square';
374
+ /** How stroke corners join: `'miter'` (default; sharp points can poke out), `'round'` or `'bevel'`. Top level or in `initial`. */
375
+ strokeJoin?: 'miter' | 'round' | 'bevel';
296
376
  }
297
- interface RectShapeSpec extends ShapeBase {
377
+ interface RectShapeSpec extends ShapeBase, Trimmable {
298
378
  shape: 'rect';
299
379
  width: PropValue;
300
380
  height: PropValue;
@@ -309,14 +389,14 @@ interface RectShapeSpec extends ShapeBase {
309
389
  anchorX?: PropValue;
310
390
  anchorY?: PropValue;
311
391
  }
312
- interface CircleShapeSpec extends ShapeBase {
392
+ interface CircleShapeSpec extends ShapeBase, Trimmable {
313
393
  shape: 'circle';
314
394
  radius: PropValue;
315
395
  /** See RectShapeSpec.anchorX. Default 0.5 (centre). */
316
396
  anchorX?: PropValue;
317
397
  anchorY?: PropValue;
318
398
  }
319
- interface EllipseShapeSpec extends ShapeBase {
399
+ interface EllipseShapeSpec extends ShapeBase, Trimmable {
320
400
  shape: 'ellipse';
321
401
  radiusX: PropValue;
322
402
  radiusY: PropValue;
@@ -324,25 +404,41 @@ interface EllipseShapeSpec extends ShapeBase {
324
404
  anchorX?: PropValue;
325
405
  anchorY?: PropValue;
326
406
  }
327
- interface LineShapeSpec extends ShapeBase {
407
+ /**
408
+ * An arc around the local origin (its centre). Angles are in degrees: 0 = 3 o'clock, positive = clockwise,
409
+ * so a progress ring starts at `startAngle: -90` (12 o'clock) and animates `endAngle` from -90 to 270.
410
+ * With a stroke and no fill it is an open arc line (`strokeCap: 'round'` for rounded ends); with a fill it is a
411
+ * sector (a pie slice), and with `innerRadius` a ring segment (a donut slice). A sweep of 360° or more is a full circle.
412
+ */
413
+ interface ArcShapeSpec extends ShapeBase {
414
+ shape: 'arc';
415
+ radius: PropValue;
416
+ /** Hole radius of a filled ring segment. Default 0 (a pie slice to the centre). Animatable. */
417
+ innerRadius?: PropValue;
418
+ /** Degrees. Default 0. Animatable. */
419
+ startAngle?: PropValue;
420
+ /** Degrees. Default 360. Animatable. */
421
+ endAngle?: PropValue;
422
+ }
423
+ interface LineShapeSpec extends ShapeBase, Trimmable {
328
424
  shape: 'line';
329
425
  /** Line endpoints relative to the shape's local origin. */
330
426
  from: [PropValue, PropValue];
331
427
  to: [PropValue, PropValue];
332
428
  }
333
- interface PolygonShapeSpec extends ShapeBase {
429
+ interface PolygonShapeSpec extends ShapeBase, Trimmable {
334
430
  shape: 'polygon';
335
431
  /** Vertices in local space. The path is auto-closed. */
336
432
  points: Array<[PropValue, PropValue]>;
337
433
  /** When true, draw an open polyline instead of a closed polygon. Default false. */
338
434
  open?: boolean;
339
435
  }
340
- interface PathShapeSpec extends ShapeBase {
436
+ interface PathShapeSpec extends ShapeBase, Trimmable {
341
437
  shape: 'path';
342
438
  /** SVG path data (`d` attribute). Goes through PIXI's GraphicsContext.svg(). */
343
439
  d: string;
344
440
  }
345
- type ShapeSequenceSpec = RectShapeSpec | CircleShapeSpec | EllipseShapeSpec | LineShapeSpec | PolygonShapeSpec | PathShapeSpec;
441
+ type ShapeSequenceSpec = RectShapeSpec | CircleShapeSpec | EllipseShapeSpec | ArcShapeSpec | LineShapeSpec | PolygonShapeSpec | PathShapeSpec;
346
442
  type SequenceSpec = VideoSequenceSpec | ImageSequenceSpec | TextSequenceSpec | AudioSequenceSpec | CompositionSequenceSpec | CameraSequenceSpec | ShapeSequenceSpec;
347
443
  /** Top-level composition (root node) — same as `CompositionSequenceSpec` minus the discriminant. */
348
444
  interface CompositionSpec extends SequenceCommon {
@@ -359,7 +455,17 @@ interface CompositionShape {
359
455
  }
360
456
  /** Audio descriptor pushed to the mixdown queue by AudioSequence/VideoSequence. */
361
457
  interface AudioDescriptor {
362
- buffer: AudioBuffer;
458
+ /** Decoded audio (a file or a video's soundtrack). Exactly one of `buffer` / `synth` is set. */
459
+ buffer?: AudioBuffer;
460
+ /** Stereo samples (left, right) made on demand at the mix's sample rate (a synthesised sfx). Equal keys render equal samples. */
461
+ synth?: {
462
+ key: string;
463
+ render: (sampleRate: number) => [Float32Array, Float32Array];
464
+ };
465
+ /** Who made it, for warnings and inspectAudio: `layer "pop-1"`. */
466
+ layer?: string;
467
+ /** What it plays: `sfx "pop"`, `asset "bgm"`, `video "green"`. */
468
+ source?: string;
363
469
  loop: boolean;
364
470
  start: number;
365
471
  end: number;
@@ -370,4 +476,4 @@ interface AudioDescriptor {
370
476
  }[];
371
477
  }
372
478
 
373
- export type { AssetSpec as A, CompositionShape as C, DipTransition as D, EllipseShapeSpec as E, FilterSpec as F, GradientSpec as G, ImageSequenceSpec as I, Keyframe as K, LineShapeSpec as L, PathShapeSpec as P, RectShapeSpec as R, SequenceSpec as S, TextSequenceSpec as T, VideoSequenceSpec as V, WipeTransition as W, ZoomTransition as Z, CameraSequenceSpec as a, SequenceCommon as b, AudioSequenceSpec as c, ChromaKeyFilterSpec as d, CircleShapeSpec as e, CompositionSequenceSpec as f, CompositionSpec as g, CrossfadeTransition as h, CustomFilterSpec as i, DissolveTransition as j, Expr as k, IrisTransition as l, PolygonShapeSpec as m, PropValue as n, Props as o, ShapeSequenceSpec as p, SlideTransition as q, TransitionCommon as r, TransitionSpec as s, AudioDescriptor as t };
479
+ export type { ArcShapeSpec as A, AudioDescriptor as B, CompositionShape as C, DipTransition as D, EllipseShapeSpec as E, FilterSpec as F, GradientSpec as G, ImageSequenceSpec as I, Keyframe as K, LineShapeSpec as L, PathShapeSpec as P, RectShapeSpec as R, SequenceSpec as S, TextSequenceSpec as T, VideoSequenceSpec as V, WipeTransition as W, ZoomTransition as Z, CameraSequenceSpec as a, SequenceCommon as b, AssetSpec as c, AudioAssetSpec as d, AudioSequenceSpec as e, AudioSfxSpec as f, ChromaKeyFilterSpec as g, CircleShapeSpec as h, CompositionSequenceSpec as i, CompositionSpec as j, CrossfadeTransition as k, CustomFilterSpec as l, DissolveTransition as m, Expr as n, IrisTransition as o, PolygonShapeSpec as p, PropValue as q, Props as r, SfxKnobs as s, SfxOptions as t, SfxPreset as u, SfxVoice as v, ShapeSequenceSpec as w, SlideTransition as x, TransitionCommon as y, TransitionSpec as z };
@@ -212,12 +212,17 @@ 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). */
218
222
  format?: {
219
223
  decimals?: number;
220
- grouping?: boolean;
224
+ grouping?: boolean; /** Zero-pad the whole part to this many digits (`pad: 2` → `05`; a clock `18:42:05`). */
225
+ pad?: number;
221
226
  };
222
227
  /** Subset of PIXI v8 TextStyleOptions. String values may be exprs (e.g. fontSize: 'GW * 0.05'). */
223
228
  style?: Record<string, PropValue | {
@@ -232,12 +237,71 @@ interface TextSequenceSpec extends SequenceCommon {
232
237
  */
233
238
  colorSpace?: 'rgb' | 'oklab' | 'oklch';
234
239
  }
235
- interface AudioSequenceSpec extends SequenceCommon {
240
+ type SfxPreset = 'click' | 'pop' | 'swoosh' | 'swipe' | 'hit' | 'riser' | 'chime' | 'beep' | 'coin' | 'glitch' | 'typewriter';
241
+ /** The intent-level knobs shared by presets and custom sounds. */
242
+ interface SfxKnobs {
243
+ /** Semitones up (+) or down (−); 12 = one octave. Default 0, range ±24. */
244
+ pitch?: number;
245
+ /** −1 = dark / muffled … 0 = as designed … 1 = bright / crisp. Default 0. */
246
+ brightness?: number;
247
+ /** Another take of the random parts (noise grain, glitch pattern, typewriter key). Same seed ⇒ same sound. Default 0. */
248
+ seed?: number;
249
+ }
250
+ /**
251
+ * One layer of a custom sound. Most sounds are one or two voices: noise through a moving band-pass
252
+ * (whooshes, air, impacts) or a tone with a pitch sweep (blips, zaps, drops).
253
+ */
254
+ interface SfxVoice {
255
+ /** `'noise'` for air, hiss and impacts; a waveform for tones. */
256
+ wave: 'sine' | 'triangle' | 'square' | 'saw' | 'noise';
257
+ /** Tone frequency in Hz, or `[from, to]` to sweep (exponentially). Ignored for noise. Default 440. */
258
+ freq?: number | [number, number];
259
+ /** Keeps part of the spectrum; `freq` may sweep: `{ type: 'bandpass', freq: [500, 5000] }` = a band of noise rising. `q` default 0.7 (≥ 0.7). */
260
+ filter?: {
261
+ type: 'lowpass' | 'highpass' | 'bandpass';
262
+ freq: number | [number, number];
263
+ q?: number;
264
+ };
265
+ /** Level over the voice: `'fall'` (default) hit then decay · `'bell'` swell then fade · `'swell'` grow to the end · `'hold'` steady. */
266
+ envelope?: 'fall' | 'bell' | 'swell' | 'hold';
267
+ /** −1 left … 1 right, or `[from, to]` to move. Default 0. */
268
+ pan?: number | [number, number];
269
+ /** The part of the sound this voice plays, as fractions 0–1 of its length. Default 0 and 1. */
270
+ from?: number;
271
+ to?: number;
272
+ /** Level relative to the other voices. Default 1. */
273
+ gain?: number;
274
+ }
275
+ /** A preset with knobs, or a custom sound made of `voices` (length = the layer's duration, default 0.5 s). */
276
+ type SfxOptions = (SfxKnobs & {
277
+ preset: SfxPreset;
278
+ voices?: never;
279
+ }) | (SfxKnobs & {
280
+ voices: SfxVoice[];
281
+ preset?: never;
282
+ });
283
+ interface AudioBase extends SequenceCommon {
236
284
  type: 'audio';
285
+ /** Level. 1 = the file as recorded, or the sfx preset's standard level (peaks at −12 dBFS). Animatable. */
286
+ volume?: number;
287
+ }
288
+ /** Plays an audio file from `assets`. `duration` defaults to the file's length (with `loop`: to the composition's end). */
289
+ interface AudioAssetSpec extends AudioBase {
237
290
  asset: string;
291
+ sfx?: never;
292
+ /** Repeat the file for the whole `duration`. Without it the layer goes silent when the file ends. */
238
293
  loop?: boolean;
239
- volume?: number;
240
294
  }
295
+ /**
296
+ * Plays a synthesised sound effect. `duration` IS the sound's length: omitted, it is the preset's own
297
+ * length; given, the sound is stretched or shortened to it. `at` is where the sound starts.
298
+ */
299
+ interface AudioSfxSpec extends AudioBase {
300
+ sfx: SfxPreset | SfxOptions;
301
+ asset?: never;
302
+ loop?: never;
303
+ }
304
+ type AudioSequenceSpec = AudioAssetSpec | AudioSfxSpec;
241
305
  interface CompositionSequenceSpec extends SequenceCommon {
242
306
  type: 'composition';
243
307
  width?: number;
@@ -276,6 +340,18 @@ interface GradientSpec {
276
340
  /** radial: outer radius where the last stop applies, 0–1. Default 0.5. */
277
341
  radius?: number;
278
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
+ }
279
355
  interface ShapeBase extends SequenceCommon {
280
356
  type: 'shape';
281
357
  /** Fill with a gradient instead of `fillColor` (top level or in `initial`). */
@@ -293,8 +369,12 @@ interface ShapeBase extends SequenceCommon {
293
369
  * sweeps; `oklab` is straight-line in the chromaticity plane.
294
370
  */
295
371
  colorSpace?: 'rgb' | 'oklab' | 'oklch';
372
+ /** How an open stroke ends: `'butt'` (default), `'round'` (a friendly progress-ring end) or `'square'`. Top level or in `initial`. */
373
+ strokeCap?: 'butt' | 'round' | 'square';
374
+ /** How stroke corners join: `'miter'` (default; sharp points can poke out), `'round'` or `'bevel'`. Top level or in `initial`. */
375
+ strokeJoin?: 'miter' | 'round' | 'bevel';
296
376
  }
297
- interface RectShapeSpec extends ShapeBase {
377
+ interface RectShapeSpec extends ShapeBase, Trimmable {
298
378
  shape: 'rect';
299
379
  width: PropValue;
300
380
  height: PropValue;
@@ -309,14 +389,14 @@ interface RectShapeSpec extends ShapeBase {
309
389
  anchorX?: PropValue;
310
390
  anchorY?: PropValue;
311
391
  }
312
- interface CircleShapeSpec extends ShapeBase {
392
+ interface CircleShapeSpec extends ShapeBase, Trimmable {
313
393
  shape: 'circle';
314
394
  radius: PropValue;
315
395
  /** See RectShapeSpec.anchorX. Default 0.5 (centre). */
316
396
  anchorX?: PropValue;
317
397
  anchorY?: PropValue;
318
398
  }
319
- interface EllipseShapeSpec extends ShapeBase {
399
+ interface EllipseShapeSpec extends ShapeBase, Trimmable {
320
400
  shape: 'ellipse';
321
401
  radiusX: PropValue;
322
402
  radiusY: PropValue;
@@ -324,25 +404,41 @@ interface EllipseShapeSpec extends ShapeBase {
324
404
  anchorX?: PropValue;
325
405
  anchorY?: PropValue;
326
406
  }
327
- interface LineShapeSpec extends ShapeBase {
407
+ /**
408
+ * An arc around the local origin (its centre). Angles are in degrees: 0 = 3 o'clock, positive = clockwise,
409
+ * so a progress ring starts at `startAngle: -90` (12 o'clock) and animates `endAngle` from -90 to 270.
410
+ * With a stroke and no fill it is an open arc line (`strokeCap: 'round'` for rounded ends); with a fill it is a
411
+ * sector (a pie slice), and with `innerRadius` a ring segment (a donut slice). A sweep of 360° or more is a full circle.
412
+ */
413
+ interface ArcShapeSpec extends ShapeBase {
414
+ shape: 'arc';
415
+ radius: PropValue;
416
+ /** Hole radius of a filled ring segment. Default 0 (a pie slice to the centre). Animatable. */
417
+ innerRadius?: PropValue;
418
+ /** Degrees. Default 0. Animatable. */
419
+ startAngle?: PropValue;
420
+ /** Degrees. Default 360. Animatable. */
421
+ endAngle?: PropValue;
422
+ }
423
+ interface LineShapeSpec extends ShapeBase, Trimmable {
328
424
  shape: 'line';
329
425
  /** Line endpoints relative to the shape's local origin. */
330
426
  from: [PropValue, PropValue];
331
427
  to: [PropValue, PropValue];
332
428
  }
333
- interface PolygonShapeSpec extends ShapeBase {
429
+ interface PolygonShapeSpec extends ShapeBase, Trimmable {
334
430
  shape: 'polygon';
335
431
  /** Vertices in local space. The path is auto-closed. */
336
432
  points: Array<[PropValue, PropValue]>;
337
433
  /** When true, draw an open polyline instead of a closed polygon. Default false. */
338
434
  open?: boolean;
339
435
  }
340
- interface PathShapeSpec extends ShapeBase {
436
+ interface PathShapeSpec extends ShapeBase, Trimmable {
341
437
  shape: 'path';
342
438
  /** SVG path data (`d` attribute). Goes through PIXI's GraphicsContext.svg(). */
343
439
  d: string;
344
440
  }
345
- type ShapeSequenceSpec = RectShapeSpec | CircleShapeSpec | EllipseShapeSpec | LineShapeSpec | PolygonShapeSpec | PathShapeSpec;
441
+ type ShapeSequenceSpec = RectShapeSpec | CircleShapeSpec | EllipseShapeSpec | ArcShapeSpec | LineShapeSpec | PolygonShapeSpec | PathShapeSpec;
346
442
  type SequenceSpec = VideoSequenceSpec | ImageSequenceSpec | TextSequenceSpec | AudioSequenceSpec | CompositionSequenceSpec | CameraSequenceSpec | ShapeSequenceSpec;
347
443
  /** Top-level composition (root node) — same as `CompositionSequenceSpec` minus the discriminant. */
348
444
  interface CompositionSpec extends SequenceCommon {
@@ -359,7 +455,17 @@ interface CompositionShape {
359
455
  }
360
456
  /** Audio descriptor pushed to the mixdown queue by AudioSequence/VideoSequence. */
361
457
  interface AudioDescriptor {
362
- buffer: AudioBuffer;
458
+ /** Decoded audio (a file or a video's soundtrack). Exactly one of `buffer` / `synth` is set. */
459
+ buffer?: AudioBuffer;
460
+ /** Stereo samples (left, right) made on demand at the mix's sample rate (a synthesised sfx). Equal keys render equal samples. */
461
+ synth?: {
462
+ key: string;
463
+ render: (sampleRate: number) => [Float32Array, Float32Array];
464
+ };
465
+ /** Who made it, for warnings and inspectAudio: `layer "pop-1"`. */
466
+ layer?: string;
467
+ /** What it plays: `sfx "pop"`, `asset "bgm"`, `video "green"`. */
468
+ source?: string;
363
469
  loop: boolean;
364
470
  start: number;
365
471
  end: number;
@@ -370,4 +476,4 @@ interface AudioDescriptor {
370
476
  }[];
371
477
  }
372
478
 
373
- export type { AssetSpec as A, CompositionShape as C, DipTransition as D, EllipseShapeSpec as E, FilterSpec as F, GradientSpec as G, ImageSequenceSpec as I, Keyframe as K, LineShapeSpec as L, PathShapeSpec as P, RectShapeSpec as R, SequenceSpec as S, TextSequenceSpec as T, VideoSequenceSpec as V, WipeTransition as W, ZoomTransition as Z, CameraSequenceSpec as a, SequenceCommon as b, AudioSequenceSpec as c, ChromaKeyFilterSpec as d, CircleShapeSpec as e, CompositionSequenceSpec as f, CompositionSpec as g, CrossfadeTransition as h, CustomFilterSpec as i, DissolveTransition as j, Expr as k, IrisTransition as l, PolygonShapeSpec as m, PropValue as n, Props as o, ShapeSequenceSpec as p, SlideTransition as q, TransitionCommon as r, TransitionSpec as s, AudioDescriptor as t };
479
+ export type { ArcShapeSpec as A, AudioDescriptor as B, CompositionShape as C, DipTransition as D, EllipseShapeSpec as E, FilterSpec as F, GradientSpec as G, ImageSequenceSpec as I, Keyframe as K, LineShapeSpec as L, PathShapeSpec as P, RectShapeSpec as R, SequenceSpec as S, TextSequenceSpec as T, VideoSequenceSpec as V, WipeTransition as W, ZoomTransition as Z, CameraSequenceSpec as a, SequenceCommon as b, AssetSpec as c, AudioAssetSpec as d, AudioSequenceSpec as e, AudioSfxSpec as f, ChromaKeyFilterSpec as g, CircleShapeSpec as h, CompositionSequenceSpec as i, CompositionSpec as j, CrossfadeTransition as k, CustomFilterSpec as l, DissolveTransition as m, Expr as n, IrisTransition as o, PolygonShapeSpec as p, PropValue as q, Props as r, SfxKnobs as s, SfxOptions as t, SfxPreset as u, SfxVoice as v, ShapeSequenceSpec as w, SlideTransition as x, TransitionCommon as y, TransitionSpec as z };
package/docs/api.md CHANGED
@@ -89,6 +89,21 @@ contactSheet(options?: {
89
89
 
90
90
  Many frames on **one** PNG, each labelled `frame N · T s`. The cheapest way to check a whole animation by eye (include the middle of every transition and the last second). Restores the current frame afterwards.
91
91
 
92
+ ### `measureText(text, style?): { width, height, lines }` and `splitText(text, style?, options?): TextPiece[]`
93
+
94
+ Text layout helpers, exported from `pixi-effects`. They measure with the renderer's own font metrics (the same numbers a text layer with that `style` has), so a layout built from them matches what is drawn. `style` is a text layer's `style` with numbers (no expressions). A web font must be loaded first.
95
+
96
+ ```ts
97
+ measureText('Hello', { fontSize: 72 }); // { width: 187.4, height: 81.3, lines: 1 }
98
+
99
+ interface SplitOptions { by?: 'chars' | 'words' | 'lines'; x?: number; y?: number; align?: 'left' | 'center' | 'right' }
100
+ interface TextPiece { text: string; x: number; y: number; width: number; index: number; line: number }
101
+ splitText('Hello world', style, { by: 'words', x: 640, y: 300, align: 'center' });
102
+ // → [{ text: 'Hello', x, y, width, index: 0, line: 0 }, { text: 'world', ... }]
103
+ ```
104
+
105
+ `x` is the line's left edge, or its centre / right edge with `align` (each line of a `\n` text is aligned on its own width); `y` is the top of the first line, and further lines go down by the line height. Whitespace takes its room but is not returned as a piece. Use one text layer per piece with the same `style` and the default top-left anchor: `{ type: 'text', text: p.text, style, initial: { x: p.x, y: p.y } }`. Kerning is kept (a piece's x is the measured width of everything before it).
106
+
92
107
  ### `movie.inspect(frame?, options?): Promise<InspectReport>`
93
108
 
94
109
  Where every layer is drawn at `frame`, as JSON — for checking layout without eyes. Seeks there and stays there. `options.layers`: `'visible'` (default, only layers drawn at this frame), `'all'`, or `'none'` (just `summary` and `issues`). Faint layers (alpha < 0.3) and text whose x / y is animated (tickers) are not reported.
@@ -109,6 +124,27 @@ interface InspectReport {
109
124
  }
110
125
  ```
111
126
 
127
+ ### `movie.inspectAudio(options?): AudioReport`
128
+
129
+ The soundtrack as numbers — for checking sound without listening. Every sound is also measured on its own, so overlaps do not blur it. `options.window`: seconds per row of `windows` (default 0.1).
130
+
131
+ ```ts
132
+ interface AudioReport {
133
+ duration: number; sampleRate: number;
134
+ peakDb: number; peakAt: number; // the mix before limiting; > 0 dBFS means it was limited
135
+ sources: Array<{
136
+ layer: string; source: string; // 'layer "hit"', 'sfx "hit"' | 'asset "bgm"' | 'video "clip"'
137
+ start: number; end: number; // movie time
138
+ peakDb: number; // the MIX while it plays
139
+ sound: { length: number; peakDb: number; loudestAt: number; brightnessHz: number }; // the sound alone
140
+ }>;
141
+ windows: Array<{ t: number; rmsDb: number; peakDb: number; brightnessHz: number }>;
142
+ issues: string[]; // limited mix, inaudible layer, sfx cut off by the end, no audio — read first
143
+ }
144
+ ```
145
+
146
+ `render()` encodes this same mix: AAC in mp4 / mov (the encoder delay is trimmed with an edit list), Opus in webm / mkv.
147
+
112
148
  ### `movie.render(options?): Promise<Blob>`
113
149
 
114
150
  Renders the entire timeline to a single video file. Pauses playback first.
@@ -146,6 +182,7 @@ movie.off(event, fn): this
146
182
  | ----------- | ------------------------------------------------- | ----------------------------------------------------- |
147
183
  | `'ready'` | none | once, after `init()` resolves |
148
184
  | `'frame'` | `{ frame: number; totalFrames: number }` | every `gotoFrame` (so once per playback frame too) |
185
+ | `'play'` | none | when `play()` actually transitions from paused |
149
186
  | `'pause'` | none | when `pause()` actually transitions from playing |
150
187
  | `'progress'`| `{ progress: number; frame: number; totalFrames: number }` | during `render()`, once per encoded frame |
151
188
 
package/docs/dsl.md CHANGED
@@ -59,7 +59,7 @@ interface SequenceCommon {
59
59
  | Layer | Where `x, y` land by default | To change it |
60
60
  |---|---|---|
61
61
  | `shape` rect / circle / ellipse | the **centre** of the shape | `anchorX` / `anchorY` (0–1). A bar growing from its base: `anchorY: 1`; left to right: `anchorX: 0` |
62
- | `shape` line / polygon / path | the centre of the shape's bounds | — |
62
+ | `shape` line / polygon / path | the centre of the shape's bounds | `pivotX` / `pivotY` (in the shape's own canvas coordinates): the point that sits at `x, y` and that rotation / scale turn about. **`anchorX` / `anchorY` do nothing on these.** |
63
63
  | `text`, `image`, `video` | the **top-left** corner | `anchorX` / `anchorY: 0.5` to centre |
64
64
  | `composition` | its top-left corner | `pivotX` / `pivotY` = the point that sits at `x, y` and that rotation / scale turn about |
65
65
 
@@ -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`):
@@ -116,7 +127,7 @@ A text layer has one animatable number, `value`, printed wherever the text conta
116
127
  }
117
128
  ```
118
129
 
119
- `format`: `decimals` (default 0) and thousands `grouping` (default false). Put prefixes and suffixes in the text (`'${value}'`, `'{value}%'`). If `value` is animated but the text has no `{value}`, a warning says so.
130
+ `format`: `decimals` (default 0), thousands `grouping` (default false) and `pad` (zero-pad the whole part to that many digits: `pad: 2` gives `05`, so a clock is `text: '18:42:{value}'` with `format: { pad: 2 }`). Put prefixes and suffixes in the text (`'${value}'`, `'{value}%'`). If `value` is animated but the text has no `{value}`, a warning says so.
120
131
 
121
132
  ### `image`
122
133
 
@@ -179,10 +190,40 @@ Audio-only sequence. No visual. Volume is animatable via keyframes.
179
190
 
180
191
  Audio is mixed during `Movie.init()` and during `Movie.render()`. Volume keyframes interpolate linearly.
181
192
 
193
+ #### Sound effects without files: `sfx`
194
+
195
+ ```ts
196
+ { type: 'audio', sfx: 'swoosh', at: 2 } // a preset at its own length
197
+ { type: 'audio', sfx: { preset: 'hit', pitch: -3, brightness: -0.5 }, at: 4 } // lower and darker
198
+ { type: 'audio', sfx: 'riser', at: 3, duration: 1.5, volume: 0.7 } // duration IS the sound's length (stretched)
199
+ { type: 'audio', at: 6, duration: 0.3, // a custom sound
200
+ sfx: { voices: [{ wave: 'square', freq: [1600, 200], filter: { type: 'lowpass', freq: 3000 }, envelope: 'fall' }] } }
201
+ ```
202
+
203
+ The sound is synthesised from data at init (no asset, no network), deterministic, and the same mix in playback and in the exported file (encoded there: AAC / Opus are lossy). `at` is where it starts; `volume` and volume keyframes work as for files; `loop` is not available (one layer per hit). Knobs: `pitch` (semitones, ±24), `brightness` (−1 dark … 1 bright), `seed` (another take of the random parts). `volume: 1` is the preset's standard level, peaking at −12 … −18 dBFS depending on the preset; the finished mix is soft-limited above 0.9 with a warning naming the layers. A custom sound has up to 16 `voices`: `wave` (`sine` `triangle` `square` `saw` `noise`), `freq` (Hz or `[from, to]`), `filter` (`{ type: 'lowpass' | 'highpass' | 'bandpass', freq, q }`), `envelope` (`fall` `bell` `swell` `hold`), `pan`, `from` / `to` (part of the sound, 0–1), `gain`; its default length is 0.5 s.
204
+
205
+ | Preset | Default length | Loudest at | Brightness | Use for |
206
+ |---|---|---|---|---|
207
+ | `click` | 0.04 s | start | ≈ 12780 Hz | UI click, tick |
208
+ | `pop` | 0.12 s | start | ≈ 390 Hz | an element appearing |
209
+ | `swoosh` | 0.5 s | 0.16 s | ≈ 3070 Hz | transitions, fly-pasts (moves left → right) |
210
+ | `swipe` | 0.22 s | start | ≈ 13190 Hz | quick UI swipe |
211
+ | `hit` | 0.7 s | start | ≈ 150 Hz | impact, slam |
212
+ | `riser` | 1 s | its end | ≈ 11850 Hz | build-up into a hit |
213
+ | `chime` | 1.4 s | start | ≈ 1400 Hz | success, reveal |
214
+ | `beep` | 0.16 s | flat | ≈ 1030 Hz | countdown, alert |
215
+ | `coin` | 0.4 s | 0.07 s | ≈ 1870 Hz | reward, collect |
216
+ | `glitch` | 0.35 s | 0.22 s | ≈ 3970 Hz | digital error |
217
+ | `typewriter` | 0.06 s | start | ≈ 3260 Hz | one key (vary `seed` per key) |
218
+
219
+ Brightness is the spectral centroid that `movie.inspectAudio()` reports (`sound.brightnessHz`), at 48 kHz with no knobs.
220
+
182
221
  ### `composition`
183
222
 
184
223
  Nested composition. Same shape as the root spec but with `type: 'composition'` and an explicit `width`/`height`. Children animate within the local coordinate system; the composition itself can be positioned, scaled, and rotated as a unit.
185
224
 
225
+ **Size, clipping and culling** (measured): a 2D composition does **not** clip its children — a child far outside `width × height` is still drawn. But the rectangle `width × height` is what the renderer uses to decide the group is on screen: when that whole rectangle is off the canvas the group is skipped even if a child is placed back on screen. So keep the rectangle around what it draws (or make it canvas-sized). A `threeD: true` composition (a card) is rendered into a texture of exactly `width × height`, so it **does** clip its children there; size it for soft shadows too.
226
+
186
227
  ```ts
187
228
  {
188
229
  type: 'composition',
@@ -220,12 +261,31 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
220
261
 
221
262
  | `shape` | Required props | Optional |
222
263
  |-----------|------------------------------------------|------------------------|
223
- | `rect` | `width`, `height` | `cornerRadius` |
224
- | `circle` | `radius` | — |
225
- | `ellipse` | `radiusX`, `radiusY` | — |
226
- | `line` | `from: [x,y]`, `to: [x,y]` (canvas coordinates; stroke in `initial`) | — |
227
- | `polygon` | `points: [[x,y], …]` | `open` (default false) |
228
- | `path` | `d` (SVG path data) | — |
264
+ | `rect` | `width`, `height` | `cornerRadius`, `trimStart`, `trimEnd` |
265
+ | `circle` | `radius` | `trimStart`, `trimEnd` |
266
+ | `ellipse` | `radiusX`, `radiusY` | `trimStart`, `trimEnd` |
267
+ | `arc` | `radius` | `innerRadius`, `startAngle` (0), `endAngle` (360) |
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` |
271
+
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.
273
+
274
+ ```ts
275
+ { type: 'shape', shape: 'arc', radius: 120, startAngle: -90, endAngle: -90, strokeCap: 'round',
276
+ initial: { x: 'GW/2', y: 'GH/2', strokeColor: '#ffd166', strokeWidth: 24 },
277
+ keyframes: [{ at: 0, to: { endAngle: 270 }, duration: 2, ease: 'power3.out' }] } // a progress ring filling clockwise from the top
278
+ ```
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
+
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'`.
229
289
 
230
290
  **Style** is set on `initial` and animatable via keyframes:
231
291
 
@@ -309,7 +369,7 @@ Per-shape choice of how colour keyframes are interpolated. Default `'rgb'` (line
309
369
 
310
370
  **Scalar geometry is animatable.** `width`, `height`, `cornerRadius` (rect), `radius` (circle), `radiusX` / `radiusY` (ellipse) all flow through the same per-frame redraw and can be tweened via keyframes — useful for progress bars (animate `width`), pulsing icons (animate `radius`), or shape-morph callouts. Array geometry (polygon `points`, line endpoints, path `d`) is baked at build time; use `scale` / `scaleX` / `scaleY` for those.
311
371
 
312
- `anchorX` / `anchorY` (default `0.5` each) control which point on the bbox sits at the local origin — `0` is left/top, `1` is right/bottom. They're animatable too. Critical for "grow from one edge" effects:
372
+ For rect / circle / ellipse, `anchorX` / `anchorY` (default `0.5` each; not available on line / polygon / path, which use `pivotX` / `pivotY`) control which point on the bbox sits at the local origin — `0` is left/top, `1` is right/bottom. They're animatable too. Critical for "grow from one edge" effects:
313
373
 
314
374
  ```ts
315
375
  // Left-anchored progress bar — width animates 0 → W, left edge stays at x