pixi-effects 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +5 -5
- package/ai/chat-template.html +3 -3
- package/ai/tools/check.mjs +26 -7
- package/ai/tools/lookup.mjs +14 -2
- package/dist/{Base-CXsMdmJB.d.ts → Base-CdF7AJop.d.ts} +1 -1
- package/dist/{Base-pNq6sVGl.d.cts → Base-CmiFiwQM.d.cts} +1 -1
- package/dist/Composition-BMRCE5FH.js +7 -0
- package/dist/{Composition-NDXKQHB4.js.map → Composition-BMRCE5FH.js.map} +1 -1
- package/dist/Composition-XRLYR6GL.cjs +16 -0
- package/dist/{Composition-OLH7RC5Z.cjs.map → Composition-XRLYR6GL.cjs.map} +1 -1
- package/dist/Controller.cjs +130 -1
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +24 -2
- package/dist/Controller.d.ts +24 -2
- package/dist/Controller.js +130 -1
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-f0tQG9y5.d.ts → Movie-Bh1HPdKw.d.cts} +30 -2
- package/dist/{Movie-CL6SsAWe.d.cts → Movie-D5WFfZdZ.d.ts} +30 -2
- package/dist/{Spans-QRMN2EZO.cjs → Spans-4IRBSOLI.cjs} +11 -11
- package/dist/{Spans-QRMN2EZO.cjs.map → Spans-4IRBSOLI.cjs.map} +1 -1
- package/dist/{Spans-QHHWC2PB.js → Spans-MWP2FX3T.js} +6 -6
- package/dist/{Spans-QHHWC2PB.js.map → Spans-MWP2FX3T.js.map} +1 -1
- package/dist/{blendModes-WIUCYIMC.cjs → blendModes-KU7XLV7U.cjs} +5 -5
- package/dist/{blendModes-WIUCYIMC.cjs.map → blendModes-KU7XLV7U.cjs.map} +1 -1
- package/dist/{blendModes-PP5NPBXY.js → blendModes-OOI3T6BE.js} +4 -4
- package/dist/{blendModes-PP5NPBXY.js.map → blendModes-OOI3T6BE.js.map} +1 -1
- package/dist/{chunk-ITDYFBB6.js → chunk-2JFFKHXO.js} +5 -5
- package/dist/chunk-2JFFKHXO.js.map +1 -0
- package/dist/{chunk-GL2AQFLQ.cjs → chunk-42ML5AKN.cjs} +5 -5
- package/dist/chunk-42ML5AKN.cjs.map +1 -0
- package/dist/{chunk-XV4L72FW.js → chunk-AZ6AINSW.js} +5 -5
- package/dist/{chunk-XV4L72FW.js.map → chunk-AZ6AINSW.js.map} +1 -1
- package/dist/{chunk-FVZEBYLW.cjs → chunk-CC7SUR7Q.cjs} +237 -54
- package/dist/chunk-CC7SUR7Q.cjs.map +1 -0
- package/dist/{chunk-Y253JQOH.js → chunk-G3DX65HO.js} +110 -10
- package/dist/chunk-G3DX65HO.js.map +1 -0
- package/dist/{chunk-LEECUCTG.cjs → chunk-KJSEQVBV.cjs} +129 -29
- package/dist/chunk-KJSEQVBV.cjs.map +1 -0
- package/dist/{chunk-IMQJVRMT.cjs → chunk-NEUPEENI.cjs} +5 -5
- package/dist/{chunk-IMQJVRMT.cjs.map → chunk-NEUPEENI.cjs.map} +1 -1
- package/dist/{chunk-67LG7MN5.js → chunk-VF56PRAA.js} +205 -22
- package/dist/chunk-VF56PRAA.js.map +1 -0
- package/dist/{chunk-RTJD3ZSW.js → chunk-WIIU42FH.js} +4 -4
- package/dist/{chunk-RTJD3ZSW.js.map → chunk-WIIU42FH.js.map} +1 -1
- package/dist/{chunk-5IJENZOG.cjs → chunk-Y5UWFH6Y.cjs} +91 -91
- package/dist/{chunk-5IJENZOG.cjs.map → chunk-Y5UWFH6Y.cjs.map} +1 -1
- package/dist/index.cjs +33 -33
- package/dist/index.d.cts +20 -6
- package/dist/index.d.ts +20 -6
- package/dist/index.js +5 -5
- package/dist/loader.css +1 -1
- package/dist/present.cjs +5 -5
- package/dist/present.d.cts +2 -2
- package/dist/present.d.ts +2 -2
- package/dist/present.js +5 -5
- package/dist/three.cjs +6 -6
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +2 -2
- package/dist/{types-CavnUvd5.d.ts → types-B6zDTU2V.d.cts} +14 -1
- package/dist/{types-CavnUvd5.d.cts → types-B6zDTU2V.d.ts} +14 -1
- package/docs/api.md +15 -4
- package/docs/dsl.md +7 -5
- package/llms-full.txt +95 -18
- package/package.json +1 -1
- package/skills/pixi-effects/SKILL.md +3 -3
- package/skills/pixi-effects/reference/cheatsheet.md +7 -6
- package/skills/pixi-effects/reference/pitfalls.md +3 -0
- package/skills/pixi-effects/reference/recipes.md +60 -0
- package/skills/pixi-effects/template.html +6 -6
- package/dist/Composition-NDXKQHB4.js +0 -7
- package/dist/Composition-OLH7RC5Z.cjs +0 -16
- package/dist/chunk-67LG7MN5.js.map +0 -1
- package/dist/chunk-FVZEBYLW.cjs.map +0 -1
- package/dist/chunk-GL2AQFLQ.cjs.map +0 -1
- package/dist/chunk-ITDYFBB6.js.map +0 -1
- package/dist/chunk-LEECUCTG.cjs.map +0 -1
- package/dist/chunk-Y253JQOH.js.map +0 -1
package/docs/dsl.md
CHANGED
|
@@ -131,7 +131,7 @@ A text layer has one animatable number, `value`, printed wherever the text conta
|
|
|
131
131
|
}
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
`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.
|
|
134
|
+
`resolution` (1 to 8, default the renderer's own) draws the text at that many times the pixels: text is drawn once at its own size and then stretched by its parents, so text inside a group scaled up to 3x is soft unless it has `resolution: 3`. `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 }`) and `time` (print the value, in SECONDS, as a clock: `'hh:mm:ss'`, `'h:mm:ss'`, `'hh:mm'`, `'mm:ss'` or `'m:ss'`; the seconds are cut, not rounded, and the hours do not wrap, so a day clock keeps the value inside 0–86399 with `mod`: `{ value: 15000 }` with `format: { time: 'hh:mm' }` is `04:10`). Put prefixes and suffixes in the text (`'${value}'`, `'{value}%'`). If `value` is animated but the text has no `{value}`, a warning says so.
|
|
135
135
|
|
|
136
136
|
**Colour inside one text layer: `spans`.** Instead of `text`, write an array of strings (drawn in the layer's own `style`) and `{ text, fill, … }` objects; they are shown one after the other, and `\n` inside a span starts a new line:
|
|
137
137
|
|
|
@@ -145,7 +145,7 @@ A span may change `fill`, `fontWeight`, `fontStyle`, `fontVariant`, `letterSpaci
|
|
|
145
145
|
|
|
146
146
|
### `image`
|
|
147
147
|
|
|
148
|
-
Renders a [PIXI.Sprite](https://pixijs.com/8.x/guides/components/scene-objects/sprite/sprite) from a registered asset. Intrinsic `w`/`h` come from the loaded texture.
|
|
148
|
+
Renders a [PIXI.Sprite](https://pixijs.com/8.x/guides/components/scene-objects/sprite/sprite) from a registered asset. Intrinsic `w`/`h` come from the loaded texture. `scaleMode: 'nearest'` draws the pixels as hard squares when the picture is scaled up (pixel art, a sprite drawn small and shown big); the default `'linear'` smooths. It belongs to the asset: every layer that shows it gets it.
|
|
149
149
|
|
|
150
150
|
```ts
|
|
151
151
|
{
|
|
@@ -268,7 +268,7 @@ A tune with no audio file: notes, chords and drum patterns written as strings, p
|
|
|
268
268
|
} }
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
-
**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
|
|
271
|
+
**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; 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.
|
|
272
272
|
|
|
273
273
|
**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.
|
|
274
274
|
|
|
@@ -348,7 +348,9 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
|
|
|
348
348
|
| `arc` | `radius` | `innerRadius`, `startAngle` (0), `endAngle` (360) |
|
|
349
349
|
| `line` | `from: [x,y]`, `to: [x,y]` (canvas coordinates; stroke in `initial`) | `trimStart`, `trimEnd` |
|
|
350
350
|
| `polygon` | `points: [[x,y], …]` | `open` (default false), `trimStart`, `trimEnd` |
|
|
351
|
-
| `path` | `d` (SVG path data) | `trimStart`, `trimEnd`, `morphTo`, `morph`, `morphPoints` |
|
|
351
|
+
| `path` | `d` (SVG path data) | `fillRule`, `trimStart`, `trimEnd`, `morphTo`, `morph`, `morphPoints` |
|
|
352
|
+
|
|
353
|
+
**Holes (`fillRule: 'evenodd'`) on a `path`.** Several sub-paths in one `d` are each filled on their own: a donut written as two circles is a solid disc. With `fillRule: 'evenodd'` the biggest sub-path is the shape and every other one is a hole cut out of it (the stroke goes round the shape and each hole): `{ shape: 'path', fillRule: 'evenodd', d: 'M 0 -100 A 100 100 0 1 1 0 100 A 100 100 0 1 1 0 -100 Z M 0 -50 A 50 50 0 1 0 0 50 A 50 50 0 1 0 0 -50 Z', initial: { x: 640, y: 360, fillColor: '#ffcc00' } }`. Write the sub-paths in absolute coordinates (each starts with `M`); an island inside a hole is not drawn, and a path that morphs (`morphTo`) has no holes.
|
|
352
354
|
|
|
353
355
|
**`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.
|
|
354
356
|
|
|
@@ -366,7 +368,7 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
|
|
|
366
368
|
keyframes: [{ at: 0, to: { trimEnd: 1 }, duration: 1, ease: 'power2.inOut' }] } // a check mark drawing itself
|
|
367
369
|
```
|
|
368
370
|
|
|
369
|
-
**Morph (`morphTo` / `morph`).** A `path` can turn into another outline: give `morphTo` (SVG path data in the same canvas coordinates as `d`) and animate `morph` from 0 (the `d` outline) to 1 (the `morphTo` outline). In between it is drawn as a polygon of points half way from one to the other (`morphPoints`: 8–2048, default 128). Closed outlines are lined up (start point and direction) so they do not twist; sub-paths are paired in order, and an extra one grows from (or shrinks into) its own centre. Fill and stroke both follow, and `trimStart` / `trimEnd` walk the morphing outline. Write both outlines as `M … L … C … Z` in absolute coordinates (arcs and holes work too, but keep the same number of sub-paths on both sides).
|
|
371
|
+
**Morph (`morphTo` / `morph`).** (Two outlines with the **same number of corners**, such as two quads, are paired corner to corner, so every in-between picture is a straight-edged quad and no corner is rounded; `morphPoints` only matters when the corner counts differ or the outlines are curves.) A `path` can turn into another outline: give `morphTo` (SVG path data in the same canvas coordinates as `d`) and animate `morph` from 0 (the `d` outline) to 1 (the `morphTo` outline). In between it is drawn as a polygon of points half way from one to the other (`morphPoints`: 8–2048, default 128). Closed outlines are lined up (start point and direction) so they do not twist; sub-paths are paired in order, and an extra one grows from (or shrinks into) its own centre. Fill and stroke both follow, and `trimStart` / `trimEnd` walk the morphing outline. Write both outlines as `M … L … C … Z` in absolute coordinates (arcs and holes work too, but keep the same number of sub-paths on both sides).
|
|
370
372
|
|
|
371
373
|
```ts
|
|
372
374
|
{ type: 'shape', shape: 'path', d: 'M 640 160 C 780 160 860 260 860 360 C 860 470 770 560 640 560 C 510 560 420 470 420 360 C 420 250 500 160 640 160 Z',
|
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.
|
|
9
|
+
> Written for pixi-effects 0.36.0. 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
|
|
|
@@ -21,10 +21,10 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
21
21
|
**Or by hand: run it and read the logs first.** `window.__ready` must be `true` and `window.__logs` should be `[]`. A pixi-effects warning is an instruction: fix what it names.
|
|
22
22
|
5. **Look at it** — you cannot judge motion from code. The library has the tools built in:
|
|
23
23
|
- `await movie.contactSheet({ count: 6, as: 'dataURL' })` → ONE image of several labelled frames (pass `frames: [...]` / `times: [...]` to choose; include the middle of every transition and the last second). Save it and view it.
|
|
24
|
-
- `await movie.inspect(frame)` → every layer's canvas bounds and visibility, plus `issues` (text off the canvas, cut off by an edge, empty,
|
|
24
|
+
- `await movie.inspect(frame)` → every layer's canvas bounds and visibility, plus `issues` (text off the canvas, cut off by an edge, empty, overlapping other text, or hidden / struck through by a picture: an opaque shape or image drawn OVER part of it, or a thin line drawn over its body; a shadow, a glow, a ring or a backdrop under it is not reported). Run it at several frames; fix every issue it names.
|
|
25
25
|
- `await movie.snapshot(frame, { as: 'dataURL' })` → one frame, canvas only (no player bar).
|
|
26
26
|
- `movie.inspectAudio()` → **hear with numbers**: for every sound its `start`, `sound.length`, `sound.peakDb`, `sound.loudestAt` (line this up with the picture) and `sound.brightnessHz` (< 500 dark, > 8000 airy), plus `issues` (limited mix, inaudible or cut-off sounds). Check it whenever the movie has audio; `render()` encodes this same mix.
|
|
27
|
-
- **A moment, a movement, the sound (`pixi-effects check` options):** `--at 3.5,title@end,50%,f120` writes those pictures to `check-out/<name>/frames/*.png` (and `at.png`): the last frame of a layer is `name@end`, the middle `name@mid`. `--onion 1:3` writes `onion.png`, eight frames of seconds 1–3 overlaid (the later the stronger): a trail that shows a movement's path and easing (even steps = constant speed, bunched = slow). `report.audio` now has `loudness` (`integratedLufs`, `truePeakDb`, `clippedSamples`, `silences`), `scenes` (per named top-level composition), `cues` (when each sound starts) and `notes` (advice: quiet or loud for web video, little headroom, silent stretches; web video is typically −14 to −16 LUFS); `waveform.png` draws the mix with the scene edges and the cues. A web font that failed to load fails the check; a text layer none of whose fonts is installed is listed for review.
|
|
27
|
+
- **A moment, a movement, the sound (`pixi-effects check` options):** `--at 3.5,title@end,50%,f120` writes those pictures to `check-out/<name>/frames/*.png` (and `at.png`; with `--out DIR` they land in `DIR/frames/` and `DIR/at.png`, there is no `<name>/` inside DIR); add `--crop X,Y,W,H` (canvas pixels) to get each picture as just that part, enlarged up to 4x, so a 40 px figure, a fine line or a texture can be judged (the contact sheet tiles are too small for that): the last frame of a layer is `name@end`, the middle `name@mid`. `--onion 1:3` writes `onion.png`, eight frames of seconds 1–3 overlaid (the later the stronger): a trail that shows a movement's path and easing (even steps = constant speed, bunched = slow). `report.audio` now has `loudness` (`integratedLufs`, `truePeakDb`, `clippedSamples`, `silences`), `scenes` (per named top-level composition), `cues` (when each sound starts) and `notes` (advice: quiet or loud for web video, little headroom, silent stretches; web video is typically −14 to −16 LUFS); `waveform.png` draws the mix with the scene edges and the cues. A web font that failed to load fails the check; a text layer none of whose fonts is installed is listed for review.
|
|
28
28
|
- **A deck** (`composition.stops`, `deck()`): `await movie.contactSheet({ frames: movie.stops.map(s => s.frame), as: 'dataURL' })` is one picture of every stop (the check writes it as `stops.png`); what the audience reads while a page waits is that picture, so check every one. `await inspectStops(movie)` (from `pixi-effects/present`) lists the stops where the picture is still changing (a stop landing before its animation ends); the check prints it as a review list: move the stop later or flag a settled one `pdf: true`. `await stopImages(movie, { which: 'pages' })` / `exportPDF(movie)` give the pages themselves.
|
|
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.
|
|
@@ -120,9 +120,10 @@ A video is **data**: a tree of plain objects. Everything below is a JS object yo
|
|
|
120
120
|
| Angles | **degrees**: `rotation`, `skew*`, `rotationX`, `rotationY` |
|
|
121
121
|
| Depth (`z`) | `+z` toward the viewer (bigger, nearer) |
|
|
122
122
|
| Numbers | any number may be an **expression string**: `'GW/2 - w/2'`, `'min(W,H)*0.4'` (build-time only; no per-frame time variable). Functions: `min max abs floor ceil round sqrt pow sin cos tan lerp clamp smoothstep mod step`, `PI`, and **seeded** `rand(seed)` (0…1) / `noise(x, seed)` (−1…1): give each layer its own seed, never `Math.random()`. **Relative values** work in a keyframe's `to` / `from` / `set` (not in `initial`, a style, or the start of a `from`+`to`): `'+=36'` / `'-=36'` are measured from where the layer stands, and the right side is an expression (`'-=GW*0.1'`). A slide-in is `from: { x: '-=36' }`, a nudge `to: { y: '+=40' }`; keyframes one after another stack; with `repeat` the same move replays from the same start (it does not stack). Expressions work in layer `x y` and the other numeric props, **not** in `line.from / to`, `polygon.points` or `path.d`: compute those in JS (`const W = 1920`) |
|
|
123
|
-
| Colours | `'#rrggbb'` strings or numbers; text colour is `style.fill
|
|
123
|
+
| Colours | `'#rrggbb'` strings or numbers; text colour is `style.fill` (or `initial: { fill }`; a `fill` keyframe animates it), shape colour is `fillColor`/`strokeColor`, image colour is `tint` |
|
|
124
124
|
| Z-order | **array order**, later = on top. A layer is hidden (not removed) outside `[at, at+duration)` and keeps its last values |
|
|
125
125
|
| Time of a sequence's `at` | seconds from the start of its **parent composition** (default 0) |
|
|
126
|
+
| `from` before its `at` | a keyframe's `from` holds **from the start of the layer**, not from its `at` (so a title waits invisible with `from: { alpha: 0 }`), and it replaces `initial`: `initial: { alpha: 0 }` with `{ at: 2, from: { alpha: 1 }, … }` shows the layer at alpha 1 from 0 s. To appear only at 2 s give the layer `at: 2`, or start from the same value in both |
|
|
126
127
|
| Time of a keyframe's `at` | seconds from the start of **its own layer**; **negative = back from the layer's end** |
|
|
127
128
|
| Time of a transition's `at` | seconds from the start of the **parent composition**; negative = back from its end |
|
|
128
129
|
| `duration` | defaults to the parent composition's duration |
|
|
@@ -160,9 +161,9 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
160
161
|
|
|
161
162
|
## Layer types
|
|
162
163
|
|
|
163
|
-
**text** — `text`, `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */ }`) — that is how counters work. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change). **Colour inside one line:** `spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'a'", fill: '#ffd166', fontWeight: '700' }, ',\n next line']` instead of `text` (strings use the layer's `style`; a span changes only fill / fontWeight / fontStyle / fontVariant / letterSpacing / dropShadow, never a size, font or stroke (those are the layer's `style`); `\n` starts a line; `style.align: 'left'` for code; `visibleChars` types over all the spans; it becomes a group of text layers `<name>.0…`).
|
|
164
|
+
**text** — `text`, `resolution` (1–8: draw at N× the pixels so text inside a parent scaled up N× stays sharp), `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */, time: 'hh:mm:ss'|'h:mm:ss'|'hh:mm'|'mm:ss'|'m:ss' /* the value in SECONDS as a clock */ }`) — that is how counters work. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change). **Colour inside one line:** `spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'a'", fill: '#ffd166', fontWeight: '700' }, ',\n next line']` instead of `text` (strings use the layer's `style`; a span changes only fill / fontWeight / fontStyle / fontVariant / letterSpacing / dropShadow, never a size, font or stroke (those are the layer's `style`); `\n` starts a line; `style.align: 'left'` for code; `visibleChars` types over all the spans; it becomes a group of text layers `<name>.0…`).
|
|
164
165
|
|
|
165
|
-
**image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'
|
|
166
|
+
**image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'`, `scaleMode: 'nearest'` (hard square pixels when scaled up: pixel art; default `'linear'`, smooth; it belongs to the asset). Default anchor top-left; natural size = `w`,`h`.
|
|
166
167
|
|
|
167
168
|
**video** — `asset`, `loop`, `audio`, `volume`, **`speed`**; `initial: { scale: 'cover' }`. `speed: 0.5` slow motion, `speed: -1` backward (it starts at |speed| × `duration`, so a 3 s file with no `duration` plays whole, backward); with no `duration` the layer lasts the file's length ÷ |speed|. For anything that is not one straight speed — a freeze, a ramp, a loop — animate **`time`** (seconds in the file) like any property: `keyframes: [{ at: 0, from: { time: 0 }, to: { time: 2 }, duration: 1 }, { at: 1, to: { time: 2 }, duration: 0.5 }, { at: 1.5, to: { time: 6 }, duration: 3, ease: 'power2.inOut' }]` (play 2 s, hold, then accelerate). `repeat` loops it, `yoyo` ping-pongs. `speed` itself is not animatable (it warns): animate `time`. The sound follows (pitch too, like a tape).
|
|
168
169
|
|
|
@@ -180,9 +181,9 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
180
181
|
| arc | `radius innerRadius startAngle endAngle` — degrees, **0° = 3 o'clock, clockwise**; centred on `x, y`; stroke only = an open arc line (`strokeCap: 'round'`), with a fill = a sector, with `innerRadius` = a donut slice; a sweep ≥ 360° = a full circle. A progress ring: `startAngle: -90` and animate `endAngle` from −90 to 270 |
|
|
181
182
|
| line | `from: [x,y] to: [x,y]` — plain canvas coordinates (omit `x,y`; if you give them they place the line's midpoint). Stroke in `initial`: `strokeColor`, `strokeWidth` |
|
|
182
183
|
| polygon | `points: [[x,y],…] open` — canvas coordinates like `line` |
|
|
183
|
-
| path | `d` (SVG path data) — canvas coordinates like `line
|
|
184
|
+
| path | `d` (SVG path data) — canvas coordinates like `line`; leave `x` / `y` out and it sits where `d` says; give them (even `y: 0`) and they place its MIDDLE, not an offset (measured: `y: 0` put a square's centre on the top edge). **Holes:** several sub-paths are each filled alone (a donut written as two circles is a disc) unless `fillRule: 'evenodd'`: the biggest is the shape, the others are holes. **Morph:** `morphTo: 'M…Z'` + animate `morph` 0 → 1 (points half way between, closed outlines lined up so they do not twist; `morphPoints` 8–2048; two outlines with the same number of corners are paired corner to corner, so a quad stays a sharp quad) |
|
|
184
185
|
|
|
185
|
-
**Draw-on:** every shape but `arc` has `trimStart` / `trimEnd` (0–1 of the outline, default 0 / 1, animatable): `trimEnd: 0` + keyframe `to: { trimEnd: 1 }` draws a line / border / path on; `trimStart` 0 → 1 after it wipes it off; both = a travelling dash. The fill is NOT trimmed. Rect outline starts top-left, circle / ellipse at 12 o'clock, both clockwise. A `path` with several sub-paths walks them in order as one length; `trimEach: true` trims each on its own (all draw on together). Use `strokeCap: 'round'`. **Dashes:** there is no dash option; write the outline as alternating sub-paths (`d: 'M 0 0 L 10 0 M 20 0 L 30 0 …'`, or sample the curve) and `trimEnd` 0 → 1 walks them in order, so the dashes draw on one after another (as in `
|
|
186
|
+
**Draw-on:** every shape but `arc` has `trimStart` / `trimEnd` (0–1 of the outline, default 0 / 1, animatable): `trimEnd: 0` + keyframe `to: { trimEnd: 1 }` draws a line / border / path on; `trimStart` 0 → 1 after it wipes it off; both = a travelling dash. The fill is NOT trimmed. Rect outline starts top-left, circle / ellipse at 12 o'clock, both clockwise. A `path` with several sub-paths walks them in order as one length; `trimEach: true` trims each on its own (all draw on together). Use `strokeCap: 'round'`. **Dashes:** there is no dash option; write the outline as alternating sub-paths (`d: 'M 0 0 L 10 0 M 20 0 L 30 0 …'`, or sample the curve) and `trimEnd` 0 → 1 walks them in order, so the dashes draw on one after another (as in `route-atlas`).
|
|
186
187
|
|
|
187
188
|
Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha strokeWidth`; `colorSpace: 'oklab'|'oklch'` for clean colour tweens. rect/circle/ellipse are **centred on `x,y` by default** (`anchorX/anchorY` default 0.5): for a bar growing from its base use `anchorY: 1` (or `anchorX: 0` for left-to-right). **`fillGradient`** (top level or `initial`; instead of `fillColor`): `{ type?: 'linear'|'radial', stops: [[0, '#000'], [1, 'rgba(0,0,0,.6)']], angle? /* linear, deg; DEFAULT 90 = top→bottom, 0 = left→right: a wide gradient slid sideways as a matte needs angle: 0 */, center?, innerRadius?, radius? /* radial, 0–1 of the SHAPE's own bounds, not the canvas: a glow on a 1500 px circle is placed with fractions of that circle */ }` — stops may have alpha, so a radial transparent→dark is a vignette. **Animate it** with a partial gradient in a keyframe (`to: { fillGradient: { angle: 360, stops: [...] } }`: keys you leave out stay; the same number of stops; `type` cannot change; a start gradient is required; use `colorSpace: 'oklch'` for a vivid fade). A text layer takes `fillGradient` too (LINEAR only: angle and stops; type / center / radius are ignored with a warning; letters re-rasterised per change). `from` alone runs from that gradient to the layer's own. The names `'fillGradient.angle'` / `gradientAngle` are not properties: write `fillGradient: { angle }`. Stroke ends and corners: `strokeCap: 'butt'|'round'|'square'`, `strokeJoin: 'miter'|'round'|'bevel'` (top level or in `initial`; not animated — `round` keeps sharp corners like an M from poking out).
|
|
188
189
|
|
|
@@ -265,7 +266,7 @@ measureText('Hello', style) // → { width, height,
|
|
|
265
266
|
splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
|
|
266
267
|
// → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
|
|
267
268
|
```
|
|
268
|
-
Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
269
|
+
Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, vertical: true /* tategaki: down a column, new line = new column to the left, x = RIGHT edge of the first column; 、。 ー 「」 placed and turned for you */, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
269
270
|
|
|
270
271
|
## Assets
|
|
271
272
|
|
|
@@ -438,6 +439,9 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
438
439
|
109. **No font is bundled, and each script needs a font that has its characters.** The machine's own fonts are used; characters a font lacks are drawn with whatever the machine falls back to, so the same page can look different on another machine. Load a web font that covers the script (Noto Sans JP, Noto Sans KR for Hangul, Noto Sans SC) with `await document.fonts.load('40px "Noto Sans KR"')` before `movie.init`, and say it in `style.fontFamily` (pitfall 103). `check` lists a layer none of whose fonts is available, but a family that nothing installs or loads is silently replaced by the next one in the list (or the generic name) without a word: the first name in `fontFamily` is not proof it was used.
|
|
439
440
|
110. **Right-to-left text: three traps.** `splitText` / `animateText` order words left to right, so Arabic or Hebrew comes out reversed (split by words yourself, from the right edge, with `measureText` of the logical prefix); a mark below the font box can be clipped (the dots of an Arabic ي) unless the layer has `style.padding`; `check` sees neither. `bulgePinch.center` is 0–1 of the layer, not pixels (pitfalls elsewhere say canvas pixels for the other centres: `twist`, `zoomBlur`, `shockwave`). `npx pixi-effects help arabic` and `help bulgePinch` print the sections.
|
|
440
441
|
111. **Only some named filters are tested on both backends.** `glow`, `outline`, `dropShadow`, `bevel`, `bloom`, `pixelate`, `crt`, `rgbSplit`, `dot`, `radialBlur` and `glitch` look the same on WebGPU (the default) and WebGL; `multiColorReplace` draws everything black on WebGPU, `kawaseBlur` is far weaker and `advancedBloom` sharper there: use `colorReplace`, `blur`, `bloom`. A filter follows the edges of the layer's own picture: `glow` on a full-screen opaque layer shows nothing (put it on the shapes and the text).
|
|
442
|
+
112. **Text inside a parent that is scaled up comes out soft.** Text is drawn once at its own size and then stretched, so `{ type: 'null', initial: { scale: 3 } }` with a text child of `fontSize: 30` is blurry (shapes stay sharp). Give the text `resolution: 3` (it is drawn at 3 times the pixels, for a parent that grows to 3x; 1 to 8, the cost is the memory), or write it at the size it is shown (`fontSize: 90`, parent scale 1), or let the parent only scale DOWN (a pull-back from big to normal is sharp the whole way). `check --at … --crop` shows the difference.
|
|
443
|
+
113. **Two limits on sound that `help sfx` / `help music` did not say.** An `sfx` lasts at most 10 s (`MAX_LENGTH` in the synth): for a longer rain or room tone, chain two audio layers that overlap by a moment, or use `loop` on a short one. A `music` layer plays at most 16 tracks, and a warning says that only the first 16 play: put the rest in a second audio layer.
|
|
444
|
+
114. **A keyframe's `from` is on screen from the start of the layer, and it beats `initial`.** `initial: { alpha: 0 }` plus `{ at: 2, from: { alpha: 1 }, to: { alpha: 0.5 }, duration: 1 }` shows the layer at full alpha from second 0 (measured in Chrome: the red square was there at 0.5 s). That is how a title waits invisible with `from: { alpha: 0 }` until its `at`, but it surprises when `from` is the visible end. Give the layer its own `at: 2`, or use `set` at the moment it should appear. No warning: the same pattern (`initial` = the resting value, `from` = where it starts) is what the recipes do everywhere.
|
|
441
445
|
|
|
442
446
|
|
|
443
447
|
<!-- ===== skills/pixi-effects/reference/recipes.md ===== -->
|
|
@@ -589,6 +593,55 @@ return [
|
|
|
589
593
|
|
|
590
594
|
---
|
|
591
595
|
|
|
596
|
+
## Push in on a whole flat scene (the 2D "camera"): a null that scales, and text drawn big enough
|
|
597
|
+
|
|
598
|
+
There is no second camera for flat scenes: the camera of the 2.5D layers is for `threeD` layers. For a push-in or pull-back of an ordinary 2D scene, put everything in a `null` layer that sits at the middle of the canvas and scale THAT (children use `parent: 'world'`, and their x / y are measured from the null's origin, so the middle of the canvas is 0, 0). Text is drawn once at its own size and then stretched, so give every text `resolution: R` where R is the biggest scale the group reaches (R 1–8); otherwise it goes soft.
|
|
599
|
+
|
|
600
|
+
```js
|
|
601
|
+
// @recipe scene-zoom
|
|
602
|
+
const R = 2; // the group grows to 2x: its text is drawn at 2x pixels
|
|
603
|
+
const inWorld = (spec) => ({ parent: 'world', ...spec });
|
|
604
|
+
return [
|
|
605
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0f1424' } },
|
|
606
|
+
{ type: 'null', name: 'world', initial: { x: 'GW/2', y: 'GH/2' },
|
|
607
|
+
keyframes: [{ at: 0.5, from: { scale: 1 }, to: { scale: R }, duration: 3.5, ease: 'power2.inOut' }] },
|
|
608
|
+
inWorld({ type: 'shape', shape: 'circle', radius: 120, initial: { x: 0, y: 0, fillColor: '#3b5bdb' } }),
|
|
609
|
+
inWorld({ type: 'shape', shape: 'circle', radius: 40, initial: { x: 260, y: -120, fillColor: '#ffd166' } }),
|
|
610
|
+
inWorld({ type: 'text', text: 'Zoom in', resolution: R, style: { fontSize: 48, fontWeight: 'bold', fill: '#ffffff' }, initial: { x: 0, y: 0, anchorX: 0.5, anchorY: 0.5 } }),
|
|
611
|
+
inWorld({ type: 'text', text: 'sharp at 2x', resolution: R, style: { fontSize: 22, fill: '#c9d3f5' }, initial: { x: 0, y: 150, anchorX: 0.5, anchorY: 0.5 } }),
|
|
612
|
+
];
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Parallax: layers that scroll past at their own speed (a window of a train, a side-scroller)
|
|
618
|
+
|
|
619
|
+
Each layer is a long strip of repeated shapes that moves left at a constant speed, `speed × duration` pixels in all, so it must be that long plus the width of the screen. The speed is the depth: the near strip fast, the far one slow. A strip that is longer than it needs to be is only more layers.
|
|
620
|
+
|
|
621
|
+
```js
|
|
622
|
+
// @recipe parallax-strips
|
|
623
|
+
const D = 8, W = 1280;
|
|
624
|
+
// one strip: `count` shapes `gap` px apart, scrolling left at `speed` px/s from x = 0 for D seconds
|
|
625
|
+
const strip = (name, speed, gap, y, size, color) => {
|
|
626
|
+
const count = Math.ceil((speed * D + W) / gap) + 1;
|
|
627
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
628
|
+
type: 'shape', shape: 'rect', name: `${name}-${i}`, width: size, height: size * 2,
|
|
629
|
+
initial: { x: i * gap, y, fillColor: color },
|
|
630
|
+
keyframes: [{ at: 0, to: { x: i * gap - speed * D }, duration: D, ease: 'none' }],
|
|
631
|
+
}));
|
|
632
|
+
};
|
|
633
|
+
return [
|
|
634
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#1b2a4a' } },
|
|
635
|
+
...strip('far', 30, 240, 420, 60, '#26365c'), // distant hills: 30 px/s
|
|
636
|
+
...strip('mid', 120, 200, 500, 40, '#314a7a'), // 120 px/s
|
|
637
|
+
...strip('near', 480, 360, 600, 24, '#0b1220'), // poles: 480 px/s
|
|
638
|
+
];
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
To land something exactly where you want at a time `t` (a moon under a bridge at 6 s), compute the strip's x at `t` the same way: `i * gap - speed * t`.
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
592
645
|
## A ticker that scrolls out completely (`w` = the text's own width)
|
|
593
646
|
|
|
594
647
|
`'-w'` is the exact x at which the text has left the screen on the left (`w` is measured after the style is applied).
|
|
@@ -1040,6 +1093,17 @@ return [
|
|
|
1040
1093
|
];
|
|
1041
1094
|
```
|
|
1042
1095
|
|
|
1096
|
+
**Vertical Japanese (tategaki)** is one option: `vertical: true`. The characters go down a column and a new line is a new column to the LEFT, so `x` is the RIGHT edge of the first column. The commas sit at the upper right of their cell, and the long vowel mark and the brackets are turned a quarter, as in print.
|
|
1097
|
+
|
|
1098
|
+
```js
|
|
1099
|
+
// @recipe vertical-text
|
|
1100
|
+
const style = { fontFamily: 'Hiragino Mincho ProN, Yu Mincho, serif', fontSize: 56, fill: '#1c1a17' };
|
|
1101
|
+
return [
|
|
1102
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#efe9dc' } },
|
|
1103
|
+
...animateText('雨のあと、\nコーヒーは「ゆっくり」', style, { x: 900, y: 70, vertical: true, at: 0.3, duration: 5, in: 'fade', stagger: { each: 0.04 } }),
|
|
1104
|
+
];
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1043
1107
|
Presets for `in` / `out`: `rise` (default) `drop` `fade` `pop` `zoom` `slide` `spin`; or `{ from: { x: -30, scale: 0.5, rotation: 20 }, duration: 0.8, ease: 'power3.out' }` (`x` / `y` in `from` are offsets from the resting place, the rest are absolute values). `idle` drifts each piece between the entrance and the exit.
|
|
1044
1108
|
|
|
1045
1109
|
---
|
|
@@ -1948,7 +2012,7 @@ A text layer has one animatable number, `value`, printed wherever the text conta
|
|
|
1948
2012
|
}
|
|
1949
2013
|
```
|
|
1950
2014
|
|
|
1951
|
-
`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.
|
|
2015
|
+
`resolution` (1 to 8, default the renderer's own) draws the text at that many times the pixels: text is drawn once at its own size and then stretched by its parents, so text inside a group scaled up to 3x is soft unless it has `resolution: 3`. `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 }`) and `time` (print the value, in SECONDS, as a clock: `'hh:mm:ss'`, `'h:mm:ss'`, `'hh:mm'`, `'mm:ss'` or `'m:ss'`; the seconds are cut, not rounded, and the hours do not wrap, so a day clock keeps the value inside 0–86399 with `mod`: `{ value: 15000 }` with `format: { time: 'hh:mm' }` is `04:10`). Put prefixes and suffixes in the text (`'${value}'`, `'{value}%'`). If `value` is animated but the text has no `{value}`, a warning says so.
|
|
1952
2016
|
|
|
1953
2017
|
**Colour inside one text layer: `spans`.** Instead of `text`, write an array of strings (drawn in the layer's own `style`) and `{ text, fill, … }` objects; they are shown one after the other, and `\n` inside a span starts a new line:
|
|
1954
2018
|
|
|
@@ -1962,7 +2026,7 @@ A span may change `fill`, `fontWeight`, `fontStyle`, `fontVariant`, `letterSpaci
|
|
|
1962
2026
|
|
|
1963
2027
|
### `image`
|
|
1964
2028
|
|
|
1965
|
-
Renders a [PIXI.Sprite](https://pixijs.com/8.x/guides/components/scene-objects/sprite/sprite) from a registered asset. Intrinsic `w`/`h` come from the loaded texture.
|
|
2029
|
+
Renders a [PIXI.Sprite](https://pixijs.com/8.x/guides/components/scene-objects/sprite/sprite) from a registered asset. Intrinsic `w`/`h` come from the loaded texture. `scaleMode: 'nearest'` draws the pixels as hard squares when the picture is scaled up (pixel art, a sprite drawn small and shown big); the default `'linear'` smooths. It belongs to the asset: every layer that shows it gets it.
|
|
1966
2030
|
|
|
1967
2031
|
```ts
|
|
1968
2032
|
{
|
|
@@ -2085,7 +2149,7 @@ A tune with no audio file: notes, chords and drum patterns written as strings, p
|
|
|
2085
2149
|
} }
|
|
2086
2150
|
```
|
|
2087
2151
|
|
|
2088
|
-
**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
|
|
2152
|
+
**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; 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.
|
|
2089
2153
|
|
|
2090
2154
|
**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.
|
|
2091
2155
|
|
|
@@ -2165,7 +2229,9 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
|
|
|
2165
2229
|
| `arc` | `radius` | `innerRadius`, `startAngle` (0), `endAngle` (360) |
|
|
2166
2230
|
| `line` | `from: [x,y]`, `to: [x,y]` (canvas coordinates; stroke in `initial`) | `trimStart`, `trimEnd` |
|
|
2167
2231
|
| `polygon` | `points: [[x,y], …]` | `open` (default false), `trimStart`, `trimEnd` |
|
|
2168
|
-
| `path` | `d` (SVG path data) | `trimStart`, `trimEnd`, `morphTo`, `morph`, `morphPoints` |
|
|
2232
|
+
| `path` | `d` (SVG path data) | `fillRule`, `trimStart`, `trimEnd`, `morphTo`, `morph`, `morphPoints` |
|
|
2233
|
+
|
|
2234
|
+
**Holes (`fillRule: 'evenodd'`) on a `path`.** Several sub-paths in one `d` are each filled on their own: a donut written as two circles is a solid disc. With `fillRule: 'evenodd'` the biggest sub-path is the shape and every other one is a hole cut out of it (the stroke goes round the shape and each hole): `{ shape: 'path', fillRule: 'evenodd', d: 'M 0 -100 A 100 100 0 1 1 0 100 A 100 100 0 1 1 0 -100 Z M 0 -50 A 50 50 0 1 0 0 50 A 50 50 0 1 0 0 -50 Z', initial: { x: 640, y: 360, fillColor: '#ffcc00' } }`. Write the sub-paths in absolute coordinates (each starts with `M`); an island inside a hole is not drawn, and a path that morphs (`morphTo`) has no holes.
|
|
2169
2235
|
|
|
2170
2236
|
**`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.
|
|
2171
2237
|
|
|
@@ -2183,7 +2249,7 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
|
|
|
2183
2249
|
keyframes: [{ at: 0, to: { trimEnd: 1 }, duration: 1, ease: 'power2.inOut' }] } // a check mark drawing itself
|
|
2184
2250
|
```
|
|
2185
2251
|
|
|
2186
|
-
**Morph (`morphTo` / `morph`).** A `path` can turn into another outline: give `morphTo` (SVG path data in the same canvas coordinates as `d`) and animate `morph` from 0 (the `d` outline) to 1 (the `morphTo` outline). In between it is drawn as a polygon of points half way from one to the other (`morphPoints`: 8–2048, default 128). Closed outlines are lined up (start point and direction) so they do not twist; sub-paths are paired in order, and an extra one grows from (or shrinks into) its own centre. Fill and stroke both follow, and `trimStart` / `trimEnd` walk the morphing outline. Write both outlines as `M … L … C … Z` in absolute coordinates (arcs and holes work too, but keep the same number of sub-paths on both sides).
|
|
2252
|
+
**Morph (`morphTo` / `morph`).** (Two outlines with the **same number of corners**, such as two quads, are paired corner to corner, so every in-between picture is a straight-edged quad and no corner is rounded; `morphPoints` only matters when the corner counts differ or the outlines are curves.) A `path` can turn into another outline: give `morphTo` (SVG path data in the same canvas coordinates as `d`) and animate `morph` from 0 (the `d` outline) to 1 (the `morphTo` outline). In between it is drawn as a polygon of points half way from one to the other (`morphPoints`: 8–2048, default 128). Closed outlines are lined up (start point and direction) so they do not twist; sub-paths are paired in order, and an extra one grows from (or shrinks into) its own centre. Fill and stroke both follow, and `trimStart` / `trimEnd` walk the morphing outline. Write both outlines as `M … L … C … Z` in absolute coordinates (arcs and holes work too, but keep the same number of sub-paths on both sides).
|
|
2187
2253
|
|
|
2188
2254
|
```ts
|
|
2189
2255
|
{ type: 'shape', shape: 'path', d: 'M 640 160 C 780 160 860 260 860 360 C 860 470 770 560 640 560 C 510 560 420 470 420 360 C 420 250 500 160 640 160 Z',
|
|
@@ -3649,6 +3715,15 @@ snapshot(frame?: number, options?: { scale?: number; type?: 'image/png' | 'image
|
|
|
3649
3715
|
|
|
3650
3716
|
A picture of one frame: **the canvas only** (the player bar is not in it). Seeks to `frame` (default: the current frame) and stays there. `as: 'dataURL'` returns a `data:` URL string, `as: 'imageData'` the pixels as an `ImageData` (to measure or compare; `quality` is the JPEG quality, 0 to 1), handy when a script can only return text. Use it to look at what you built.
|
|
3651
3717
|
|
|
3718
|
+
### `movie.thumbnail(frame, options?): Promise<HTMLCanvasElement | null>`
|
|
3719
|
+
|
|
3720
|
+
```ts
|
|
3721
|
+
thumbnail(frame: number, options?: { width?: number; scale?: number }): Promise<HTMLCanvasElement | null> // width default 160 px
|
|
3722
|
+
movie.thumbnailsOff // null, 'video' or 'variables'
|
|
3723
|
+
```
|
|
3724
|
+
|
|
3725
|
+
A small picture of any frame, made **without moving the playhead or changing what is on the canvas**, also while the movie plays (a seek-bar preview): the scene is put on that moment, drawn, copied and put back inside one task, so the browser never shows the in-between. It is the picture `snapshot()` gives at that size, without motion blur. It gives `null` when it cannot be made right now (a seek, a render or another thumbnail is running; ask again) and always when `movie.thumbnailsOff` is set: `'video'` for a movie with a video layer (the footage would have to seek twice), `'variables'` for a movie that declares variables. The player bar uses it for `thumbnails: true`.
|
|
3726
|
+
|
|
3652
3727
|
### `movie.contactSheet(options?): Promise<Blob | string>`
|
|
3653
3728
|
|
|
3654
3729
|
```ts
|
|
@@ -3670,13 +3745,13 @@ Text layout helpers, exported from `pixi-effects`. They measure with the rendere
|
|
|
3670
3745
|
```ts
|
|
3671
3746
|
measureText('Hello', { fontSize: 72 }); // { width: 187.4, height: 81.3, lines: 1 }
|
|
3672
3747
|
|
|
3673
|
-
interface SplitOptions { by?: 'chars' | 'words' | 'lines'; x?: number; y?: number; align?: 'left' | 'center' | 'right' }
|
|
3674
|
-
interface TextPiece { text: string; x: number; y: number; width: number; index: number; line: number }
|
|
3748
|
+
interface SplitOptions { by?: 'chars' | 'words' | 'lines'; x?: number; y?: number; align?: 'left' | 'center' | 'right'; vertical?: boolean }
|
|
3749
|
+
interface TextPiece { text: string; x: number; y: number; width: number; index: number; line: number; cx?: number; cy?: number; rotation?: number } // cx / cy / rotation: vertical text only
|
|
3675
3750
|
splitText('Hello world', style, { by: 'words', x: 640, y: 300, align: 'center' });
|
|
3676
3751
|
// → [{ text: 'Hello', x, y, width, index: 0, line: 0 }, { text: 'world', ... }]
|
|
3677
3752
|
```
|
|
3678
3753
|
|
|
3679
|
-
`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).
|
|
3754
|
+
`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. **Vertical writing (`vertical: true`, tategaki):** characters go down a column and a new line is a new column to the LEFT; `x` is the RIGHT edge of the first column, `y` the top of a column (`align: 'center'` / `'right'` put `y` at its middle / bottom). Each piece also has `cx` / `cy` (the centre of its cell, with 、。 moved to the upper right and small kana a little up and right) and `rotation` (90 for the long vowel mark, dashes, waves, leaders and brackets, which lie turned in vertical text): put a text layer with `anchorX: 0.5, anchorY: 0.5` at `cx` / `cy` with `rotation: p.rotation`, or call `animateText(..., { vertical: true })`, which does it. Latin letters and digits stay upright, one per cell (no tate-chu-yoko), and a line is only cut at `\n` (no kinsoku). Only `by: 'chars'` has a vertical form. 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).
|
|
3680
3755
|
|
|
3681
3756
|
### `movie.inspect(frame?, options?): Promise<InspectReport>`
|
|
3682
3757
|
|
|
@@ -3686,7 +3761,7 @@ Where every layer is drawn at `frame`, as JSON — for checking layout without e
|
|
|
3686
3761
|
interface InspectReport {
|
|
3687
3762
|
frame: number; time: number; canvas: { width: number; height: number };
|
|
3688
3763
|
summary: { layers: number; visible: number };
|
|
3689
|
-
issues: string[]; // text off the canvas / cut by an edge / empty / overlapping other text — read this first
|
|
3764
|
+
issues: string[]; // text off the canvas / cut by an edge / empty / overlapping other text / partly hidden by an opaque shape or image drawn over it / struck through by a thin line drawn over it — read this first
|
|
3690
3765
|
layers: Array<{
|
|
3691
3766
|
path: string; // names (or type#index) from the root, joined by '/'
|
|
3692
3767
|
name?: string; type: string; threeD: boolean;
|
|
@@ -3711,6 +3786,7 @@ interface AudioReport {
|
|
|
3711
3786
|
start: number; end: number; // movie time
|
|
3712
3787
|
peakDb: number; // the MIX while it plays
|
|
3713
3788
|
sound: { length: number; peakDb: number; loudestAt: number; brightnessHz: number }; // the sound alone
|
|
3789
|
+
belowMixDb?: number; // how far below the loudest sample of the mix (itself included) around its own loudest moment it is; short sfx more than 12 dB under get ONE note, "buried", naming the worst
|
|
3714
3790
|
}>;
|
|
3715
3791
|
windows: Array<{ t: number; rmsDb: number; peakDb: number; brightnessHz: number }>;
|
|
3716
3792
|
issues: string[]; // limited mix, inaudible layer, sfx cut off by the end, no audio — read first
|
|
@@ -4023,6 +4099,7 @@ interface ControllerOptions {
|
|
|
4023
4099
|
keyboard?: boolean; // default true
|
|
4024
4100
|
presentButton?: boolean; // default true: a movie with stops gets a Present button (marks on the seek bar are always shown)
|
|
4025
4101
|
loopButton?: boolean; // default true: a loop button next to the volume (and the L key) that flips movie.loop
|
|
4102
|
+
thumbnails?: boolean; // default false: a small picture of the frame under the pointer above the seek bar (movie.thumbnail()); off by itself with video footage or variables
|
|
4026
4103
|
pauseAtStops?: boolean; // default true: with stops, play / Space / click on the picture play to the next stop and pause; false plays straight through
|
|
4027
4104
|
clickToPlay?: boolean; // default true: a click / tap on the picture plays / pauses (like <video>); false leaves the canvas alone
|
|
4028
4105
|
className?: string; // default 'movie-controller'
|
package/package.json
CHANGED
|
@@ -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.
|
|
6
|
+
> Written for pixi-effects 0.36.0. 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
|
|
|
@@ -18,10 +18,10 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
18
18
|
**Or by hand: run it and read the logs first.** `window.__ready` must be `true` and `window.__logs` should be `[]`. A pixi-effects warning is an instruction: fix what it names.
|
|
19
19
|
5. **Look at it** — you cannot judge motion from code. The library has the tools built in:
|
|
20
20
|
- `await movie.contactSheet({ count: 6, as: 'dataURL' })` → ONE image of several labelled frames (pass `frames: [...]` / `times: [...]` to choose; include the middle of every transition and the last second). Save it and view it.
|
|
21
|
-
- `await movie.inspect(frame)` → every layer's canvas bounds and visibility, plus `issues` (text off the canvas, cut off by an edge, empty,
|
|
21
|
+
- `await movie.inspect(frame)` → every layer's canvas bounds and visibility, plus `issues` (text off the canvas, cut off by an edge, empty, overlapping other text, or hidden / struck through by a picture: an opaque shape or image drawn OVER part of it, or a thin line drawn over its body; a shadow, a glow, a ring or a backdrop under it is not reported). Run it at several frames; fix every issue it names.
|
|
22
22
|
- `await movie.snapshot(frame, { as: 'dataURL' })` → one frame, canvas only (no player bar).
|
|
23
23
|
- `movie.inspectAudio()` → **hear with numbers**: for every sound its `start`, `sound.length`, `sound.peakDb`, `sound.loudestAt` (line this up with the picture) and `sound.brightnessHz` (< 500 dark, > 8000 airy), plus `issues` (limited mix, inaudible or cut-off sounds). Check it whenever the movie has audio; `render()` encodes this same mix.
|
|
24
|
-
- **A moment, a movement, the sound (`pixi-effects check` options):** `--at 3.5,title@end,50%,f120` writes those pictures to `check-out/<name>/frames/*.png` (and `at.png`): the last frame of a layer is `name@end`, the middle `name@mid`. `--onion 1:3` writes `onion.png`, eight frames of seconds 1–3 overlaid (the later the stronger): a trail that shows a movement's path and easing (even steps = constant speed, bunched = slow). `report.audio` now has `loudness` (`integratedLufs`, `truePeakDb`, `clippedSamples`, `silences`), `scenes` (per named top-level composition), `cues` (when each sound starts) and `notes` (advice: quiet or loud for web video, little headroom, silent stretches; web video is typically −14 to −16 LUFS); `waveform.png` draws the mix with the scene edges and the cues. A web font that failed to load fails the check; a text layer none of whose fonts is installed is listed for review.
|
|
24
|
+
- **A moment, a movement, the sound (`pixi-effects check` options):** `--at 3.5,title@end,50%,f120` writes those pictures to `check-out/<name>/frames/*.png` (and `at.png`; with `--out DIR` they land in `DIR/frames/` and `DIR/at.png`, there is no `<name>/` inside DIR); add `--crop X,Y,W,H` (canvas pixels) to get each picture as just that part, enlarged up to 4x, so a 40 px figure, a fine line or a texture can be judged (the contact sheet tiles are too small for that): the last frame of a layer is `name@end`, the middle `name@mid`. `--onion 1:3` writes `onion.png`, eight frames of seconds 1–3 overlaid (the later the stronger): a trail that shows a movement's path and easing (even steps = constant speed, bunched = slow). `report.audio` now has `loudness` (`integratedLufs`, `truePeakDb`, `clippedSamples`, `silences`), `scenes` (per named top-level composition), `cues` (when each sound starts) and `notes` (advice: quiet or loud for web video, little headroom, silent stretches; web video is typically −14 to −16 LUFS); `waveform.png` draws the mix with the scene edges and the cues. A web font that failed to load fails the check; a text layer none of whose fonts is installed is listed for review.
|
|
25
25
|
- **A deck** (`composition.stops`, `deck()`): `await movie.contactSheet({ frames: movie.stops.map(s => s.frame), as: 'dataURL' })` is one picture of every stop (the check writes it as `stops.png`); what the audience reads while a page waits is that picture, so check every one. `await inspectStops(movie)` (from `pixi-effects/present`) lists the stops where the picture is still changing (a stop landing before its animation ends); the check prints it as a review list: move the stop later or flag a settled one `pdf: true`. `await stopImages(movie, { which: 'pages' })` / `exportPDF(movie)` give the pages themselves.
|
|
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.
|
|
@@ -10,9 +10,10 @@ A video is **data**: a tree of plain objects. Everything below is a JS object yo
|
|
|
10
10
|
| Angles | **degrees**: `rotation`, `skew*`, `rotationX`, `rotationY` |
|
|
11
11
|
| Depth (`z`) | `+z` toward the viewer (bigger, nearer) |
|
|
12
12
|
| Numbers | any number may be an **expression string**: `'GW/2 - w/2'`, `'min(W,H)*0.4'` (build-time only; no per-frame time variable). Functions: `min max abs floor ceil round sqrt pow sin cos tan lerp clamp smoothstep mod step`, `PI`, and **seeded** `rand(seed)` (0…1) / `noise(x, seed)` (−1…1): give each layer its own seed, never `Math.random()`. **Relative values** work in a keyframe's `to` / `from` / `set` (not in `initial`, a style, or the start of a `from`+`to`): `'+=36'` / `'-=36'` are measured from where the layer stands, and the right side is an expression (`'-=GW*0.1'`). A slide-in is `from: { x: '-=36' }`, a nudge `to: { y: '+=40' }`; keyframes one after another stack; with `repeat` the same move replays from the same start (it does not stack). Expressions work in layer `x y` and the other numeric props, **not** in `line.from / to`, `polygon.points` or `path.d`: compute those in JS (`const W = 1920`) |
|
|
13
|
-
| Colours | `'#rrggbb'` strings or numbers; text colour is `style.fill
|
|
13
|
+
| Colours | `'#rrggbb'` strings or numbers; text colour is `style.fill` (or `initial: { fill }`; a `fill` keyframe animates it), shape colour is `fillColor`/`strokeColor`, image colour is `tint` |
|
|
14
14
|
| Z-order | **array order**, later = on top. A layer is hidden (not removed) outside `[at, at+duration)` and keeps its last values |
|
|
15
15
|
| Time of a sequence's `at` | seconds from the start of its **parent composition** (default 0) |
|
|
16
|
+
| `from` before its `at` | a keyframe's `from` holds **from the start of the layer**, not from its `at` (so a title waits invisible with `from: { alpha: 0 }`), and it replaces `initial`: `initial: { alpha: 0 }` with `{ at: 2, from: { alpha: 1 }, … }` shows the layer at alpha 1 from 0 s. To appear only at 2 s give the layer `at: 2`, or start from the same value in both |
|
|
16
17
|
| Time of a keyframe's `at` | seconds from the start of **its own layer**; **negative = back from the layer's end** |
|
|
17
18
|
| Time of a transition's `at` | seconds from the start of the **parent composition**; negative = back from its end |
|
|
18
19
|
| `duration` | defaults to the parent composition's duration |
|
|
@@ -50,9 +51,9 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
50
51
|
|
|
51
52
|
## Layer types
|
|
52
53
|
|
|
53
|
-
**text** — `text`, `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */ }`) — that is how counters work. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change). **Colour inside one line:** `spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'a'", fill: '#ffd166', fontWeight: '700' }, ',\n next line']` instead of `text` (strings use the layer's `style`; a span changes only fill / fontWeight / fontStyle / fontVariant / letterSpacing / dropShadow, never a size, font or stroke (those are the layer's `style`); `\n` starts a line; `style.align: 'left'` for code; `visibleChars` types over all the spans; it becomes a group of text layers `<name>.0…`).
|
|
54
|
+
**text** — `text`, `resolution` (1–8: draw at N× the pixels so text inside a parent scaled up N× stays sharp), `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */, time: 'hh:mm:ss'|'h:mm:ss'|'hh:mm'|'mm:ss'|'m:ss' /* the value in SECONDS as a clock */ }`) — that is how counters work. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change). **Colour inside one line:** `spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'a'", fill: '#ffd166', fontWeight: '700' }, ',\n next line']` instead of `text` (strings use the layer's `style`; a span changes only fill / fontWeight / fontStyle / fontVariant / letterSpacing / dropShadow, never a size, font or stroke (those are the layer's `style`); `\n` starts a line; `style.align: 'left'` for code; `visibleChars` types over all the spans; it becomes a group of text layers `<name>.0…`).
|
|
54
55
|
|
|
55
|
-
**image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'
|
|
56
|
+
**image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'`, `scaleMode: 'nearest'` (hard square pixels when scaled up: pixel art; default `'linear'`, smooth; it belongs to the asset). Default anchor top-left; natural size = `w`,`h`.
|
|
56
57
|
|
|
57
58
|
**video** — `asset`, `loop`, `audio`, `volume`, **`speed`**; `initial: { scale: 'cover' }`. `speed: 0.5` slow motion, `speed: -1` backward (it starts at |speed| × `duration`, so a 3 s file with no `duration` plays whole, backward); with no `duration` the layer lasts the file's length ÷ |speed|. For anything that is not one straight speed — a freeze, a ramp, a loop — animate **`time`** (seconds in the file) like any property: `keyframes: [{ at: 0, from: { time: 0 }, to: { time: 2 }, duration: 1 }, { at: 1, to: { time: 2 }, duration: 0.5 }, { at: 1.5, to: { time: 6 }, duration: 3, ease: 'power2.inOut' }]` (play 2 s, hold, then accelerate). `repeat` loops it, `yoyo` ping-pongs. `speed` itself is not animatable (it warns): animate `time`. The sound follows (pitch too, like a tape).
|
|
58
59
|
|
|
@@ -70,9 +71,9 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
70
71
|
| arc | `radius innerRadius startAngle endAngle` — degrees, **0° = 3 o'clock, clockwise**; centred on `x, y`; stroke only = an open arc line (`strokeCap: 'round'`), with a fill = a sector, with `innerRadius` = a donut slice; a sweep ≥ 360° = a full circle. A progress ring: `startAngle: -90` and animate `endAngle` from −90 to 270 |
|
|
71
72
|
| line | `from: [x,y] to: [x,y]` — plain canvas coordinates (omit `x,y`; if you give them they place the line's midpoint). Stroke in `initial`: `strokeColor`, `strokeWidth` |
|
|
72
73
|
| polygon | `points: [[x,y],…] open` — canvas coordinates like `line` |
|
|
73
|
-
| path | `d` (SVG path data) — canvas coordinates like `line
|
|
74
|
+
| path | `d` (SVG path data) — canvas coordinates like `line`; leave `x` / `y` out and it sits where `d` says; give them (even `y: 0`) and they place its MIDDLE, not an offset (measured: `y: 0` put a square's centre on the top edge). **Holes:** several sub-paths are each filled alone (a donut written as two circles is a disc) unless `fillRule: 'evenodd'`: the biggest is the shape, the others are holes. **Morph:** `morphTo: 'M…Z'` + animate `morph` 0 → 1 (points half way between, closed outlines lined up so they do not twist; `morphPoints` 8–2048; two outlines with the same number of corners are paired corner to corner, so a quad stays a sharp quad) |
|
|
74
75
|
|
|
75
|
-
**Draw-on:** every shape but `arc` has `trimStart` / `trimEnd` (0–1 of the outline, default 0 / 1, animatable): `trimEnd: 0` + keyframe `to: { trimEnd: 1 }` draws a line / border / path on; `trimStart` 0 → 1 after it wipes it off; both = a travelling dash. The fill is NOT trimmed. Rect outline starts top-left, circle / ellipse at 12 o'clock, both clockwise. A `path` with several sub-paths walks them in order as one length; `trimEach: true` trims each on its own (all draw on together). Use `strokeCap: 'round'`. **Dashes:** there is no dash option; write the outline as alternating sub-paths (`d: 'M 0 0 L 10 0 M 20 0 L 30 0 …'`, or sample the curve) and `trimEnd` 0 → 1 walks them in order, so the dashes draw on one after another (as in `
|
|
76
|
+
**Draw-on:** every shape but `arc` has `trimStart` / `trimEnd` (0–1 of the outline, default 0 / 1, animatable): `trimEnd: 0` + keyframe `to: { trimEnd: 1 }` draws a line / border / path on; `trimStart` 0 → 1 after it wipes it off; both = a travelling dash. The fill is NOT trimmed. Rect outline starts top-left, circle / ellipse at 12 o'clock, both clockwise. A `path` with several sub-paths walks them in order as one length; `trimEach: true` trims each on its own (all draw on together). Use `strokeCap: 'round'`. **Dashes:** there is no dash option; write the outline as alternating sub-paths (`d: 'M 0 0 L 10 0 M 20 0 L 30 0 …'`, or sample the curve) and `trimEnd` 0 → 1 walks them in order, so the dashes draw on one after another (as in `route-atlas`).
|
|
76
77
|
|
|
77
78
|
Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha strokeWidth`; `colorSpace: 'oklab'|'oklch'` for clean colour tweens. rect/circle/ellipse are **centred on `x,y` by default** (`anchorX/anchorY` default 0.5): for a bar growing from its base use `anchorY: 1` (or `anchorX: 0` for left-to-right). **`fillGradient`** (top level or `initial`; instead of `fillColor`): `{ type?: 'linear'|'radial', stops: [[0, '#000'], [1, 'rgba(0,0,0,.6)']], angle? /* linear, deg; DEFAULT 90 = top→bottom, 0 = left→right: a wide gradient slid sideways as a matte needs angle: 0 */, center?, innerRadius?, radius? /* radial, 0–1 of the SHAPE's own bounds, not the canvas: a glow on a 1500 px circle is placed with fractions of that circle */ }` — stops may have alpha, so a radial transparent→dark is a vignette. **Animate it** with a partial gradient in a keyframe (`to: { fillGradient: { angle: 360, stops: [...] } }`: keys you leave out stay; the same number of stops; `type` cannot change; a start gradient is required; use `colorSpace: 'oklch'` for a vivid fade). A text layer takes `fillGradient` too (LINEAR only: angle and stops; type / center / radius are ignored with a warning; letters re-rasterised per change). `from` alone runs from that gradient to the layer's own. The names `'fillGradient.angle'` / `gradientAngle` are not properties: write `fillGradient: { angle }`. Stroke ends and corners: `strokeCap: 'butt'|'round'|'square'`, `strokeJoin: 'miter'|'round'|'bevel'` (top level or in `initial`; not animated — `round` keeps sharp corners like an M from poking out).
|
|
78
79
|
|
|
@@ -155,7 +156,7 @@ measureText('Hello', style) // → { width, height,
|
|
|
155
156
|
splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
|
|
156
157
|
// → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
|
|
157
158
|
```
|
|
158
|
-
Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
159
|
+
Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, vertical: true /* tategaki: down a column, new line = new column to the left, x = RIGHT edge of the first column; 、。 ー 「」 placed and turned for you */, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
159
160
|
|
|
160
161
|
## Assets
|
|
161
162
|
|
|
@@ -139,3 +139,6 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
139
139
|
109. **No font is bundled, and each script needs a font that has its characters.** The machine's own fonts are used; characters a font lacks are drawn with whatever the machine falls back to, so the same page can look different on another machine. Load a web font that covers the script (Noto Sans JP, Noto Sans KR for Hangul, Noto Sans SC) with `await document.fonts.load('40px "Noto Sans KR"')` before `movie.init`, and say it in `style.fontFamily` (pitfall 103). `check` lists a layer none of whose fonts is available, but a family that nothing installs or loads is silently replaced by the next one in the list (or the generic name) without a word: the first name in `fontFamily` is not proof it was used.
|
|
140
140
|
110. **Right-to-left text: three traps.** `splitText` / `animateText` order words left to right, so Arabic or Hebrew comes out reversed (split by words yourself, from the right edge, with `measureText` of the logical prefix); a mark below the font box can be clipped (the dots of an Arabic ي) unless the layer has `style.padding`; `check` sees neither. `bulgePinch.center` is 0–1 of the layer, not pixels (pitfalls elsewhere say canvas pixels for the other centres: `twist`, `zoomBlur`, `shockwave`). `npx pixi-effects help arabic` and `help bulgePinch` print the sections.
|
|
141
141
|
111. **Only some named filters are tested on both backends.** `glow`, `outline`, `dropShadow`, `bevel`, `bloom`, `pixelate`, `crt`, `rgbSplit`, `dot`, `radialBlur` and `glitch` look the same on WebGPU (the default) and WebGL; `multiColorReplace` draws everything black on WebGPU, `kawaseBlur` is far weaker and `advancedBloom` sharper there: use `colorReplace`, `blur`, `bloom`. A filter follows the edges of the layer's own picture: `glow` on a full-screen opaque layer shows nothing (put it on the shapes and the text).
|
|
142
|
+
112. **Text inside a parent that is scaled up comes out soft.** Text is drawn once at its own size and then stretched, so `{ type: 'null', initial: { scale: 3 } }` with a text child of `fontSize: 30` is blurry (shapes stay sharp). Give the text `resolution: 3` (it is drawn at 3 times the pixels, for a parent that grows to 3x; 1 to 8, the cost is the memory), or write it at the size it is shown (`fontSize: 90`, parent scale 1), or let the parent only scale DOWN (a pull-back from big to normal is sharp the whole way). `check --at … --crop` shows the difference.
|
|
143
|
+
113. **Two limits on sound that `help sfx` / `help music` did not say.** An `sfx` lasts at most 10 s (`MAX_LENGTH` in the synth): for a longer rain or room tone, chain two audio layers that overlap by a moment, or use `loop` on a short one. A `music` layer plays at most 16 tracks, and a warning says that only the first 16 play: put the rest in a second audio layer.
|
|
144
|
+
114. **A keyframe's `from` is on screen from the start of the layer, and it beats `initial`.** `initial: { alpha: 0 }` plus `{ at: 2, from: { alpha: 1 }, to: { alpha: 0.5 }, duration: 1 }` shows the layer at full alpha from second 0 (measured in Chrome: the red square was there at 0.5 s). That is how a title waits invisible with `from: { alpha: 0 }` until its `at`, but it surprises when `from` is the visible end. Give the layer its own `at: 2`, or use `set` at the moment it should appear. No warning: the same pattern (`initial` = the resting value, `from` = where it starts) is what the recipes do everywhere.
|
|
@@ -145,6 +145,55 @@ return [
|
|
|
145
145
|
|
|
146
146
|
---
|
|
147
147
|
|
|
148
|
+
## Push in on a whole flat scene (the 2D "camera"): a null that scales, and text drawn big enough
|
|
149
|
+
|
|
150
|
+
There is no second camera for flat scenes: the camera of the 2.5D layers is for `threeD` layers. For a push-in or pull-back of an ordinary 2D scene, put everything in a `null` layer that sits at the middle of the canvas and scale THAT (children use `parent: 'world'`, and their x / y are measured from the null's origin, so the middle of the canvas is 0, 0). Text is drawn once at its own size and then stretched, so give every text `resolution: R` where R is the biggest scale the group reaches (R 1–8); otherwise it goes soft.
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
// @recipe scene-zoom
|
|
154
|
+
const R = 2; // the group grows to 2x: its text is drawn at 2x pixels
|
|
155
|
+
const inWorld = (spec) => ({ parent: 'world', ...spec });
|
|
156
|
+
return [
|
|
157
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0f1424' } },
|
|
158
|
+
{ type: 'null', name: 'world', initial: { x: 'GW/2', y: 'GH/2' },
|
|
159
|
+
keyframes: [{ at: 0.5, from: { scale: 1 }, to: { scale: R }, duration: 3.5, ease: 'power2.inOut' }] },
|
|
160
|
+
inWorld({ type: 'shape', shape: 'circle', radius: 120, initial: { x: 0, y: 0, fillColor: '#3b5bdb' } }),
|
|
161
|
+
inWorld({ type: 'shape', shape: 'circle', radius: 40, initial: { x: 260, y: -120, fillColor: '#ffd166' } }),
|
|
162
|
+
inWorld({ type: 'text', text: 'Zoom in', resolution: R, style: { fontSize: 48, fontWeight: 'bold', fill: '#ffffff' }, initial: { x: 0, y: 0, anchorX: 0.5, anchorY: 0.5 } }),
|
|
163
|
+
inWorld({ type: 'text', text: 'sharp at 2x', resolution: R, style: { fontSize: 22, fill: '#c9d3f5' }, initial: { x: 0, y: 150, anchorX: 0.5, anchorY: 0.5 } }),
|
|
164
|
+
];
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Parallax: layers that scroll past at their own speed (a window of a train, a side-scroller)
|
|
170
|
+
|
|
171
|
+
Each layer is a long strip of repeated shapes that moves left at a constant speed, `speed × duration` pixels in all, so it must be that long plus the width of the screen. The speed is the depth: the near strip fast, the far one slow. A strip that is longer than it needs to be is only more layers.
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
// @recipe parallax-strips
|
|
175
|
+
const D = 8, W = 1280;
|
|
176
|
+
// one strip: `count` shapes `gap` px apart, scrolling left at `speed` px/s from x = 0 for D seconds
|
|
177
|
+
const strip = (name, speed, gap, y, size, color) => {
|
|
178
|
+
const count = Math.ceil((speed * D + W) / gap) + 1;
|
|
179
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
180
|
+
type: 'shape', shape: 'rect', name: `${name}-${i}`, width: size, height: size * 2,
|
|
181
|
+
initial: { x: i * gap, y, fillColor: color },
|
|
182
|
+
keyframes: [{ at: 0, to: { x: i * gap - speed * D }, duration: D, ease: 'none' }],
|
|
183
|
+
}));
|
|
184
|
+
};
|
|
185
|
+
return [
|
|
186
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#1b2a4a' } },
|
|
187
|
+
...strip('far', 30, 240, 420, 60, '#26365c'), // distant hills: 30 px/s
|
|
188
|
+
...strip('mid', 120, 200, 500, 40, '#314a7a'), // 120 px/s
|
|
189
|
+
...strip('near', 480, 360, 600, 24, '#0b1220'), // poles: 480 px/s
|
|
190
|
+
];
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
To land something exactly where you want at a time `t` (a moon under a bridge at 6 s), compute the strip's x at `t` the same way: `i * gap - speed * t`.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
148
197
|
## A ticker that scrolls out completely (`w` = the text's own width)
|
|
149
198
|
|
|
150
199
|
`'-w'` is the exact x at which the text has left the screen on the left (`w` is measured after the style is applied).
|
|
@@ -596,6 +645,17 @@ return [
|
|
|
596
645
|
];
|
|
597
646
|
```
|
|
598
647
|
|
|
648
|
+
**Vertical Japanese (tategaki)** is one option: `vertical: true`. The characters go down a column and a new line is a new column to the LEFT, so `x` is the RIGHT edge of the first column. The commas sit at the upper right of their cell, and the long vowel mark and the brackets are turned a quarter, as in print.
|
|
649
|
+
|
|
650
|
+
```js
|
|
651
|
+
// @recipe vertical-text
|
|
652
|
+
const style = { fontFamily: 'Hiragino Mincho ProN, Yu Mincho, serif', fontSize: 56, fill: '#1c1a17' };
|
|
653
|
+
return [
|
|
654
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#efe9dc' } },
|
|
655
|
+
...animateText('雨のあと、\nコーヒーは「ゆっくり」', style, { x: 900, y: 70, vertical: true, at: 0.3, duration: 5, in: 'fade', stagger: { each: 0.04 } }),
|
|
656
|
+
];
|
|
657
|
+
```
|
|
658
|
+
|
|
599
659
|
Presets for `in` / `out`: `rise` (default) `drop` `fade` `pop` `zoom` `slide` `spin`; or `{ from: { x: -30, scale: 0.5, rotation: 20 }, duration: 0.8, ease: 'power3.out' }` (`x` / `y` in `from` are offsets from the resting place, the rest are absolute values). `idle` drifts each piece between the entrance and the exit.
|
|
600
660
|
|
|
601
661
|
---
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>pixi-effects video</title>
|
|
7
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/loader.css" /> <!-- the loader: HTML + CSS only, so it is on screen at once and keeps moving while the page builds the video -->
|
|
8
8
|
<style>
|
|
9
9
|
body { background: #0d1220; margin: 0; padding: 16px; }
|
|
10
10
|
.stage { position: relative; width: min(960px, 100%); } /* the player and the loader both sit inside this box */
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"imports": {
|
|
38
38
|
"pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
|
|
39
39
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
40
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
41
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
42
|
-
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
43
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
40
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/index.js",
|
|
41
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/Controller.js",
|
|
42
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/present.js",
|
|
43
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/three.js",
|
|
44
44
|
"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
|
|
45
45
|
"three": "https://esm.sh/three@0.178.0"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
</script>
|
|
49
|
-
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
49
|
+
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/index.js" /> <!-- after the import map: the library downloads while the page is parsed -->
|
|
50
50
|
|
|
51
51
|
<script type="module">
|
|
52
52
|
import { Movie, kenBurns, withFade } from 'pixi-effects';
|