pixi-effects 0.27.2 → 0.28.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 (66) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +6 -6
  3. package/ai/chat-template.html +3 -3
  4. package/ai/tools/check.mjs +30 -0
  5. package/dist/{Base-CKGl6jCo.d.ts → Base-C6f0fE8j.d.ts} +1 -1
  6. package/dist/{Base-DjhdL8B1.d.cts → Base-DI1FCGcH.d.cts} +1 -1
  7. package/dist/Composition-HFEVIYHN.js +7 -0
  8. package/dist/{Composition-TZHVYIGH.js.map → Composition-HFEVIYHN.js.map} +1 -1
  9. package/dist/Composition-NI74UJT3.cjs +16 -0
  10. package/dist/{Composition-DRCTWIYH.cjs.map → Composition-NI74UJT3.cjs.map} +1 -1
  11. package/dist/Controller.d.cts +2 -2
  12. package/dist/Controller.d.ts +2 -2
  13. package/dist/{Movie-CbYwTs6U.d.cts → Movie-CHcM8HNB.d.ts} +3 -1
  14. package/dist/{Movie-ILKq2fw_.d.ts → Movie-Cx5ozUGb.d.cts} +3 -1
  15. package/dist/Presenter.d.cts +2 -2
  16. package/dist/Presenter.d.ts +2 -2
  17. package/dist/chunk-4LEX3G3M.cjs +259 -0
  18. package/dist/chunk-4LEX3G3M.cjs.map +1 -0
  19. package/dist/{chunk-BQNNZEAX.js → chunk-AK54FIWM.js} +7 -4
  20. package/dist/chunk-AK54FIWM.js.map +1 -0
  21. package/dist/{chunk-YOV6MEXH.cjs → chunk-MZCDBRTY.cjs} +7 -4
  22. package/dist/chunk-MZCDBRTY.cjs.map +1 -0
  23. package/dist/{chunk-C7HJJ7MZ.cjs → chunk-SBOVUZZY.cjs} +61 -61
  24. package/dist/{chunk-C7HJJ7MZ.cjs.map → chunk-SBOVUZZY.cjs.map} +1 -1
  25. package/dist/{chunk-K3F2YKL4.js → chunk-WEJAQAFX.js} +3 -3
  26. package/dist/{chunk-K3F2YKL4.js.map → chunk-WEJAQAFX.js.map} +1 -1
  27. package/dist/chunk-ZHUUIF6A.js +250 -0
  28. package/dist/chunk-ZHUUIF6A.js.map +1 -0
  29. package/dist/events-EE3SM5LM.cjs +16 -0
  30. package/dist/events-EE3SM5LM.cjs.map +1 -0
  31. package/dist/events-KXBWR4IV.js +3 -0
  32. package/dist/events-KXBWR4IV.js.map +1 -0
  33. package/dist/index.cjs +49 -33
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +19 -5
  36. package/dist/index.d.ts +19 -5
  37. package/dist/index.js +18 -7
  38. package/dist/index.js.map +1 -1
  39. package/dist/loader.css +1 -1
  40. package/dist/{music-6MTHQEXS.cjs → music-6WFTPVHU.cjs} +43 -202
  41. package/dist/music-6WFTPVHU.cjs.map +1 -0
  42. package/dist/{music-72YJAMXH.js → music-JKOM5YK6.js} +8 -192
  43. package/dist/music-JKOM5YK6.js.map +1 -0
  44. package/dist/three.cjs +5 -5
  45. package/dist/three.d.cts +2 -2
  46. package/dist/three.d.ts +2 -2
  47. package/dist/three.js +1 -1
  48. package/dist/{types-BNVsBBZf.d.ts → types-DpK1vwcl.d.cts} +79 -1
  49. package/dist/{types-BNVsBBZf.d.cts → types-DpK1vwcl.d.ts} +79 -1
  50. package/docs/api.md +28 -0
  51. package/docs/dsl.md +8 -4
  52. package/llms-full.txt +216 -6
  53. package/llms.txt +1 -0
  54. package/package.json +1 -1
  55. package/skills/pixi-effects/SKILL.md +11 -1
  56. package/skills/pixi-effects/reference/cheatsheet.md +1 -1
  57. package/skills/pixi-effects/reference/direction.md +136 -0
  58. package/skills/pixi-effects/reference/pitfalls.md +3 -0
  59. package/skills/pixi-effects/reference/recipes.md +25 -0
  60. package/skills/pixi-effects/template.html +6 -6
  61. package/dist/Composition-DRCTWIYH.cjs +0 -16
  62. package/dist/Composition-TZHVYIGH.js +0 -7
  63. package/dist/chunk-BQNNZEAX.js.map +0 -1
  64. package/dist/chunk-YOV6MEXH.cjs.map +0 -1
  65. package/dist/music-6MTHQEXS.cjs.map +0 -1
  66. package/dist/music-72YJAMXH.js.map +0 -1
@@ -172,6 +172,80 @@ declare function pictureStops(stops: readonly Stop[], opts?: {
172
172
  pdf?: boolean;
173
173
  }): Stop[];
174
174
 
175
+ /** One note or chord of a track, as a score reader sees it. */
176
+ interface MusicNote {
177
+ /** Where it starts: beats from the start, and the same moment in seconds (the tempo map applied; swing and humanize are not). */
178
+ beat: number;
179
+ time: number;
180
+ /** How long it is held, in beats and in seconds. */
181
+ duration: number;
182
+ seconds: number;
183
+ /** The bar it starts in (1-based, from `meter`) and its beat inside that bar (1 = the downbeat, 1.5 = the "and" of 1). */
184
+ bar: number;
185
+ beatInBar: number;
186
+ /** The audible MIDI notes (`music.transpose` and the track's `transpose` are in). */
187
+ midi: number[];
188
+ /** What the score called it: `Am7`, `c4`, `[c4 e4 g4]`. */
189
+ name: string;
190
+ /** More than one note at once. */
191
+ chord: boolean;
192
+ /** 0–1 (0.8 normal, 1 accented with `!`, 0.55 soft with `,`). */
193
+ velocity: number;
194
+ }
195
+ interface MusicTrackEvents {
196
+ inst: string;
197
+ /** The track's length in beats and in bars (not a whole number when a bar is short: that is what to look for). */
198
+ beats: number;
199
+ bars: number;
200
+ notes: MusicNote[];
201
+ }
202
+ interface MusicDrumHit {
203
+ beat: number;
204
+ time: number;
205
+ bar: number;
206
+ beatInBar: number;
207
+ velocity: number;
208
+ }
209
+ interface MusicDrumEvents {
210
+ kind: string;
211
+ hits: MusicDrumHit[];
212
+ }
213
+ interface MusicEvents {
214
+ /** The tempo at the start. */
215
+ bpm: number;
216
+ meter: number;
217
+ beats: number;
218
+ bars: number;
219
+ /** The music without its reverb tail. */
220
+ seconds: number;
221
+ tracks: MusicTrackEvents[];
222
+ /** Every drum that is hit, over all sections, with every hit written out. */
223
+ drums: MusicDrumEvents[];
224
+ }
225
+ /** What the `check` tool prints about a music layer: lengths in bars (a short track shows), what starts in each bar, where the drums play. */
226
+ interface MusicSummary {
227
+ bpm: number;
228
+ meter: number;
229
+ /** The score set `meter`; when false 4 is only assumed. */
230
+ meterSet: boolean;
231
+ beats: number;
232
+ bars: number;
233
+ seconds: number;
234
+ tail: number;
235
+ tracks: Array<{
236
+ inst: string;
237
+ beats: number;
238
+ bars: number;
239
+ events: number;
240
+ perBar: string[];
241
+ }>;
242
+ drums: Array<{
243
+ from: number;
244
+ to: number;
245
+ kinds: string[];
246
+ }>;
247
+ }
248
+
175
249
  /**
176
250
  * pixi-effects public DSL types.
177
251
  *
@@ -588,6 +662,8 @@ type MusicDrums = {
588
662
  interface MusicOptions {
589
663
  /** Beats per minute, or `[beat, bpm]` points for a tempo change (a ritardando: `[[0, 96], [28, 96], [32, 60]]`). */
590
664
  bpm: number | Array<[number, number]>;
665
+ /** Beats in a bar (default 4; a waltz is 3). Only names the bars in `musicEvents()` and the `check` read-back; playback does not use it. */
666
+ meter?: number;
591
667
  tracks?: MusicTrack[];
592
668
  /** Drum patterns that repeat, or a list of them with `from` / `to` (beats) for sections. */
593
669
  drums?: MusicDrums | MusicDrums[];
