pixi-effects 0.13.0 → 0.14.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 +20 -0
- package/README.md +5 -5
- package/ai/SKILL.md +1 -1
- package/ai/reference/cheatsheet.md +1 -0
- package/ai/reference/pitfalls.md +1 -1
- package/ai/template.html +5 -5
- package/ai/tools/check.mjs +15 -2
- package/dist/Controller.cjs +2 -3
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/Controller.js +2 -3
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-DnR4H1OO.d.ts → Movie-BwT6XtGz.d.ts} +23 -0
- package/dist/{Movie-Tl5dRLJl.d.cts → Movie-qmgOU4in.d.cts} +23 -0
- package/dist/Presenter.d.cts +1 -1
- package/dist/Presenter.d.ts +1 -1
- package/dist/index.cjs +59 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +59 -5
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/docs/api.md +4 -0
- package/llms-full.txt +7 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
**Added**
|
|
6
|
+
|
|
7
|
+
- **`movie.inspectStops({ lookback?, tolerance? })`**: is the picture at each stop still changing? It compares each stop with the picture 3 frames before it and reports the stops where more than 0.4 % of the pixels differ (a stop that lands before its animation ends, so the audience, the page overview and a PDF see it half-finished), naming the stop, its page and what to do. `pixi-effects-check` runs it for a deck and lists the result as a review (`--strict` fails on it). It found a real fault in `quiet-hours` (page four's first stop was a few frames early), now fixed.
|
|
8
|
+
|
|
9
|
+
**Fixed**
|
|
10
|
+
|
|
11
|
+
- `pixi-effects-check` no longer prints "audio: Unable to decode audio data" for a movie with no sound: it says "no audio track".
|
|
12
|
+
|
|
13
|
+
## 0.13.1
|
|
14
|
+
|
|
15
|
+
**Fixed**
|
|
16
|
+
|
|
17
|
+
- **A deck's first click played through the stops.** The guide's live demos and the gallery started a piece with `movie.play()`, which ignores stops; for a movie with stops they now call `movie.next()` and stop at the first stop.
|
|
18
|
+
|
|
19
|
+
**Changed**
|
|
20
|
+
|
|
21
|
+
- **The stop marks are dots on the seek bar** (they were thin bars): a small white dot with a dark ring for a step, a larger dot with an accent ring where a page starts.
|
|
22
|
+
|
|
3
23
|
## 0.13.0
|
|
4
24
|
|
|
5
25
|
**Changed**
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.
|
|
3
|
+
> **Status**: experimental — current release `0.14.0`. The API may still change between minor versions (see [CHANGELOG](./CHANGELOG.md): `0.14.0` adds `movie.inspectStops()` (and a check review) for stops that land mid-animation, `0.13.1` fixes decks starting past their first stop in the guide and gallery and draws the stop marks as dots, `0.13.0` makes the player bar play a deck stop by stop (`pauseAtStops`), marks the stops more clearly, adds PDF to the download panel and a `pdf` flag on stops, `0.12.0` adds presentations (`stops`, `Presenter`, `deck()`, page overview, presenter view, PDF export) and audio-reactive visuals (`audioEnvelope()`, `react()`), `0.11.0` adds particles, motion blur, a shared loader and a much faster build for pieces with many layers, `0.10.0` adds seeded randomness, `wiggle` and `stagger`, null layers with `parent`, `animateText`, `followPath` and path morphing, and click-to-play in the player, `0.9.0` adds a poster time (`movie.init({ poster })`) and a guide site, `0.8.0` adds events and a theme for building your own player, `0.7.0` adds filters by name, the `pixi-effects-render` and `pixi-effects-view` commands and the timeline chart, `0.6.0` adds draw-on strokes (`trimEnd`) and text that changes over time, `0.5.0` adds synthesised sound effects (`sfx`, no files) and the `check` tool, `0.4.0` makes a mask share its layer's lifetime, `0.3.0` made keyframe `at` relative to the layer).
|
|
4
4
|
|
|
5
5
|
**[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 37 portfolio pieces written as plain data by AI models · **[Live demos →](https://yjmtmtk.github.io/pixi-effects/)** · 12 numbered examples + an in-browser playground.
|
|
6
6
|
|
|
@@ -42,8 +42,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
42
42
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
43
43
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
44
44
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
45
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
46
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
45
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.14.0/dist/index.js",
|
|
46
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.14.0/dist/Controller.js"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
</script>
|
|
@@ -54,7 +54,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
54
54
|
</script>
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.
|
|
57
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.14.0/dist/index.js`) rather than a CDN-rebundled build such as `esm.sh/pixi-effects` or jsDelivr's `+esm`: the entries (`pixi-effects`, `…/controller`, `…/three`) share internal chunks, which only works when each file is served untouched.
|
|
58
58
|
|
|
59
59
|
> **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
|
|
60
60
|
>
|
|
@@ -70,7 +70,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
|
|
|
70
70
|
"imports": {
|
|
71
71
|
"...": "(everything from the importmap above)",
|
|
72
72
|
"three": "https://esm.sh/three@0.178.0",
|
|
73
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
73
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.14.0/dist/three.js"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
</script>
|
package/ai/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
19
19
|
- `await movie.inspect(frame)` → every layer's canvas bounds and visibility, plus `issues` (text off the canvas, cut off by an edge, empty, or overlapping other text). Run it at several frames; fix every issue it names.
|
|
20
20
|
- `await movie.snapshot(frame, { as: 'dataURL' })` → one frame, canvas only (no player bar).
|
|
21
21
|
- `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.
|
|
22
|
-
- **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 movie.stopImages({ which: 'pages' })` / `movie.exportPDF()` give the pages themselves.
|
|
22
|
+
- **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 movie.inspectStops()` 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 movie.stopImages({ which: 'pages' })` / `movie.exportPDF()` give the pages themselves.
|
|
23
23
|
From a browser tool the result is a `data:` URL string: `agent-browser eval "movie.contactSheet({ count: 6, as: 'dataURL' })" | python3 ai/tools/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.
|
|
24
24
|
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).
|
|
25
25
|
|
|
@@ -130,6 +130,7 @@ new Presenter(movie, { canvas }); // import { Presenter } from 'pixi-eff
|
|
|
130
130
|
movie.next() / prev() / goToStop(i) / goToPage(n) / movie.stops / movie.on('stop', …) // Controller: marks on the seek bar, play / Space / click = play to the next stop (pauseAtStops: false = straight through), a Present button, PDF in the download panel
|
|
131
131
|
// Presenter extras: G = page overview (pictures), P = presenter view (a second window: live picture, next picture, notes, timer); start() makes the pictures before the audience sees anything
|
|
132
132
|
await movie.stopImages({ as: 'dataURL', scale: 0.25 }) // [{ stop, page, image }] one per page (its last stop); { which: 'stops' } one per stop (a page's picture honours `pdf` flags)
|
|
133
|
+
await movie.inspectStops() // { stops, issues }: stops whose picture is still changing (a stop landing before its animation ends); the check lists them
|
|
133
134
|
await movie.exportPDF({ title }) // Blob: a PDF page per page of the deck; CLI: pixi-effects-render talk.html -o talk.pdf (--all-stops)
|
|
134
135
|
const env = await audioEnvelope('music.mp3', { frameRate: 30 }) // BEFORE movie.init: .series.level|bass|mid|treble (0–1 per frame), .beats [s], .bpm, .at(t, band); bands: { name: [Hz, Hz] }
|
|
135
136
|
const env = bpmEnvelope(120, { duration: 12 }) // no audio file: kick / snare / hats exactly on a tempo
|
package/ai/reference/pitfalls.md
CHANGED
|
@@ -81,4 +81,4 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
81
81
|
54. **A deck page lasts until its last stop plus the transition.** `deck()` lays pages out one after another in page-local time and a `transition` overlaps them: a page shorter than its last stop (or shorter than the transition) loses its final step or flashes. A step is a layer that starts at the previous stop (`at: <previous stop>`), so the thing that appears on a press is fully visible when the movie stops. The transition option is `kind`, not `type`. If the last stop is mid-exit on paper, flag the stop you want in the PDF: `{ at, pdf: true }` (in `deck()`: `stops: [1.2, { at: 3, pdf: true }]`), and `pdf: false` keeps one out.
|
|
82
82
|
55. **Presenter and Controller both listen to click and keys.** Use the `Presenter` for a talk (or let the Controller's Present button start one); do not run both on the same canvas yourself. `movie.play()` ignores stops, but the Controller's play button / Space / click-on-picture play to the next stop and pause (`pauseAtStops: false` plays through); `movie.next()` plays to the next stop (a second press skips to it), `prev()` jumps back (nothing plays in reverse).
|
|
83
83
|
56. **Analyse audio before `init`, and give it the movie's frame rate.** `const env = await audioEnvelope('music.mp3', { frameRate })` is async; `react()` is pure and bakes the keyframes, so there is no per-frame callback. A custom `bands` list replaces the defaults (include `bass` if you want it; beats are picked there). No file yet: `bpmEnvelope(120, { duration })` is a stand-in with a kick on every beat. Offset the music with `audioOffset` when the audio layer does not start at 0.
|
|
84
|
-
57. **Review a deck by its stops, not by its timeline.** `pixi-effects-check` writes `stops.png` (one picture per stop, in order) for a page with `stops`; the picture at a stop is what the audience reads while it waits.
|
|
84
|
+
57. **Review a deck by its stops, not by its timeline.** `pixi-effects-check` writes `stops.png` (one picture per stop, in order) for a page with `stops`; the picture at a stop is what the audience reads while it waits. A stop that lands a few frames before an animation ends shows it half-finished (also in the PDF): `movie.inspectStops()` / the check's review list names such stops; give an animation a little more time than its step (e.g. the next stop 0.3 s after it ends).
|
package/ai/template.html
CHANGED
|
@@ -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.14.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 */
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
40
40
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
41
41
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
42
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
43
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
44
|
-
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
45
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
42
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.14.0/dist/index.js",
|
|
43
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.14.0/dist/Controller.js",
|
|
44
|
+
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.14.0/dist/Presenter.js",
|
|
45
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.14.0/dist/three.js",
|
|
46
46
|
"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
|
|
47
47
|
"three": "https://esm.sh/three@0.178.0"
|
|
48
48
|
}
|
package/ai/tools/check.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Needs: Node >= 22 (built-in WebSocket), Chrome / Chromium installed (or --chrome PATH / CHROME=PATH). No npm dependencies.
|
|
14
14
|
* The page must follow ai/template.html: it exposes `window.movie` and sets `window.__ready = true` (and `window.__logs`).
|
|
15
15
|
*
|
|
16
|
-
* Options: --strict (text overlaps fail the check; by default they are only listed for review) · --out DIR · --frames N (contact sheet tiles, default 12) · --formats mp4,webm (default mp4) · --no-export ·
|
|
16
|
+
* Options: --strict (text overlaps and stops where the picture is still changing fail the check; by default they are only listed for review) · --out DIR · --frames N (contact sheet tiles, default 12) · --formats mp4,webm (default mp4) · --no-export ·
|
|
17
17
|
* --timeout SECONDS (default 240) · --root DIR (static server root; default: the nearest folder above the page with dist/) · --chrome PATH
|
|
18
18
|
*/
|
|
19
19
|
import { spawn } from 'node:child_process';
|
|
@@ -187,6 +187,7 @@ const exportScript = format => `(() => {
|
|
|
187
187
|
info.video = { duration: v.duration, width: v.videoWidth, height: v.videoHeight };
|
|
188
188
|
} catch (e) { info.video = { error: String(e.message || e) }; }
|
|
189
189
|
try {
|
|
190
|
+
if (!movie.audioBuffer) throw null; // a movie with no sound has no audio track to decode: not an error
|
|
190
191
|
const buf = await new OfflineAudioContext(2, 1, 48000).decodeAudioData(await blob.arrayBuffer());
|
|
191
192
|
const L = buf.getChannelData(0), R = buf.getChannelData(buf.numberOfChannels > 1 ? 1 : 0), sr = buf.sampleRate;
|
|
192
193
|
const rmsDb = [];
|
|
@@ -197,7 +198,7 @@ const exportScript = format => `(() => {
|
|
|
197
198
|
rmsDb.push(Math.round(20 * Math.log10(Math.max(Math.sqrt(sum / Math.max(1, n)), 1e-6)) * 10) / 10);
|
|
198
199
|
}
|
|
199
200
|
info.audio = { duration: Math.round(buf.duration * 1000) / 1000, sampleRate: sr, peakDb: Math.round(20 * Math.log10(Math.max(peak, 1e-6)) * 10) / 10, rmsDbPerSecond: rmsDb };
|
|
200
|
-
} catch (e) { info.audio = { error: String(e.message || e) }; }
|
|
201
|
+
} catch (e) { info.audio = e === null ? null : { error: String(e.message || e) }; }
|
|
201
202
|
info.dataUrl = await new Promise(r => { const f = new FileReader(); f.onload = () => r(f.result); f.readAsDataURL(blob); });
|
|
202
203
|
window.__x = { state: 'done', info };
|
|
203
204
|
} catch (e) { window.__x = { state: 'error', message: String((e && e.message) || e) }; }
|
|
@@ -283,6 +284,13 @@ export async function runCheck(opts, log = console.log) {
|
|
|
283
284
|
const stops = await cdp.eval(`(() => { const s = movie.stops || []; return s.length ? { count: s.length, pages: movie.pageCount, items: s.map(x => ({ at: Math.round(x.at * 100) / 100, frame: x.frame, page: x.page, pageStart: x.pageStart, notes: !!x.notes, pdf: x.pdf })) } : null; })()`).catch(() => null);
|
|
284
285
|
if (stops) {
|
|
285
286
|
report.stops = stops;
|
|
287
|
+
// is the picture at each stop still changing? (the audience, the page overview and a PDF would see a half-finished animation)
|
|
288
|
+
const settled = await cdp.eval('movie.inspectStops()').catch(() => null);
|
|
289
|
+
if (settled) {
|
|
290
|
+
report.stops.review = settled.issues;
|
|
291
|
+
report.stops.moving = settled.stops.map(s => Math.round(s.moving * 10000) / 10000);
|
|
292
|
+
if (opts.strict && settled.issues.length) report.problems.push(`${settled.issues.length} stop(s) where the picture is still changing (movie.inspectStops)`);
|
|
293
|
+
}
|
|
286
294
|
const stopsSheet = await cdp.eval(`movie.contactSheet({ frames: movie.stops.map(s => s.frame), as: 'dataURL' })`).catch(() => null);
|
|
287
295
|
if (stopsSheet) {
|
|
288
296
|
const stopsFile = path.join(outDir, 'stops.png');
|
|
@@ -382,6 +390,11 @@ function finish(report, outDir, log) {
|
|
|
382
390
|
if (report.stops) {
|
|
383
391
|
const s = report.stops;
|
|
384
392
|
L.push(` stops ${s.count} stop(s) on ${s.pages} page(s) — stops.png shows each one in order; the picture at a stop is what the audience sees while it waits`);
|
|
393
|
+
if (s.review?.length) {
|
|
394
|
+
L.push(` review ${s.review.length} stop(s) where the picture is still changing (may be on purpose, e.g. something always moving; use --strict to fail on them):`);
|
|
395
|
+
for (const r of s.review.slice(0, 4)) L.push(` - ${r}`);
|
|
396
|
+
if (s.review.length > 4) L.push(` … ${s.review.length - 4} more (report.json)`);
|
|
397
|
+
} else if (s.review) L.push(' settled every stop is a settled picture (movie.inspectStops)');
|
|
385
398
|
}
|
|
386
399
|
if (report.audio !== undefined) {
|
|
387
400
|
const a = report.audio;
|
package/dist/Controller.cjs
CHANGED
|
@@ -49,9 +49,8 @@ var CONTROLLER_CSS = `
|
|
|
49
49
|
}
|
|
50
50
|
.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }
|
|
51
51
|
.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }
|
|
52
|
-
.mc-stop-tick { position: absolute; top: 50%; width:
|
|
53
|
-
.mc-stop-page { width:
|
|
54
|
-
.mc-stop-page::before { content: ''; position: absolute; left: 50%; top: -3px; width: 8px; height: 8px; transform: translateX(-50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }
|
|
52
|
+
.mc-stop-tick { position: absolute; top: 50%; width: 7px; height: 7px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5); }
|
|
53
|
+
.mc-stop-page { width: 11px; height: 11px; box-shadow: 0 0 0 2.5px var(--mc-accent, #007AFF), 0 0 0 4px rgba(0,0,0,0.35); }
|
|
55
54
|
.mc-progress-fill {
|
|
56
55
|
position: absolute; left: 12px; top: 50%;
|
|
57
56
|
height: var(--mc-track-height, 3px);
|