@@ -853,6 +929,8 @@ interface AudioDescriptor {
853
929
  layer?: string;
854
930
  /** What it plays: `sfx "pop"`, `asset "bgm"`, `video "green"`. */
855
931
  source?: string;
932
+ /** A music layer's score read back: bars per track, what starts in each bar, the drum spans (for `movie.inspectAudio()` and the `check` tool). */
933
+ music?: MusicSummary;
856
934
  loop: boolean;
857
935
  start: number;
858
936
  end: number;
@@ -870,4 +948,4 @@ interface AudioDescriptor {
870
948
  sourceMap?: (u: number) => number;
871
949
  }
872
950
 
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 };
951
+ export { type SfxPreset 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 MusicInstrument as H, type ImageSequenceSpec as I, type MusicLevel as J, type Keyframe as K, type LightSequenceSpec as L, type MusicOptions as M, type MusicNote as N, type MusicSummary as O, type Props as P, type MusicTrack as Q, type MusicTrackEvents as R, type SequenceSpec as S, type TextSequenceSpec as T, type NullSequenceSpec as U, type PathShapeSpec as V, type PolygonShapeSpec as W, type PropValue as X, type RectShapeSpec as Y, type SfxKnobs as Z, type SfxOptions as _, type CameraSequenceSpec as a, type SfxVoice as a0, type ShaderSequenceSpec as a1, type ShapeSequenceSpec as a2, type SlideTransition as a3, type Stop as a4, type StopSpec as a5, type TransitionCommon as a6, type VideoSequenceSpec as a7, type WipeTransition as a8, type ZoomTransition as a9, normalizeStops as aa, pictureStops as ab, type Timeline as ac, type AudioDescriptor as ad, type MusicEvents as b, type TransitionSpec as c, type CompositionSpec as d, type SequenceCommon as e, type AssetSpec as f, type AudioAssetSpec as g, type AudioMusicSpec as h, type AudioSequenceSpec as i, type AudioSfxSpec as j, type ChromaKeyFilterSpec as k, type CircleShapeSpec as l, type CompositionSequenceSpec as m, type CrossfadeTransition as n, type CustomFilterSpec as o, type DissolveTransition as p, type Expr as q, type IrisTransition as r, type LineShapeSpec as s, type LumaTransition as t, type MaskRefSpec as u, type MatteRefSpec as v, type MusicDrum as w, type MusicDrumEvents as x, type MusicDrumHit as y, type MusicDrums as z };
package/docs/api.md CHANGED
@@ -211,6 +211,23 @@ 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
+ ### Your own layer type: `Sequence` and `registerSequenceType(type, ctor)`
215
+
216
+ A layer type that the library does not have (a different renderer, a data view) is a subclass of `Sequence`, registered under a `type` name; then `{ type: 'mine', at, duration, initial, keyframes, filters, mask }` works like any layer, in any composition.
217
+
218
+ ```js
219
+ import { Sequence, registerSequenceType } from 'pixi-effects';
220
+ import { Container } from 'pixi.js';
221
+
222
+ class Mine extends Sequence {
223
+ async build() { this.target = new Container(); this.intrinsicWidth = 200; this.intrinsicHeight = 100; } // the display object the keyframes move
224
+ async awaitFrameAt(local) { /* optional: draw the frame at `local` seconds (playback and export both call it, so draw from the time only) */ }
225
+ }
226
+ registerSequenceType('mine', Mine);
227
+ ```
228
+
229
+ `target` is the Pixi display object; `spec`, `parent`, `root`, `at`, `duration` are set for you; `scope()` gives the expression scope. Keyframes, filters, masks and the lifespan work on `target` as for the built-in layers. `awaitFrameAt(local)` is called once per frame by `gotoFrame` (the one path playback and export share), after the timeline has been set to that frame.
230
+
214
231
  ### `ease(name): (p: number) => number`
215
232
 
216
233
  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`.
@@ -351,6 +368,17 @@ const env = await musicEnvelope(music, { frameRate: 30 });
351
368
 
352
369
  The [audio-reactive](#audio-reactive-audioenvelope-react-bpmenvelope) envelope of an audio layer's [`music`](dsl.md#music-written-as-text-music): it renders the score and analyses that very sound, so `env.series` (`level`, `bass`, `mid`, `treble`, or your `bands`) follows the notes. `env.beats` are exact (the kick hits, or every beat when the score has no kick) and `env.bpm` is the starting tempo. Options are `audioEnvelope`'s (`frameRate`, `bands`, `beatBand`, `beatSensitivity`). Throws, saying why, when the score cannot play (for example no `bpm`). The envelope covers the music and its tail; when the layer starts at `at`, pass `at` to `react()`.
353
370
 
371
+ ### `musicEvents()`
372
+
373
+ ```ts
374
+ import { musicEvents } from 'pixi-effects';
375
+ const ev = await musicEvents(music); // the same object the audio layer plays; BEFORE movie.init, async like musicEnvelope()
376
+ ev.tracks[0].notes[0]; // { beat: 0, time: 0, duration: 4, seconds: 2, bar: 1, beatInBar: 1, name: 'Am7', chord: true, midi: [57, 60, 64, 67], velocity: 0.8 }
377
+ ev.drums[0]; // { kind: 'kick', hits: [{ beat, time, bar, beatInBar, velocity }, ...] }
378
+ ```
379
+
380
+ What a [`music`](dsl.md#music-written-as-text-music) score plays, as data: every note and chord with its `beat` and `time` (seconds from the start of the music, the tempo map applied: add the audio layer's `at` for movie time; swing and humanize are not applied), `duration`, `bar` and `beatInBar` (from `meter`, default 4), the audible `midi` pitches (`transpose` is in), the `name` **as the score wrote it** (a transposed score still says `Am7` while `midi` sounds a tone higher: caption from `midi` if you transpose), and every drum hit with the pattern repeated out. Also `bpm`, `meter`, `beats`, `bars`, `seconds` and per track `beats` / `bars`. Use it to make the picture follow the score: light the keys of the chord that sounds, caption its name, flash on the snare. A score that cannot play throws with the reason; warnings are logged as for the layer.
381
+
354
382
  ### `musicBuffer()`
355
383
 
356
384
  ```ts
package/docs/dsl.md CHANGED
@@ -254,9 +254,9 @@ A tune with no audio file: notes, chords and drum patterns written as strings, p
254
254
  } }
255
255
  ```
256
256
 
257
- **Layer.** `at` starts it, `volume` (and volume keyframes) scale it. `duration` defaults to the music's length plus its reverb tail (`tail`, 2.5 s), but never past the end of the composition (the music then ends with the movie, with a short fade). A shorter `duration` cuts it with a fade; a longer one is silent after the music unless **`loop: true`**, which repeats the score until the layer ends (default: the end of the composition). At `volume: 1` the music peaks at −6 dBFS (about −20 dB RMS), a bed that sits under speech and sound effects (an sfx peaks at −12 dBFS). Measured: a 4-bar tune (bell + bass + kick, a pluck and a hat on the repeat) at `volume: 0.8` reads −22 LUFS and −7.7 dBTP (`piano-roll`); web video is usually −14 to −16 LUFS, so 0.8 is not a limit: raise `volume` and watch `inspectAudio().loudness.truePeakDb`. `inspectAudio()` lists a music layer as ONE source (its start); to line a picture up with a note, compute the onset yourself: `at + beat × 60 / bpm` (build the bars and the score from one array so they cannot drift).
257
+ **Layer.** `at` starts it, `volume` (and volume keyframes) scale it. `duration` defaults to the music's length plus its reverb tail (`tail`, 2.5 s), but never past the end of the composition (the music then ends with the movie, with a short fade). A shorter `duration` cuts it with a fade; a longer one is silent after the music unless **`loop: true`**, which repeats the score until the layer ends (default: the end of the composition). At `volume: 1` the music peaks at −6 dBFS (about −20 dB RMS), a bed that sits under speech and sound effects (an sfx peaks at −12 dBFS). Measured: a 4-bar tune (bell + bass + kick, a pluck and a hat on the repeat) at `volume: 0.8` reads −22 LUFS and −7.7 dBTP (`piano-roll`); web video is usually −14 to −16 LUFS, so 0.8 is not a limit: raise `volume` and watch `inspectAudio().loudness.truePeakDb`. `inspectAudio()` lists a music layer as ONE source (its start); to line a picture up with a note, take its time from [`musicEvents(music)`](api.md#musicevents) (seconds from the start of the music, the tempo map applied: add the layer's `at`), or build the bars and the score from one array so they cannot drift.
258
258
 
259
- **Score** (`music`): `bpm` (a number, or `[beat, bpm]` points for a tempo change: `[[0, 96], [28, 96], [32, 60]]` slows down over beats 28–32, a ritardando), `tracks`, `drums`, `swing` (0–0.4: delays the off-beat eighths and sixteenths; 0.16 is a gentle lo-fi lilt), `reverb` (0–0.6, default 0.22), `humanize` (seconds of seeded timing jitter, default 0.012), `drumVol` (default 0.8), `grid` (drum steps per beat, default 4), `transpose` (semitones, all pitched tracks), `tail`, `seed`. Misspelt names warn with "did you mean". The music's length is its longest track (drums repeat up to it), so a track of rests (`'_:16'`) sets a length.
259
+ **Score** (`music`): `bpm` (a number, or `[beat, bpm]` points for a tempo change: `[[0, 96], [28, 96], [32, 60]]` slows down over beats 28–32, a ritardando), `tracks`, `drums`, `swing` (0–0.4: delays the off-beat eighths and sixteenths; 0.16 is a gentle lo-fi lilt), `reverb` (0–0.6, default 0.22), `humanize` (seconds of seeded timing jitter, default 0.012), `drumVol` (default 0.8), `grid` (drum steps per beat, default 4), `meter` (beats in a bar, default 4: it only names the bars in [`musicEvents()`](api.md#musicevents) and in the `check` read-back, playback does not use it), `transpose` (semitones, all pitched tracks), `tail`, `seed`. Misspelt names warn with "did you mean". The music's length is its longest track (drums repeat up to it), so a track of rests (`'_:16'`) sets a length.
260
260
 
261
261
  **Tracks.** `inst` is one of `keys` (electric piano), `pluck` (guitar / harp), `pad` (slow warm synth pad), `bass` (synth bass), `sub` (pure sine bass, for a heartbeat), `lead` (soft melodic lead), `bell` (glockenspiel), `musicbox` (a bright tine with a short ring). Options: `vol` (0–1.5, default 0.8, or `[beat, level]` points joined by lines for a fade-in or a swell), `pan` (−1…1), `step` (length of a token without `:`, in beats, default 1), `strum` (seconds between the notes of a chord, 0.01 sounds natural), `legato` (0.1–1, how much of a note's length it sounds), `tone` (0–1, brightness; lower is darker, a lo-fi dulling), `reverb` (0–1, this track's share of the reverb: dry bass, wet bells), `attack` / `release` (seconds; a pad's fade in and out), `ring` (seconds a bell or musicbox rings), `transpose` (semitones for this track). Good ranges: bass c1–c3, keys / pluck c3–c6, pad c2–c5, lead c4–c6, bell c5–c7.
262
262
 
@@ -271,13 +271,17 @@ A tune with no audio file: notes, chords and drum patterns written as strings, p
271
271
  | `~` `~:1` | hold: lengthens the note before |
272
272
  | `!` `,` | accent / soft, before or after the length: `c4!:1`, `c4:1,` |
273
273
 
274
- **Drums.** `drums` is an object of patterns, or a list of them with `from` / `to` (beats) so a groove can start late or stop (`[{ kick: 'x...' }, { from: 8, snare: '....x...' }]`). A pattern is a string of steps: `x` hit, `o` soft hit, `.` nothing (`|` and spaces are ignored); one step is 1/`grid` of a beat, so with the default grid 4 a 16-step string is one bar of 4/4, and a longer string (32 steps) is a longer figure that repeats. Drums: `kick snare hat openhat clap rim tom crash shaker sleigh` (`sleigh` is a jingle of small bells).
274
+ **Drums.** `drums` is an object of patterns, or a list of them with `from` / `to` (beats) so a groove can start late or stop (`[{ kick: 'x...' }, { from: 8, snare: '....x...' }]`). A pattern is a string of steps: `x` hit, `o` soft hit, `.` nothing (`|` and spaces are ignored); one step is 1/`grid` of a beat, so with the default grid 4 a 16-step string is one bar of 4/4, and a longer string (32 steps) is a longer figure that repeats. **The pattern repeats by itself** from `from` to `to`: you write one bar, not eight. `from` and `to` are **beats** from the start of the music (not bars): bar 9 of a 4/4 piece is `from: 32`; leave `to` out to play to the end. Drums: `kick snare hat openhat clap rim tom crash shaker sleigh` (`sleigh` is a jingle of small bells).
275
275
 
276
276
  **Other meters.** The notation counts beats (the quarter note is 1), so 3/4 is bars of 3 beats. 6/8 is bars of 3 beats too: a dotted quarter is `:1.5`, an eighth `:0.5`; set `grid: 2` so a drum step is an eighth note (a 6-step pattern is one bar; the same `grid: 2` gives 6 eighth steps per bar in 3/4), and leave `swing` at 0. `grid: 3` is for triplet eighths in 4/4 (a shuffle).
277
277
 
278
+ **Comping on named beats, and swing.** There is no "on beats 2 and 4" option: write the rests, `_:1 Dm7:0.5 _:0.5 _:1 Dm7:0.5 _:0.5` is a short chord on beats 2 and 4 of one bar. `swing` delays only notes that start on an off-beat eighth (`.5`) or sixteenth, so notes on whole beats stay straight; for a swung comp put the chords on the "and" (`_:1.5 Dm7:0.5 ...`).
279
+
280
+ **Read it back.** `check` prints a `music` block for every music layer: its length in bars per track (a track that ends early, or a bar that is short, is named), the names that start in each bar with their beat (`Am(2)`: on beat 2; no mark: on the downbeat), and where the drums play. That is how to verify a score without listening. Set `meter` for anything but 4/4, or a waltz is reported as bars of 4. [`musicEvents(music)`](api.md#musicevents) gives the same facts as data, with every note's beat, time and pitches.
281
+
278
282
  **Hear it without a movie.** [`musicBuffer(music)`](api.md#musicbuffer) returns a Web Audio buffer; [`examples/music-lab.html`](../examples/music-lab.html) plays eight finished scores and shows each one as source.
279
283
 
280
- **Seeing the music in the picture.** [`musicEnvelope(music, { frameRate })`](api.md#musicenvelope) analyses the same score into `level` / `bass` / `mid` / `treble` and exact beats for [`react()`](#react), so a title can pulse with the kick of a tune that has no file.
284
+ **Seeing the music in the picture.** [`musicEnvelope(music, { frameRate })`](api.md#musicenvelope) analyses the same score into `level` / `bass` / `mid` / `treble` and exact beats for [`react()`](#react), so a title can pulse with the kick of a tune that has no file. [`musicEvents(music)`](api.md#musicevents) lists the notes themselves (which chord, which pitches, which bar), so a keyboard can light the keys that sound or a caption can name the chord.
281
285
 
282
286
  ### `composition`
283
287
 
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.27.2. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
9
+ > Written for pixi-effects 0.28.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
 
@@ -29,6 +29,15 @@ You describe a video as a tree of plain objects; the library plays it in a canva
29
29
  From a browser tool the result is a `data:` URL string: `agent-browser eval "movie.contactSheet({ count: 6, as: 'dataURL' })" | python3 scripts/save-image.py /absolute/sheet.png`, then open the PNG. Sheet tiles are small (~480 px): use `snapshot` for detail. `inspect` output is long on busy scenes: pass `{ layers: 'none' }` and read `issues` first. Give layers a `name` so inspect paths are readable. A moving ticker is not flagged as cut off.
30
30
  6. **Iterate** on what you saw (clipped text, collisions, wrong timing). Fast motion looks choppy: add `motionBlur: true` to `movie.init` (render, snapshot and contact sheet then show a film-style blur; renders take ~8× longer). Then export: `const blob = await movie.render({ format: 'mp4' })` (about real time; `movie.on('progress', …)` reports 0–100). **While you iterate, do not export the whole movie:** `check --no-export --at <the moment you changed>` takes seconds (3 to 10 s on the gallery pieces: most of it is loading the page and measuring the sound); to see motion, `render({ range: [from, to] })` / `pixi-effects-render --range 10:15` / `--scene name` exports only that part (picture and sound); `draft: true` / `--draft` is half size, low quality and no motion blur: a much smaller file (a piece that asks for motion blur renders much faster as a draft: 4 s of `hanabi-night` took 14.0 s, and 8.2 s as a draft, about 7 s of either being the browser's start-up), but the drawing itself is not faster. Export the whole movie once, at the end.
31
31
 
32
+ ## For a film (more than a title card): direct it first
33
+
34
+ **Read `reference/direction.md` before writing a composition for a product film, a company introduction, a launch or a data story.** It is renderer-neutral: write `brief.md` (the one sentence to remember, the REAL assets with paths, what must never be invented; if a needed asset is missing, stop and ask), `style-guide.md` (palette and type taken from the real source, a motion rule per class of object) and `shotlist.md` (a row per beat with entry and exit state) next to the page; then build, then review the rendered frames cold. What is specific to this library:
35
+
36
+ - **Time and sound:** pick a tempo whose bar is a round number of seconds (96 bpm: 2.5 s; 120 bpm: 2 s), put every cut on a bar line and every sfx on its beat. `npx pixi-effects check` prints a `music` read-back (bars per track, what starts on which beat, the drum span): verify the sound with it.
37
+ - **Review:** `check --at 3.5,title@end,50%` writes the contact sheet and single frames; look at mid-transition frames of the exported file too. Frames are seekable and deterministic here: render the same range twice and compare the frames if you doubt it.
38
+ - **Formats:** make the formats the human asked for, one if they did not say. When a vertical cut is wanted it is its own composition: one source, a layout function, `?f=9x16` (recipe `two-formats`); `check` and `render` take `--query f=9x16`.
39
+ - Fit a picture into a rectangle with the `photo()` helper of that recipe (`scale: 'cover'` fits the PARENT only); designed pictures (posters, flyers, screenshots) are shown whole, never cropped.
40
+
32
41
  ## The rules that cause most failures
33
42
 
34
43
  - **Keyframe `at` starts at 0 when the layer appears** (negative = back from the layer's end). A layer's own `at` and a transition's `at` are composition time.
@@ -56,6 +65,7 @@ Centred text must be positioned by `anchorX/anchorY: 0.5`; estimate text width a
56
65
  - `reference/cheatsheet.md` — every layer type, prop, default and rule on one page.
57
66
  - `reference/recipes.md` — tested building blocks: slam type, lower-third, marquee, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions + music, looping, particles, gradients/vignette, three.js metal.
58
67
  - `reference/pitfalls.md` — the full list of real mistakes, with status.
68
+ - `reference/direction.md` — how to direct a film (any renderer): brief, style guide, shot list, assets rule, sound on the beat grid, review protocol, formats, gates.
59
69
  - `npx pixi-effects check` — the one-command review (see Workflow step 5).
60
70
  - `template.html` — the starting file. (It loads the released library from a CDN, so it works from any folder.)
61
71
  - `scripts/save-image.py` — save a `dataURL` result (contact sheet / snapshot) to a PNG.
@@ -123,7 +133,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
123
133
 
124
134
  **audio** — a file: `asset`, `loop`, `volume`, `speed` (the pitch follows; `time` keyframes work as for video; not for `sfx` / `music`: change an sfx's `pitch` instead, or put it in a composition and remap that); `volume` (top level, or in `initial` like every other layer's starting values); fade with volume keyframes (`{ at: -2, to: { volume: 0 }, duration: 2 }` fades the last 2 s). Omit `duration` for a one-shot clip: it lasts exactly as long as the clip. With `loop: true` and no `duration` it lasts until the composition ends. An explicit `duration` longer than the clip goes silent after the clip unless `loop: true`.
125
135
  **Or a sound effect with no file — `sfx`:** `{ type: 'audio', sfx: 'swoosh', at: 2 }` is the whole thing. Presets, default length in s: `click` 0.04 · `pop` 0.12 · `swoosh` 0.5 (loudest 0.16 s in, moves left → right) · `swipe` 0.22 · `hit` 0.7 · `riser` 1 (loudest at its END: `at = hit − duration`) · `chime` 1.4 · `beep` 0.16 · `coin` 0.4 · `glitch` 0.35 · `typewriter` 0.06. Knobs: `sfx: { preset, pitch /* semitones ±24 */, brightness /* −1 dark … 1 bright */, seed /* another take */ }`. **The layer's `duration` IS the sound's length** (omit it; set it to stretch, e.g. a riser; at most 10 s); `volume` is its level (1 = the preset's standard level, peaking at −12 … −18 dBFS by preset; above 1 is allowed: a sparse track of clicks measured −32 LUFS at 0.5 and needed about 2 to reach −24 LUFS). No `loop`: one layer per hit (a JS loop). Custom sound: `sfx: { voices: [{ wave: 'noise' | 'sine' | 'square' | 'saw' | 'triangle', freq: 440 | [from, to], filter: { type: 'lowpass' | 'highpass' | 'bandpass', freq: [from, to] }, envelope: 'fall' | 'bell' | 'swell' | 'hold', pan: [-1, 1] }] }`. The export (mp4 / webm / mov) encodes this same mix (lossy AAC / Opus), on time within a few ms.
126
- **Or music with no file — `music`:** `{ type: 'audio', music: { bpm: 96, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4 Dm7:4 G7:4' }, { inst: 'bass', notes: 'c2:2 g1:2 …' }], drums: { kick: 'x...x...', snare: '....x...', hat: 'x.x.x.x.' } } }` — a tune written as text (notes in LOWERCASE `c4:2` in beats, chords start with a CAPITAL `Am7` `G7` `C@4` `Am7/e` (`g7` is a very high note, not G7), `[c4 e4 g4]`, rest `_`, hold `~`, `!` accent; `|` ignored). Instruments `keys pluck pad bass sub lead bell musicbox`; drums `kick snare hat openhat clap rim tom crash shaker sleigh` as 16th-note step strings (`x` hit, `o` soft, `.` none; a list of `{ from, to, … }` for sections). Score options `bpm` (or `[[beat, bpm], …]` for a ritardando) `swing reverb (0–0.6) humanize drumVol grid transpose tail seed`; track options `vol` (or `[[beat, level], …]` for a fade) `pan step strum legato tone reverb attack release ring transpose`. Lasts its notes + tail, ends with the movie, `loop: true` repeats; peaks −6 dBFS at `volume: 1`. `await musicEnvelope(music, { frameRate })` → an envelope for `react()`; `await musicBuffer(music)` → a Web Audio `AudioBuffer` to play yourself.
136
+ **Or music with no file — `music`:** `{ type: 'audio', music: { bpm: 96, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4 Dm7:4 G7:4' }, { inst: 'bass', notes: 'c2:2 g1:2 …' }], drums: { kick: 'x...x...', snare: '....x...', hat: 'x.x.x.x.' } } }` — a tune written as text (notes in LOWERCASE `c4:2` in beats, chords start with a CAPITAL `Am7` `G7` `C@4` `Am7/e` (`g7` is a very high note, not G7), `[c4 e4 g4]`, rest `_`, hold `~`, `!` accent; `|` ignored). Instruments `keys pluck pad bass sub lead bell musicbox`; drums `kick snare hat openhat clap rim tom crash shaker sleigh` as 16th-note step strings (`x` hit, `o` soft, `.` none; a list of `{ from, to, … }` for sections). Score options `bpm` (or `[[beat, bpm], …]` for a ritardando) `meter` (beats in a bar, default 4: names the bars in `check`'s read-back, no effect on the sound) `swing reverb (0–0.6) humanize drumVol grid transpose tail seed`; track options `vol` (or `[[beat, level], …]` for a fade) `pan step strum legato tone reverb attack release ring transpose`. A plain capital letter is a major chord (`G`, `Bb`, `F#`; `Ebmaj7`, `F#m7`), `E5` is root and fifth only (e3 b3), every chord token is struck again (`~` holds), a track can start late with a rest (`_:32 e4:1 …`), `f#3` / `bb2` are sharps and flats, c4 is middle C, chord names are voiced around octave 3 (good ranges for notes: bass c1–c3, keys / pluck c3–c6, pad c2–c5, lead c4–c6, bell c5–c7). **Drums repeat by themselves** (one bar is enough) and `drums: { from: 32, to: 64, … }` counts BEATS, not bars (`drums` is one object, or a list of them for sections). `check` reads the score back (bars per track, what starts in each bar and on which beat: `Am(2)`; drum span): trust that over counting by hand; `await musicEvents(music)` gives the same as data. Lasts its notes + tail, ends with the movie, `loop: true` repeats; peaks −6 dBFS at `volume: 1`. `await musicEnvelope(music, { frameRate })` → an envelope for `react()`; `await musicBuffer(music)` → a Web Audio `AudioBuffer` to play yourself.
127
137
 
128
138
  **shape** — `shape: 'rect' | 'circle' | 'ellipse' | 'arc' | 'line' | 'polygon' | 'path'`:
129
139
 
@@ -379,6 +389,9 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
379
389
  97. **A deck inside your own composition loses its `transitions` and `stops`** unless you take them across: `const d = deck({ width: 1920, height: 1080, pages })`, then `{ type: 'composition', sequences: d.composition.sequences, transitions: d.composition.transitions }`. Pass `width` / `height` yourself for a page that is not the movie's own size.
380
390
  98. **A matte layer needs a fill.** A named matte (`mask: 'glass'`) that is a shape with no `fillColor` / `fillGradient` (or a text with no fill) cuts everything away and nothing warns: the layers it should show simply vanish. Give the matte a fill (white for `alpha`, a gradient for `luma`).
381
391
  99. **In a `music` score a capital letter is a chord and a lowercase one a note.** `G7` is G dominant seventh, `C5` a power chord, `C6` a sixth chord; `g7`, `c5`, `c6` are notes (octave 7 is a very high note: it used to be what `G7` meant, silently). A bare note in capitals (`Bb2`) is refused with the lowercase spelling (`bb2`); inside brackets (`[C4 E4 G4]`) any case is a note. An unknown chord kind suggests the nearest (`Cmin7` → `Cm7`, `Csus` → `Csus4`, `CM7` → `Cmaj7`).
392
+ 100. **Read the score back before you trust it.** `check` prints a `music` block: every track's length in bars (a track that ends early or has a short bar is named), the names that start in each bar with their beat (`Am(2)` starts on beat 2; no mark = the downbeat; the first four names of a bar are shown, `musicEvents()` lists everything), and the drum span. Count bars by the read-back, not by hand: a bar of 3 beats in a score with no `meter` is shown as bars of 4, so set `meter: 3` for a waltz. Drums repeat by themselves and `from` / `to` are beats; `swing` only moves notes that start off the beat, so a comp on beats 2 and 4 stays straight.
393
+ 101. **A film built from one adjective looks like every other one.** Read `reference/direction.md` and write `brief.md` / `style-guide.md` / `shotlist.md` first: real assets with paths, the one sentence to remember, a row per beat with entry and exit state. If a real asset is missing, ask: a drawn fake (a logo, a dashboard, a figure) is the failure that matters. Judge the contact sheet cold: three defects, each with a timestamp, evidence and a local fix.
394
+ 102. **A vertical cut is a second composition, not a crop, and only when someone asked for it.** One source, a layout function, `?f=9x16`; `check` and `render` take `--query f=9x16` (recipe `two-formats`). Look at a contact sheet of each format: type that fits wide is tiny tall. Ask which formats are wanted; make one if the answer is open. In a 1080p film keep every line of text at 34 px or more; if you cannot read it in a sheet tile, a phone cannot.
382
395
 
383
396
 
384
397
  <!-- ===== skills/pixi-effects/reference/recipes.md ===== -->
@@ -1488,6 +1501,31 @@ return [
1488
1501
  ];
1489
1502
  ```
1490
1503
 
1504
+ ## One source, two formats (16:9 and 9:16), and a photo inside a region
1505
+
1506
+ Only when a vertical cut is wanted (ask first). It is not a crop of the wide one: write each scene as a function of a layout, switch with a query (`?f=9x16`), and render each format on its own: `npx pixi-effects check film.html --query f=9x16`, `npx pixi-effects render film.html --query f=9x16 -o film-9x16.mp4`. `photo()` fits a picture into any rectangle like CSS `cover` (scale from the image's own size, a mask for the rectangle); `scale: 'cover'` only fits the PARENT.
1507
+
1508
+ ```js
1509
+ // @docs-only two-formats
1510
+ const PORTRAIT = new URLSearchParams(location.search).get('f') === '9x16';
1511
+ const W = PORTRAIT ? 1080 : 1920, H = PORTRAIT ? 1920 : 1080, MX = PORTRAIT ? 76 : 150; // margins: nothing important in the outer 5 %
1512
+ const IMG = { hero: [1920, 1280] }; // each photo's size (assets: [{ name: 'hero', src: '...' }])
1513
+ const photo = (key, x, y, w, h, duration) => {
1514
+ const cover = Math.max(w / IMG[key][0], h / IMG[key][1]);
1515
+ return { type: 'image', asset: key, duration,
1516
+ mask: { type: 'shape', shape: 'rect', width: w, height: h, initial: { x: x + w / 2, y: y + h / 2, fillColor: '#fff' } }, // the mask lives in the parent's coordinates
1517
+ initial: { x: x + w / 2, y: y + h / 2, anchorX: 0.5, anchorY: 0.5, scale: cover }, // image anchors go in `initial`
1518
+ keyframes: [{ at: 0, to: { scale: cover * 1.1 }, duration, ease: 'sine.inOut' }] }; // a slow push
1519
+ };
1520
+ const scene = { type: 'composition', name: 'hook', at: 0, duration: 5.5, width: W, height: H, sequences: [
1521
+ photo('hero', 0, 0, W, H, 5.5),
1522
+ { type: 'text', text: 'Headline', initial: { x: MX, y: PORTRAIT ? 1150 : 620 }, style: { fontSize: PORTRAIT ? 100 : 112, fill: '#fff' } }, // wide: lower left; tall: lower third
1523
+ ] };
1524
+ // movie.init({ width: W, height: H, composition: { sequences: [scene] } })
1525
+ ```
1526
+
1527
+ ---
1528
+
1491
1529
  ## A batch of videos from a table (one video per row)
1492
1530
 
1493
1531
  A batch of videos is a loop outside the video. The page reads what changes from its address (`new URLSearchParams(location.search)`), and `pixi-effects-render --query "name=Aiko&score=92"` opens it with those values. Names with spaces or non-Latin letters must be URL-encoded (`jq -sRr @uri` as below, or `encodeURIComponent` in Node). Each video starts its own browser, so N videos cost N start-ups: try one with `--draft` (half size, fast), then render the real ones. To make the pieces of one row differ in more than text, use the same values to pick colours or an image name.
@@ -1517,6 +1555,146 @@ done < table.csv
1517
1555
  ```
1518
1556
 
1519
1557
 
1558
+ <!-- ===== skills/pixi-effects/reference/direction.md ===== -->
1559
+
1560
+ # Directing a film (for any AI that makes video with code)
1561
+
1562
+ Read this before you write a composition for anything longer than a title card: a product film, a company introduction, a launch, a data story. It is about the decisions around the renderer, so it names no tool; `SKILL.md` has the pixi-effects commands.
1563
+
1564
+ A one-line prompt makes the model fill every missing decision with its safest default: large text in the middle, a gradient behind it, everything fading in, the logo last. The fix is not a cleverer prompt. It is to remove the missing decisions before anything is drawn, and to judge what was drawn with evidence.
1565
+
1566
+ ## The five layers (do not collapse them into one step)
1567
+
1568
+ | Layer | The question | Where the answer lives |
1569
+ |---|---|---|
1570
+ | Director | What is the film trying to say, to whom? | `brief.md` |
1571
+ | Reference | What should it look and feel like? | `style-guide.md` |
1572
+ | Timeline | What changes at each beat, and why? | `shotlist.md` |
1573
+ | Renderer | How is every frame produced? | `src/` (seekable code) |
1574
+ | Critic | What failed, exactly where, and why? | `reviews/` |
1575
+
1576
+ ## Keep the decisions in files, not in the chat
1577
+
1578
+ A film whose only record is a conversation is a lucky session. Put the decisions where the next run can read them:
1579
+
1580
+ ```
1581
+ film/
1582
+ brief.md the one-sentence goal, the audience, the product facts, the real assets
1583
+ style-guide.md palette, type, motion rules, banned defaults
1584
+ shotlist.md beats, states, timestamps, purpose
1585
+ assets/ approved logos, photos, screens, footage, sound
1586
+ src/ the composition and its code
1587
+ reviews/ contact sheets and defect notes
1588
+ out/ exports
1589
+ ```
1590
+
1591
+ The next film swaps the brief and the assets and keeps the rest. A run that stops can resume from the last file it finished and say which one is missing; "the last command exited 0" is never the definition of done.
1592
+
1593
+ ## brief.md: facts about the product, choices about the film
1594
+
1595
+ Separate what is true (it comes from the product) from what is chosen (it comes from you). The model may invent a camera move or a stronger opening. It may not invent a dashboard, a figure, a logo or a client.
1596
+
1597
+ ```
1598
+ FILM
1599
+ One sentence: what the viewer should remember
1600
+ Audience: who it is for
1601
+ Duration: seconds
1602
+ Formats: ask the human; one format unless they name more (see "Formats" below)
1603
+
1604
+ ASSETS (paths; everything on screen comes from here)
1605
+ Logo · photos · screens · copy and numbers · brand type and colours · sound reference
1606
+
1607
+ VISUAL RULES
1608
+ Keep: traits taken from the reference
1609
+ Avoid: the clichés you will not use
1610
+ Never: invent a product screen, a metric, a claim or a logo
1611
+
1612
+ DELIVERABLES
1613
+ Final video, poster frame, contact sheets, source, an asset list, a note on what a human still has to check
1614
+ ```
1615
+
1616
+ **Collect the assets first, list them, and read the list before building.** If the brief needs an asset that is missing, stop and ask. A convincing fake (a drawn logo, an invented dashboard, a guessed number) is the one failure that cannot be repaired afterwards. Words and numbers on screen are the source's own; take colours and type from the source itself (a site's CSS, a brand sheet), never from an adjective such as "premium".
1617
+
1618
+ ## style-guide.md: a visual grammar written down
1619
+
1620
+ Without one the model supplies its own, which is why unrelated one-prompt films look related. Give it a frame, a film or the real site, and make it write what it saw:
1621
+
1622
+ ```
1623
+ REFERENCE ANALYSIS
1624
+ Palette: the actual colours (hex), not an adjective
1625
+ Typography: family, weight, scale
1626
+ Composition: where the eye goes first
1627
+ Pacing: average time between meaningful changes
1628
+ Motion: snap, glide, overshoot, cut or hold
1629
+ Texture: grain, paper, glass, flat
1630
+ DO NOT COPY: the reference's subjects, logos, copy or product imagery
1631
+ ```
1632
+
1633
+ The result is a style guide, not a collage of borrowed shots. Add a **motion rule per class of object**, because "motion with weight" is the difference between decoration and direction: small controls snap, large panels settle, the camera is almost invisible, headlines enter strongly and then hold long enough to read, one playful object is allowed to overshoot. If everything overshoots, nothing feels precise. After each move ask: does the viewer know where to look?
1634
+
1635
+ ## shotlist.md: states and beats, not effects
1636
+
1637
+ "Zoom in, then morph, then add particles" lists activity. A plan says what the viewer knows at each moment:
1638
+
1639
+ ```
1640
+ 00-02 HOOK one image that earns attention
1641
+ 02-05 PROBLEM the friction, shown, not described in a paragraph
1642
+ 05-09 REVEAL the product enters through a real interaction
1643
+ 09-13 PROOF one screen or number that demonstrates the claim
1644
+ 13-16 CLOSE a clean final state and a readable call to action
1645
+ ```
1646
+
1647
+ Each row has an entry state, an exit state and a reason to exist. For a UI film write the state list first (SEARCH → RESULT → DETAIL → ACTION → CONFIRMATION) so one component can change size, colour and content while staying recognisably the same object. The shot list is a contract: a shot nobody can justify is cut.
1648
+
1649
+ ## Seekable and deterministic, or it cannot be reviewed
1650
+
1651
+ Frame 240 must render directly, without replaying frames 0 to 239: no hidden clock, no unseeded randomness, nothing that only works after eight seconds of running. Think of a frame as a query: `frame = render(time, shotlist, assets, style, seed)`. Then a revision is local (change one time value, one curve, one asset) and a reviewed frame stays reviewed. Two renders of the same moment must give the same pixels; check it.
1652
+
1653
+ ## Sound is on the same timeline
1654
+
1655
+ Many AI films look finished with the sound off; then the click lands after the button, the cut misses the beat, the logo arrives before the music resolves. Write a beat grid and drive picture and sound from it: choose a tempo whose bar is a round number of seconds, put every cut on a bar line and every cue on the beat it belongs to. With a licensed or original track, measure its beats and cut against them. Test: close your eyes and listen; if the film has no shape without the picture, the sound is not finished.
1656
+
1657
+ ## Review the rendered frames, not your intent
1658
+
1659
+ Do not start by watching a full export ten times, and do not describe what you meant to make. Ask the renderer for a contact sheet: one frame at every beat, a short strip around the fastest transitions, and a frame from the middle of each cut taken from the exported file. Look cold, then write the **three largest defects**, each with a **timestamp**, the **evidence** (what you see) and a **local fix**; patch only those, render those frames again. Ask for a second pair of eyes on the sheet whenever one is available: the author reads their own intent into it.
1660
+
1661
+ ```
1662
+ [ ] text readable at phone size (about 34 px or more on a 1080p frame)
1663
+ [ ] nothing in the outer 5 % of the frame
1664
+ [ ] real assets only, none redrawn or invented
1665
+ [ ] one type and colour system across scenes
1666
+ [ ] no awkward half-transition frame
1667
+ [ ] cuts and sound cues land together
1668
+ [ ] the first two seconds are a real hook
1669
+ [ ] the last frame works as a poster
1670
+ ```
1671
+
1672
+ "The agent made a video" stops when the file exists. "The agent directed a video" stops when the evidence says the film works.
1673
+
1674
+ ## Formats
1675
+
1676
+ A 16:9 film and a 9:16 cut are different compositions: the vertical one needs larger type, fewer things at once, another layout, sometimes a tighter story. Never crop the middle of the wide one. **Do not make a format nobody asked for:** ask which formats are wanted, make one if the answer is "whatever", and offer the others. When more than one is wanted, keep the same assets, timeline and states, define the layout per format, and look at a contact sheet of each: an export is correct because the composition survived the format, not because the encoder finished.
1677
+
1678
+ ## Gates, and what to do when no human is there
1679
+
1680
+ Long runs need places to stop. With a human present:
1681
+
1682
+ 1. approve the references and the asset list
1683
+ 2. approve the style guide and the shot list
1684
+ 3. look at representative stills
1685
+ 4. look at a rough cut and its contact sheet
1686
+ 5. repair the three highest-impact defects
1687
+ 6. check sound, formats and deliverables
1688
+
1689
+ Between gates the agent may work for hours; it never makes an irreversible brand decision (logo, claim, number) for the human, and a missing real asset is a blocker, not an invitation to invent one. If the reference is unclear, the right output is a question or one small test frame, not nine hundred polished wrong ones. With nobody to ask, record each decision you took as a "ruling" in the review notes, with what it costs if wrong, and finish with a short note of what still needs a human.
1690
+
1691
+ ## The loop
1692
+
1693
+ PROMPT → CLIP is a demo. BRIEF → SYSTEM → FILM → REVIEW → BETTER SYSTEM is a studio: every stage leaves evidence the next one can use, and the tenth film can be better than the first.
1694
+
1695
+ *The shape of this guide follows a published breakdown of building a code-rendered video studio around an AI model ("Motion Engineering", @0xwhrrari, September 2026); the wording and the checklist are rewritten for this library's users.*
1696
+
1697
+
1520
1698
  <!-- ===== docs/dsl.md ===== -->
1521
1699
 
1522
1700
  # DSL Reference
@@ -1775,9 +1953,9 @@ A tune with no audio file: notes, chords and drum patterns written as strings, p
1775
1953
  } }
1776
1954
  ```
1777
1955
 
1778
- **Layer.** `at` starts it, `volume` (and volume keyframes) scale it. `duration` defaults to the music's length plus its reverb tail (`tail`, 2.5 s), but never past the end of the composition (the music then ends with the movie, with a short fade). A shorter `duration` cuts it with a fade; a longer one is silent after the music unless **`loop: true`**, which repeats the score until the layer ends (default: the end of the composition). At `volume: 1` the music peaks at −6 dBFS (about −20 dB RMS), a bed that sits under speech and sound effects (an sfx peaks at −12 dBFS). Measured: a 4-bar tune (bell + bass + kick, a pluck and a hat on the repeat) at `volume: 0.8` reads −22 LUFS and −7.7 dBTP (`piano-roll`); web video is usually −14 to −16 LUFS, so 0.8 is not a limit: raise `volume` and watch `inspectAudio().loudness.truePeakDb`. `inspectAudio()` lists a music layer as ONE source (its start); to line a picture up with a note, compute the onset yourself: `at + beat × 60 / bpm` (build the bars and the score from one array so they cannot drift).
1956
+ **Layer.** `at` starts it, `volume` (and volume keyframes) scale it. `duration` defaults to the music's length plus its reverb tail (`tail`, 2.5 s), but never past the end of the composition (the music then ends with the movie, with a short fade). A shorter `duration` cuts it with a fade; a longer one is silent after the music unless **`loop: true`**, which repeats the score until the layer ends (default: the end of the composition). At `volume: 1` the music peaks at −6 dBFS (about −20 dB RMS), a bed that sits under speech and sound effects (an sfx peaks at −12 dBFS). Measured: a 4-bar tune (bell + bass + kick, a pluck and a hat on the repeat) at `volume: 0.8` reads −22 LUFS and −7.7 dBTP (`piano-roll`); web video is usually −14 to −16 LUFS, so 0.8 is not a limit: raise `volume` and watch `inspectAudio().loudness.truePeakDb`. `inspectAudio()` lists a music layer as ONE source (its start); to line a picture up with a note, take its time from [`musicEvents(music)`](api.md#musicevents) (seconds from the start of the music, the tempo map applied: add the layer's `at`), or build the bars and the score from one array so they cannot drift.
1779
1957
 
1780
- **Score** (`music`): `bpm` (a number, or `[beat, bpm]` points for a tempo change: `[[0, 96], [28, 96], [32, 60]]` slows down over beats 28–32, a ritardando), `tracks`, `drums`, `swing` (0–0.4: delays the off-beat eighths and sixteenths; 0.16 is a gentle lo-fi lilt), `reverb` (0–0.6, default 0.22), `humanize` (seconds of seeded timing jitter, default 0.012), `drumVol` (default 0.8), `grid` (drum steps per beat, default 4), `transpose` (semitones, all pitched tracks), `tail`, `seed`. Misspelt names warn with "did you mean". The music's length is its longest track (drums repeat up to it), so a track of rests (`'_:16'`) sets a length.
1958
+ **Score** (`music`): `bpm` (a number, or `[beat, bpm]` points for a tempo change: `[[0, 96], [28, 96], [32, 60]]` slows down over beats 28–32, a ritardando), `tracks`, `drums`, `swing` (0–0.4: delays the off-beat eighths and sixteenths; 0.16 is a gentle lo-fi lilt), `reverb` (0–0.6, default 0.22), `humanize` (seconds of seeded timing jitter, default 0.012), `drumVol` (default 0.8), `grid` (drum steps per beat, default 4), `meter` (beats in a bar, default 4: it only names the bars in [`musicEvents()`](api.md#musicevents) and in the `check` read-back, playback does not use it), `transpose` (semitones, all pitched tracks), `tail`, `seed`. Misspelt names warn with "did you mean". The music's length is its longest track (drums repeat up to it), so a track of rests (`'_:16'`) sets a length.
1781
1959
 
1782
1960
  **Tracks.** `inst` is one of `keys` (electric piano), `pluck` (guitar / harp), `pad` (slow warm synth pad), `bass` (synth bass), `sub` (pure sine bass, for a heartbeat), `lead` (soft melodic lead), `bell` (glockenspiel), `musicbox` (a bright tine with a short ring). Options: `vol` (0–1.5, default 0.8, or `[beat, level]` points joined by lines for a fade-in or a swell), `pan` (−1…1), `step` (length of a token without `:`, in beats, default 1), `strum` (seconds between the notes of a chord, 0.01 sounds natural), `legato` (0.1–1, how much of a note's length it sounds), `tone` (0–1, brightness; lower is darker, a lo-fi dulling), `reverb` (0–1, this track's share of the reverb: dry bass, wet bells), `attack` / `release` (seconds; a pad's fade in and out), `ring` (seconds a bell or musicbox rings), `transpose` (semitones for this track). Good ranges: bass c1–c3, keys / pluck c3–c6, pad c2–c5, lead c4–c6, bell c5–c7.
1783
1961
 
@@ -1792,13 +1970,17 @@ A tune with no audio file: notes, chords and drum patterns written as strings, p
1792
1970
  | `~` `~:1` | hold: lengthens the note before |
1793
1971
  | `!` `,` | accent / soft, before or after the length: `c4!:1`, `c4:1,` |
1794
1972
 
1795
- **Drums.** `drums` is an object of patterns, or a list of them with `from` / `to` (beats) so a groove can start late or stop (`[{ kick: 'x...' }, { from: 8, snare: '....x...' }]`). A pattern is a string of steps: `x` hit, `o` soft hit, `.` nothing (`|` and spaces are ignored); one step is 1/`grid` of a beat, so with the default grid 4 a 16-step string is one bar of 4/4, and a longer string (32 steps) is a longer figure that repeats. Drums: `kick snare hat openhat clap rim tom crash shaker sleigh` (`sleigh` is a jingle of small bells).
1973
+ **Drums.** `drums` is an object of patterns, or a list of them with `from` / `to` (beats) so a groove can start late or stop (`[{ kick: 'x...' }, { from: 8, snare: '....x...' }]`). A pattern is a string of steps: `x` hit, `o` soft hit, `.` nothing (`|` and spaces are ignored); one step is 1/`grid` of a beat, so with the default grid 4 a 16-step string is one bar of 4/4, and a longer string (32 steps) is a longer figure that repeats. **The pattern repeats by itself** from `from` to `to`: you write one bar, not eight. `from` and `to` are **beats** from the start of the music (not bars): bar 9 of a 4/4 piece is `from: 32`; leave `to` out to play to the end. Drums: `kick snare hat openhat clap rim tom crash shaker sleigh` (`sleigh` is a jingle of small bells).
1796
1974
 
1797
1975
  **Other meters.** The notation counts beats (the quarter note is 1), so 3/4 is bars of 3 beats. 6/8 is bars of 3 beats too: a dotted quarter is `:1.5`, an eighth `:0.5`; set `grid: 2` so a drum step is an eighth note (a 6-step pattern is one bar; the same `grid: 2` gives 6 eighth steps per bar in 3/4), and leave `swing` at 0. `grid: 3` is for triplet eighths in 4/4 (a shuffle).
1798
1976
 
1977
+ **Comping on named beats, and swing.** There is no "on beats 2 and 4" option: write the rests, `_:1 Dm7:0.5 _:0.5 _:1 Dm7:0.5 _:0.5` is a short chord on beats 2 and 4 of one bar. `swing` delays only notes that start on an off-beat eighth (`.5`) or sixteenth, so notes on whole beats stay straight; for a swung comp put the chords on the "and" (`_:1.5 Dm7:0.5 ...`).
1978
+
1979
+ **Read it back.** `check` prints a `music` block for every music layer: its length in bars per track (a track that ends early, or a bar that is short, is named), the names that start in each bar with their beat (`Am(2)`: on beat 2; no mark: on the downbeat), and where the drums play. That is how to verify a score without listening. Set `meter` for anything but 4/4, or a waltz is reported as bars of 4. [`musicEvents(music)`](api.md#musicevents) gives the same facts as data, with every note's beat, time and pitches.
1980
+
1799
1981
  **Hear it without a movie.** [`musicBuffer(music)`](api.md#musicbuffer) returns a Web Audio buffer; [`examples/music-lab.html`](../examples/music-lab.html) plays eight finished scores and shows each one as source.
1800
1982
 
1801
- **Seeing the music in the picture.** [`musicEnvelope(music, { frameRate })`](api.md#musicenvelope) analyses the same score into `level` / `bass` / `mid` / `treble` and exact beats for [`react()`](#react), so a title can pulse with the kick of a tune that has no file.
1983
+ **Seeing the music in the picture.** [`musicEnvelope(music, { frameRate })`](api.md#musicenvelope) analyses the same score into `level` / `bass` / `mid` / `treble` and exact beats for [`react()`](#react), so a title can pulse with the kick of a tune that has no file. [`musicEvents(music)`](api.md#musicevents) lists the notes themselves (which chord, which pitches, which bar), so a keyboard can light the keys that sound or a caption can name the chord.
1802
1984
 
1803
1985
  ### `composition`
1804
1986
 
@@ -3403,6 +3585,23 @@ The whole movie as rows on a time axis, for a person to read at a glance. `timel
3403
3585
 
3404
3586
  `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.
3405
3587
 
3588
+ ### Your own layer type: `Sequence` and `registerSequenceType(type, ctor)`
3589
+
3590
+ A layer type that the library does not have (a different renderer, a data view) is a subclass of `Sequence`, registered under a `type` name; then `{ type: 'mine', at, duration, initial, keyframes, filters, mask }` works like any layer, in any composition.
3591
+
3592
+ ```js
3593
+ import { Sequence, registerSequenceType } from 'pixi-effects';
3594
+ import { Container } from 'pixi.js';
3595
+
3596
+ class Mine extends Sequence {
3597
+ async build() { this.target = new Container(); this.intrinsicWidth = 200; this.intrinsicHeight = 100; } // the display object the keyframes move
3598
+ async awaitFrameAt(local) { /* optional: draw the frame at `local` seconds (playback and export both call it, so draw from the time only) */ }
3599
+ }
3600
+ registerSequenceType('mine', Mine);
3601
+ ```
3602
+
3603
+ `target` is the Pixi display object; `spec`, `parent`, `root`, `at`, `duration` are set for you; `scope()` gives the expression scope. Keyframes, filters, masks and the lifespan work on `target` as for the built-in layers. `awaitFrameAt(local)` is called once per frame by `gotoFrame` (the one path playback and export share), after the timeline has been set to that frame.
3604
+
3406
3605
  ### `ease(name): (p: number) => number`
3407
3606
 
3408
3607
  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`.
@@ -3543,6 +3742,17 @@ const env = await musicEnvelope(music, { frameRate: 30 });
3543
3742
 
3544
3743
  The [audio-reactive](#audio-reactive-audioenvelope-react-bpmenvelope) envelope of an audio layer's [`music`](dsl.md#music-written-as-text-music): it renders the score and analyses that very sound, so `env.series` (`level`, `bass`, `mid`, `treble`, or your `bands`) follows the notes. `env.beats` are exact (the kick hits, or every beat when the score has no kick) and `env.bpm` is the starting tempo. Options are `audioEnvelope`'s (`frameRate`, `bands`, `beatBand`, `beatSensitivity`). Throws, saying why, when the score cannot play (for example no `bpm`). The envelope covers the music and its tail; when the layer starts at `at`, pass `at` to `react()`.
3545
3744
 
3745
+ ### `musicEvents()`
3746
+
3747
+ ```ts
3748
+ import { musicEvents } from 'pixi-effects';
3749
+ const ev = await musicEvents(music); // the same object the audio layer plays; BEFORE movie.init, async like musicEnvelope()
3750
+ ev.tracks[0].notes[0]; // { beat: 0, time: 0, duration: 4, seconds: 2, bar: 1, beatInBar: 1, name: 'Am7', chord: true, midi: [57, 60, 64, 67], velocity: 0.8 }
3751
+ ev.drums[0]; // { kind: 'kick', hits: [{ beat, time, bar, beatInBar, velocity }, ...] }
3752
+ ```
3753
+
3754
+ What a [`music`](dsl.md#music-written-as-text-music) score plays, as data: every note and chord with its `beat` and `time` (seconds from the start of the music, the tempo map applied: add the audio layer's `at` for movie time; swing and humanize are not applied), `duration`, `bar` and `beatInBar` (from `meter`, default 4), the audible `midi` pitches (`transpose` is in), the `name` **as the score wrote it** (a transposed score still says `Am7` while `midi` sounds a tone higher: caption from `midi` if you transpose), and every drum hit with the pattern repeated out. Also `bpm`, `meter`, `beats`, `bars`, `seconds` and per track `beats` / `bars`. Use it to make the picture follow the score: light the keys of the chord that sounds, caption its name, flash on the snare. A score that cannot play throws with the reason; warnings are logged as for the layer.
3755
+
3546
3756
  ### `musicBuffer()`
3547
3757
 
3548
3758
  ```ts
package/llms.txt CHANGED
@@ -18,6 +18,7 @@ Rules that cause most failures (details in the skill and pitfalls):
18
18
  - [Cheatsheet](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/cheatsheet.md): every layer type, prop, default and convention on one page
19
19
  - [Recipes](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/recipes.md): tested building blocks (kinetic type, lower-third, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions, sound effects without files, music written as text, three.js, a presentation deck, visuals that follow music)
20
20
  - [Pitfalls](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/pitfalls.md): real mistakes made by AI authors, with fixes
21
+ - [Directing a film](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/direction.md): for a product film or company intro, before building: brief, style guide, shot list, assets rule, review protocol (any renderer)
21
22
  - [Starter template](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/template.html): copy this file to begin
22
23
 
23
24
  ## Reference
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-effects",
3
- "version": "0.27.2",
3
+ "version": "0.28.1",
4
4
  "description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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.27.2. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
6
+ > Written for pixi-effects 0.28.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
 
@@ -26,6 +26,15 @@ You describe a video as a tree of plain objects; the library plays it in a canva
26
26
  From a browser tool the result is a `data:` URL string: `agent-browser eval "movie.contactSheet({ count: 6, as: 'dataURL' })" | python3 scripts/save-image.py /absolute/sheet.png`, then open the PNG. Sheet tiles are small (~480 px): use `snapshot` for detail. `inspect` output is long on busy scenes: pass `{ layers: 'none' }` and read `issues` first. Give layers a `name` so inspect paths are readable. A moving ticker is not flagged as cut off.
27
27
  6. **Iterate** on what you saw (clipped text, collisions, wrong timing). Fast motion looks choppy: add `motionBlur: true` to `movie.init` (render, snapshot and contact sheet then show a film-style blur; renders take ~8× longer). Then export: `const blob = await movie.render({ format: 'mp4' })` (about real time; `movie.on('progress', …)` reports 0–100). **While you iterate, do not export the whole movie:** `check --no-export --at <the moment you changed>` takes seconds (3 to 10 s on the gallery pieces: most of it is loading the page and measuring the sound); to see motion, `render({ range: [from, to] })` / `pixi-effects-render --range 10:15` / `--scene name` exports only that part (picture and sound); `draft: true` / `--draft` is half size, low quality and no motion blur: a much smaller file (a piece that asks for motion blur renders much faster as a draft: 4 s of `hanabi-night` took 14.0 s, and 8.2 s as a draft, about 7 s of either being the browser's start-up), but the drawing itself is not faster. Export the whole movie once, at the end.
28
28
 
29
+ ## For a film (more than a title card): direct it first
30
+
31
+ **Read `reference/direction.md` before writing a composition for a product film, a company introduction, a launch or a data story.** It is renderer-neutral: write `brief.md` (the one sentence to remember, the REAL assets with paths, what must never be invented; if a needed asset is missing, stop and ask), `style-guide.md` (palette and type taken from the real source, a motion rule per class of object) and `shotlist.md` (a row per beat with entry and exit state) next to the page; then build, then review the rendered frames cold. What is specific to this library:
32
+
33
+ - **Time and sound:** pick a tempo whose bar is a round number of seconds (96 bpm: 2.5 s; 120 bpm: 2 s), put every cut on a bar line and every sfx on its beat. `npx pixi-effects check` prints a `music` read-back (bars per track, what starts on which beat, the drum span): verify the sound with it.
34
+ - **Review:** `check --at 3.5,title@end,50%` writes the contact sheet and single frames; look at mid-transition frames of the exported file too. Frames are seekable and deterministic here: render the same range twice and compare the frames if you doubt it.
35
+ - **Formats:** make the formats the human asked for, one if they did not say. When a vertical cut is wanted it is its own composition: one source, a layout function, `?f=9x16` (recipe `two-formats`); `check` and `render` take `--query f=9x16`.
36
+ - Fit a picture into a rectangle with the `photo()` helper of that recipe (`scale: 'cover'` fits the PARENT only); designed pictures (posters, flyers, screenshots) are shown whole, never cropped.
37
+
29
38
  ## The rules that cause most failures
30
39
 
31
40
  - **Keyframe `at` starts at 0 when the layer appears** (negative = back from the layer's end). A layer's own `at` and a transition's `at` are composition time.
@@ -53,6 +62,7 @@ Centred text must be positioned by `anchorX/anchorY: 0.5`; estimate text width a
53
62
  - `reference/cheatsheet.md` — every layer type, prop, default and rule on one page.
54
63
  - `reference/recipes.md` — tested building blocks: slam type, lower-third, marquee, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions + music, looping, particles, gradients/vignette, three.js metal.
55
64
  - `reference/pitfalls.md` — the full list of real mistakes, with status.
65
+ - `reference/direction.md` — how to direct a film (any renderer): brief, style guide, shot list, assets rule, sound on the beat grid, review protocol, formats, gates.
56
66
  - `npx pixi-effects check` — the one-command review (see Workflow step 5).
57
67
  - `template.html` — the starting file. (It loads the released library from a CDN, so it works from any folder.)
58
68
  - `scripts/save-image.py` — save a `dataURL` result (contact sheet / snapshot) to a PNG.
@@ -57,7 +57,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
57
57
 
58
58
  **audio** — a file: `asset`, `loop`, `volume`, `speed` (the pitch follows; `time` keyframes work as for video; not for `sfx` / `music`: change an sfx's `pitch` instead, or put it in a composition and remap that); `volume` (top level, or in `initial` like every other layer's starting values); fade with volume keyframes (`{ at: -2, to: { volume: 0 }, duration: 2 }` fades the last 2 s). Omit `duration` for a one-shot clip: it lasts exactly as long as the clip. With `loop: true` and no `duration` it lasts until the composition ends. An explicit `duration` longer than the clip goes silent after the clip unless `loop: true`.
59
59
  **Or a sound effect with no file — `sfx`:** `{ type: 'audio', sfx: 'swoosh', at: 2 }` is the whole thing. Presets, default length in s: `click` 0.04 · `pop` 0.12 · `swoosh` 0.5 (loudest 0.16 s in, moves left → right) · `swipe` 0.22 · `hit` 0.7 · `riser` 1 (loudest at its END: `at = hit − duration`) · `chime` 1.4 · `beep` 0.16 · `coin` 0.4 · `glitch` 0.35 · `typewriter` 0.06. Knobs: `sfx: { preset, pitch /* semitones ±24 */, brightness /* −1 dark … 1 bright */, seed /* another take */ }`. **The layer's `duration` IS the sound's length** (omit it; set it to stretch, e.g. a riser; at most 10 s); `volume` is its level (1 = the preset's standard level, peaking at −12 … −18 dBFS by preset; above 1 is allowed: a sparse track of clicks measured −32 LUFS at 0.5 and needed about 2 to reach −24 LUFS). No `loop`: one layer per hit (a JS loop). Custom sound: `sfx: { voices: [{ wave: 'noise' | 'sine' | 'square' | 'saw' | 'triangle', freq: 440 | [from, to], filter: { type: 'lowpass' | 'highpass' | 'bandpass', freq: [from, to] }, envelope: 'fall' | 'bell' | 'swell' | 'hold', pan: [-1, 1] }] }`. The export (mp4 / webm / mov) encodes this same mix (lossy AAC / Opus), on time within a few ms.
60
- **Or music with no file — `music`:** `{ type: 'audio', music: { bpm: 96, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4 Dm7:4 G7:4' }, { inst: 'bass', notes: 'c2:2 g1:2 …' }], drums: { kick: 'x...x...', snare: '....x...', hat: 'x.x.x.x.' } } }` — a tune written as text (notes in LOWERCASE `c4:2` in beats, chords start with a CAPITAL `Am7` `G7` `C@4` `Am7/e` (`g7` is a very high note, not G7), `[c4 e4 g4]`, rest `_`, hold `~`, `!` accent; `|` ignored). Instruments `keys pluck pad bass sub lead bell musicbox`; drums `kick snare hat openhat clap rim tom crash shaker sleigh` as 16th-note step strings (`x` hit, `o` soft, `.` none; a list of `{ from, to, … }` for sections). Score options `bpm` (or `[[beat, bpm], …]` for a ritardando) `swing reverb (0–0.6) humanize drumVol grid transpose tail seed`; track options `vol` (or `[[beat, level], …]` for a fade) `pan step strum legato tone reverb attack release ring transpose`. Lasts its notes + tail, ends with the movie, `loop: true` repeats; peaks −6 dBFS at `volume: 1`. `await musicEnvelope(music, { frameRate })` → an envelope for `react()`; `await musicBuffer(music)` → a Web Audio `AudioBuffer` to play yourself.
60
+ **Or music with no file — `music`:** `{ type: 'audio', music: { bpm: 96, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4 Dm7:4 G7:4' }, { inst: 'bass', notes: 'c2:2 g1:2 …' }], drums: { kick: 'x...x...', snare: '....x...', hat: 'x.x.x.x.' } } }` — a tune written as text (notes in LOWERCASE `c4:2` in beats, chords start with a CAPITAL `Am7` `G7` `C@4` `Am7/e` (`g7` is a very high note, not G7), `[c4 e4 g4]`, rest `_`, hold `~`, `!` accent; `|` ignored). Instruments `keys pluck pad bass sub lead bell musicbox`; drums `kick snare hat openhat clap rim tom crash shaker sleigh` as 16th-note step strings (`x` hit, `o` soft, `.` none; a list of `{ from, to, … }` for sections). Score options `bpm` (or `[[beat, bpm], …]` for a ritardando) `meter` (beats in a bar, default 4: names the bars in `check`'s read-back, no effect on the sound) `swing reverb (0–0.6) humanize drumVol grid transpose tail seed`; track options `vol` (or `[[beat, level], …]` for a fade) `pan step strum legato tone reverb attack release ring transpose`. A plain capital letter is a major chord (`G`, `Bb`, `F#`; `Ebmaj7`, `F#m7`), `E5` is root and fifth only (e3 b3), every chord token is struck again (`~` holds), a track can start late with a rest (`_:32 e4:1 …`), `f#3` / `bb2` are sharps and flats, c4 is middle C, chord names are voiced around octave 3 (good ranges for notes: bass c1–c3, keys / pluck c3–c6, pad c2–c5, lead c4–c6, bell c5–c7). **Drums repeat by themselves** (one bar is enough) and `drums: { from: 32, to: 64, … }` counts BEATS, not bars (`drums` is one object, or a list of them for sections). `check` reads the score back (bars per track, what starts in each bar and on which beat: `Am(2)`; drum span): trust that over counting by hand; `await musicEvents(music)` gives the same as data. Lasts its notes + tail, ends with the movie, `loop: true` repeats; peaks −6 dBFS at `volume: 1`. `await musicEnvelope(music, { frameRate })` → an envelope for `react()`; `await musicBuffer(music)` → a Web Audio `AudioBuffer` to play yourself.
61
61
 
62
62
  **shape** — `shape: 'rect' | 'circle' | 'ellipse' | 'arc' | 'line' | 'polygon' | 'path'`:
63
63