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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.36.0
|
|
4
|
+
|
|
5
|
+
**Vertical writing, `check` that sees pictures over text, holes in paths, and the fixes found by 18 fresh authors (the claims that were verified on the real build)**
|
|
6
|
+
|
|
7
|
+
- **New: vertical writing (tategaki).** `animateText(..., { vertical: true })` and `splitText(..., { vertical: true })`: 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). 、。 sit at the upper right of their cell, small kana a little up and right, and the long vowel mark, dashes, waves and brackets are turned a quarter clockwise (`rotation: 90`); pieces carry `cx` / `cy` / `rotation`. No tate-chu-yoko and no automatic line breaking.
|
|
8
|
+
- **New: `check` lists an opaque shape or image drawn over part of a text, and a thin line drawn over its body** (review list, with the other overlaps; a shadow, glow, ring or backdrop is not reported), **`check --at … --crop X,Y,W,H`** writes each picture as that part of the canvas, enlarged up to 4x, and **`help`** finds a heading written with spaces (`motionBlur`), a table row (`path`) and a call (`steps`).
|
|
9
|
+
- **New: `format: { time: 'hh:mm' }`** (also `'hh:mm:ss'`, `'h:mm:ss'`, `'mm:ss'`, `'m:ss'`) prints a counter's value, in seconds, as a clock; **`resolution`** on a text layer (1–8) draws it at N× the pixels so text inside a group scaled up N× stays sharp; **image `scaleMode: 'nearest'`** for pixel art; **`inspectAudio`** gives each source `belowMixDb` (how far below the mix around its loudest moment it is) and `check` gives ONE note when short sfx sit more than 12 dB under the mix ("buried", naming the worst).
|
|
10
|
+
- **Guide demos load this repository's build** (`../../dist/…`), like the gallery pages, so a demo shows the code of the same commit.
|
|
11
|
+
- **Fixed: `initial: { fill }` on a text layer was dropped without a word** (only a `fill` keyframe worked). It is now the colour from the start (it wins over `style.fill`).
|
|
12
|
+
- **Fixed: a filter option set to `undefined` (a misspelt variable) overwrote the filter's own default**: `{ type: 'bloom', strength: undefined }` made `movie.init` fail with "Cannot read properties of undefined (reading 'x')". The key is now left out and a warning names the filter and the options.
|
|
13
|
+
- **`check --at`** no longer leaves the pictures of an earlier run in `frames/` (it cleared nothing, so an old frame could be taken for the current one). The music read-back no longer says "ends early" for a track that is a few millionths of a beat shorter (decimal note lengths add up with float noise).
|
|
14
|
+
- **`pixi-effects new --size`** sizes the shown page for a tall or square movie (it was 960 px wide, so a 9:16 page was 1700 px tall).
|
|
15
|
+
- **New: `fillRule: 'evenodd'` on a `path`** (a donut written as two circles in one `d` was a solid disc: sub-paths were never holes). The biggest sub-path is the shape, every other one is cut out of it, and the stroke goes round all of them.
|
|
16
|
+
- **Changed: a morph between two outlines with the same number of corners is paired corner to corner** (it resampled both to 128 points along the edges, so a morph between two quads whose edges are not in proportion rounded the corners half way). Outlines with different corner counts, and curves, behave as before.
|
|
17
|
+
- Docs: a keyframe's `from` holds from the start of the layer and beats `initial` (cheatsheet, pitfall 114: no warning, because `initial` = the resting value and `from` = where it starts is what the recipes do); a path's `x` / `y`, when given, place its middle.
|
|
18
|
+
- Docs: `--out DIR` writes straight into DIR (no `<name>/` inside); pitfalls 112 (text under a scaled-up parent is soft) and 113 (an `sfx` lasts at most 10 s, a `music` layer plays 16 tracks).
|
|
19
|
+
- Gallery and guide: 59 pieces (18 new, 17 swapped, 11 removed); the guide no longer shows gallery pieces (its demos are built from its own recipes, `scripts/build-guide-demos.mjs`); `scripts/gallery-swap.mjs` also adds (`--add`) and removes (`--remove`).
|
|
20
|
+
|
|
21
|
+
## 0.35.0
|
|
22
|
+
|
|
23
|
+
**The player's seek-bar preview (`thumbnails: true`, off by default) and `movie.thumbnail()`**
|
|
24
|
+
|
|
25
|
+
- **`movie.thumbnail(frame, { width? | scale? })`** gives a small canvas of any frame **without moving the playhead or changing the picture**, also while the movie plays. It draws the frame on the side inside one task (nominal time set, 3D / shader layers awaited, the playhead put back and awaited), so the browser never shows the in-between; it equals `snapshot({ motionBlur: false })` at that size. `null` when a seek, render or another thumbnail is running. Seeks, renders and peeks wait for a running thumbnail, so they never interleave with it.
|
|
26
|
+
- **`movie.thumbnailsOff`** is `'variables'` for a movie that declares variables and `'video'` for one with a video layer, else `null`; `thumbnail()` gives `null` then (a video would have to seek twice, a variable movie can be re-laid out under it).
|
|
27
|
+
- **`new Controller(movie, { canvas, thumbnails: true })`**: hover (or move along) the seek bar and a 160 px picture with the time appears above it, kept inside the picture, hidden while dragging; pictures are cached per tenth of a second until a rebuild. Default off.
|
|
28
|
+
- Tests: `tests/tools/thumbnail.test.ts` (exact against `snapshot`, undisturbed canvas and playhead, playback with a thumbnail per frame, the off reasons, options) and `tests/tools/thumbnailPreview.test.ts` (the bar, with real mouse moves).
|
|
29
|
+
|
|
3
30
|
## 0.34.0
|
|
4
31
|
|
|
5
32
|
**From the capability probe of 2026-10-12 (15 kinds of video tried by fresh agents; nothing was impossible, 9 needed hand-written glue; these are the defects that were verified on the real build)**
|
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.36.0`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
|
|
4
4
|
|
|
5
5
|
**[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 52 portfolio pieces written as plain data by AI models · **[Examples →](https://yjmtmtk.github.io/pixi-effects/examples/)** · 15 numbered examples, a music lab + an in-browser playground.
|
|
6
6
|
|
|
@@ -49,8 +49,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
49
49
|
"imports": {
|
|
50
50
|
"pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
|
|
51
51
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
52
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
53
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
52
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/index.js",
|
|
53
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/Controller.js"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
</script>
|
|
@@ -61,7 +61,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
61
61
|
</script>
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.
|
|
64
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.36.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.
|
|
65
65
|
|
|
66
66
|
> **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
|
|
67
67
|
>
|
|
@@ -77,7 +77,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
|
|
|
77
77
|
"imports": {
|
|
78
78
|
"...": "(everything from the importmap above)",
|
|
79
79
|
"three": "https://esm.sh/three@0.178.0",
|
|
80
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
80
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/three.js"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
</script>
|
package/ai/chat-template.html
CHANGED
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"imports": {
|
|
60
60
|
"pixi.js": "https://cdn.jsdelivr.net/npm/pixi.js@8.22.0/dist/pixi.min.mjs",
|
|
61
61
|
"mediabunny": "https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm",
|
|
62
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
63
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
64
|
-
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
62
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/index.js",
|
|
63
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/Controller.js",
|
|
64
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.36.0/dist/present.js"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
</script>
|
package/ai/tools/check.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* It starts a private static server and a private headless Chrome (nothing shared with other sessions), opens the page,
|
|
8
8
|
* waits for `window.__ready === true`, then checks: console warnings / errors, `movie.inspect` over the whole timeline
|
|
9
9
|
* (grouped), `movie.inspectAudio()`, a contact sheet (a PNG you can look at), and a real export (`movie.render`) that it
|
|
10
|
-
* decodes again: video size / length, audio length, loudness per second, silence. Output goes to `check-out/<name>/`
|
|
10
|
+
* decodes again: video size / length, audio length, loudness per second, silence. Output goes to `check-out/<name>/` (`--out DIR` writes straight into DIR, with no <name>/ inside it)
|
|
11
11
|
* (`sheet.png`, `<name>.<format>`, `report.json`; a presentation with `stops` also gets `stops.png`, one picture per stop) and a short summary is printed. Exit code 0 = nothing to fix, 1 = problems.
|
|
12
12
|
*
|
|
13
13
|
* Needs: Node >= 22 (built-in WebSocket), Chrome / Chromium installed (or --chrome PATH / CHROME=PATH). No npm dependencies.
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Options: --draft (the export is a draft: half size, low quality, no motion blur: a much smaller file; the drawing itself is not faster) · --at LIST (pictures at moments you name: 3.5, 50%, f120, title@end → frames/*.png and at.png) ·
|
|
17
17
|
* --query "a=1&b=2" (added to the page address: for a page that reads it, such as one video of a batch) ·
|
|
18
|
+
* --crop X,Y,W,H (with --at: each picture is that part of the canvas, in canvas pixels, enlarged up to 4x so a small figure can be judged) ·
|
|
18
19
|
* --onion A:B (one picture of the movement between A and B seconds, frames overlaid: onion.png) ·
|
|
19
20
|
* --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 ·
|
|
20
21
|
* --timeout SECONDS (default 240) · --root DIR (static server root; default: the nearest folder above the page with dist/) · --chrome PATH
|
|
@@ -44,8 +45,8 @@ export function formatMusic(sources) {
|
|
|
44
45
|
out.push(` music ${s.layer} · ${num(m.bpm)} bpm · ${num(m.beats)} beats = ${num(m.bars)} bars of ${m.meter} beats${assumed} · ${num(Math.round(m.seconds * 10) / 10)} s (+ ${num(m.tail)} s tail)`);
|
|
45
46
|
for (const t of m.tracks) {
|
|
46
47
|
const whole = Math.abs(t.bars - Math.round(t.bars)) < 1e-6;
|
|
47
|
-
// a track shorter than the longest stops early (whatever its length in bars); a short BAR can only be the longest track's, and only the author's meter can say so
|
|
48
|
-
const note = t.beats < m.beats - 1e-
|
|
48
|
+
// a track shorter than the longest stops early (whatever its length in bars; decimal note lengths add up with float noise, so a gap under 1e-4 of a beat is none); a short BAR can only be the longest track's, and only the author's meter can say so
|
|
49
|
+
const note = t.beats < m.beats - 1e-4 ? ` (ends early: silent from beat ${num(t.beats)})` : !whole && m.meterSet !== false ? ' (not a whole number of bars: a bar is short)' : '';
|
|
49
50
|
const cell = names => { const n = names.match(/\[[^\]]*\]\S*|\S+/g) ?? []; return n.length === 0 ? '–' : n.length > 4 ? `${n.slice(0, 4).join(' ')} …` : n.join(' '); };
|
|
50
51
|
const shown = t.perBar.slice(0, 16).map(cell).join(' | ');
|
|
51
52
|
out.push(`${pad}${t.inst.padEnd(6)} ${num(t.bars)} bar${t.bars === 1 ? '' : 's'}${note} | ${shown}${t.perBar.length > 16 ? ` | … (${t.perBar.length - 16} more)` : ''}`);
|
|
@@ -79,10 +80,10 @@ export function findChrome(env = process.env, exists = fs.existsSync, platform =
|
|
|
79
80
|
return candidates.find(c => c && exists(c)) ?? null;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
export const USAGE = 'usage: node ai/tools/check.mjs <page.html> [--out DIR] [--frames N] [--formats mp4,webm] [--no-export] [--draft] [--at 3.5,title@end] [--onion 1:3] [--query a=1&b=2] [--var name=value] [--size 1920x1080,1080x1920] [--timeout S] [--root DIR] [--chrome PATH] [--strict] [--help]\n(the header of ai/tools/check.mjs explains each option)';
|
|
83
|
+
export const USAGE = 'usage: node ai/tools/check.mjs <page.html> [--out DIR] [--frames N] [--formats mp4,webm] [--no-export] [--draft] [--at 3.5,title@end] [--crop X,Y,W,H] [--onion 1:3] [--query a=1&b=2] [--var name=value] [--size 1920x1080,1080x1920] [--timeout S] [--root DIR] [--chrome PATH] [--strict] [--help]\n(the header of ai/tools/check.mjs explains each option)';
|
|
83
84
|
|
|
84
85
|
export function parseArgs(argv) {
|
|
85
|
-
const o = { page: null, out: null, frames: 12, formats: ['mp4'], export: true, timeout: 240, root: null, chrome: null, strict: false, at: null, draft: false, onion: null, query: null, vars: [], sizes: [], help: false };
|
|
86
|
+
const o = { page: null, out: null, frames: 12, formats: ['mp4'], export: true, timeout: 240, root: null, chrome: null, strict: false, at: null, crop: null, draft: false, onion: null, query: null, vars: [], sizes: [], help: false };
|
|
86
87
|
const need = (i, name) => { if (i + 1 >= argv.length) throw new Error(`${name} needs a value`); return argv[i + 1]; };
|
|
87
88
|
for (let i = 0; i < argv.length; i++) {
|
|
88
89
|
const a = argv[i];
|
|
@@ -95,6 +96,11 @@ export function parseArgs(argv) {
|
|
|
95
96
|
else if (a === '--draft') o.draft = true;
|
|
96
97
|
else if (a === '--query') o.query = need(i++, a).replace(/^\?/, '');
|
|
97
98
|
else if (a === '--at') o.at = need(i++, a);
|
|
99
|
+
else if (a === '--crop') {
|
|
100
|
+
const m = need(i++, a).split(',').map(Number);
|
|
101
|
+
if (m.length !== 4 || !m.every(Number.isFinite) || m[0] < 0 || m[1] < 0 || m[2] < 1 || m[3] < 1) throw new Error(`--crop must look like 100,200,400,300 (x, y, width, height in canvas pixels, width and height at least 1), got "${argv[i]}"`);
|
|
102
|
+
o.crop = { x: Math.round(m[0]), y: Math.round(m[1]), width: Math.round(m[2]), height: Math.round(m[3]) };
|
|
103
|
+
}
|
|
98
104
|
else if (a === '--var') o.vars.push(parseVar(need(i++, a)));
|
|
99
105
|
else if (a === '--size') o.sizes.push(...parseSizes(need(i++, a)));
|
|
100
106
|
else if (a === '--onion') {
|
|
@@ -109,6 +115,7 @@ export function parseArgs(argv) {
|
|
|
109
115
|
else if (!o.page) o.page = a;
|
|
110
116
|
else throw new Error(`unexpected argument ${a}`);
|
|
111
117
|
}
|
|
118
|
+
if (o.crop && !o.at) throw new Error('--crop crops the pictures of --at: give --at too (for example --at 3.5 --crop 100,200,400,300)');
|
|
112
119
|
return o;
|
|
113
120
|
}
|
|
114
121
|
|
|
@@ -377,6 +384,7 @@ export async function runCheck(opts, log = console.log) {
|
|
|
377
384
|
if (r.error) throw new Error(r.error.replace(/^pixi-effects: /, ''));
|
|
378
385
|
atList = r.at;
|
|
379
386
|
}
|
|
387
|
+
if (opts.crop && (opts.crop.x + opts.crop.width > info.width || opts.crop.y + opts.crop.height > info.height)) throw new Error(`--crop ${opts.crop.x},${opts.crop.y},${opts.crop.width},${opts.crop.height} goes past the canvas (${info.width}×${info.height})`);
|
|
380
388
|
if (opts.onion && opts.onion[1] > info.duration + 1e-9) throw new Error(`--onion ${opts.onion[0]}:${opts.onion[1]} goes past the end: the movie is ${info.duration} s`);
|
|
381
389
|
|
|
382
390
|
const rv = await cdp.eval(`movie.review(${JSON.stringify({ strict: !!opts.strict })})`);
|
|
@@ -398,8 +406,16 @@ export async function runCheck(opts, log = console.log) {
|
|
|
398
406
|
const wanted = atList;
|
|
399
407
|
const framesDir = path.join(outDir, 'frames');
|
|
400
408
|
fs.mkdirSync(framesDir, { recursive: true });
|
|
409
|
+
for (const old of fs.readdirSync(framesDir)) if (old.endsWith('.png')) fs.rmSync(path.join(framesDir, old)); // the pictures of an earlier run: frames/ shows this run's moments only
|
|
410
|
+
// --crop: that part of the canvas, enlarged by a whole factor (up to 4x, the longer side to about 800 px) with no smoothing, so small figures and fine lines can be judged
|
|
411
|
+
const crop = opts.crop ? { ...opts.crop, scale: Math.max(1, Math.min(4, Math.floor(800 / Math.max(opts.crop.width, opts.crop.height)))) } : null;
|
|
412
|
+
const cropped = [];
|
|
401
413
|
for (const w of wanted) {
|
|
402
|
-
|
|
414
|
+
let png = await cdp.eval(`movie.snapshot(${w.frame}, { as: 'dataURL' })`);
|
|
415
|
+
if (crop) {
|
|
416
|
+
png = await cdp.eval(`(async (url, c) => { const img = new Image(); img.src = url; await img.decode(); const cv = document.createElement('canvas'); cv.width = c.width * c.scale; cv.height = c.height * c.scale; const g = cv.getContext('2d'); g.imageSmoothingEnabled = false; g.drawImage(img, c.x, c.y, c.width, c.height, 0, 0, cv.width, cv.height); return cv.toDataURL('image/png'); })(${JSON.stringify(png)}, ${JSON.stringify(crop)})`);
|
|
417
|
+
cropped.push(png);
|
|
418
|
+
}
|
|
403
419
|
fs.writeFileSync(path.join(framesDir, `${w.label}.png`), Buffer.from(png.slice(png.indexOf(',') + 1), 'base64'));
|
|
404
420
|
// depth of field: the blur (px) each threeD layer has at this moment, so a focus pull can be read as numbers (0 = sharp)
|
|
405
421
|
const rows = await cdp.eval(`movie.inspect(${w.frame}).then(r => r.layers.filter(l => l.depthBlur !== undefined).map(l => [l.path, l.depthBlur]))`).catch(() => []);
|
|
@@ -408,12 +424,15 @@ export async function runCheck(opts, log = console.log) {
|
|
|
408
424
|
const lit = await cdp.eval(`movie.inspect(${w.frame}).then(r => r.layers.filter(l => l.light !== undefined).map(l => [l.path, l.light]))`).catch(() => []);
|
|
409
425
|
if (lit.length) w.light = Object.fromEntries(lit);
|
|
410
426
|
}
|
|
411
|
-
const atSheet =
|
|
427
|
+
const atSheet = crop
|
|
428
|
+
? await cdp.eval(`(async (urls) => { const imgs = await Promise.all(urls.map(async u => { const i = new Image(); i.src = u; await i.decode(); return i; })); const cols = Math.min(imgs.length, 3), rows = Math.ceil(imgs.length / cols), w = imgs[0].width, h = imgs[0].height; const cv = document.createElement('canvas'); cv.width = cols * w; cv.height = rows * h; const g = cv.getContext('2d'); imgs.forEach((im, k) => g.drawImage(im, (k % cols) * w, Math.floor(k / cols) * h)); return cv.toDataURL('image/png'); })(${JSON.stringify(cropped)})`)
|
|
429
|
+
: await cdp.eval(`movie.contactSheet({ frames: ${JSON.stringify(wanted.map(w => w.frame))}, as: 'dataURL' })`);
|
|
412
430
|
const atFile = path.join(outDir, 'at.png');
|
|
413
431
|
fs.writeFileSync(atFile, Buffer.from(atSheet.slice(atSheet.indexOf(',') + 1), 'base64'));
|
|
414
432
|
report.files.at = shown(atFile);
|
|
415
433
|
report.files.frames = shown(framesDir);
|
|
416
434
|
report.at = wanted;
|
|
435
|
+
if (crop) report.crop = crop;
|
|
417
436
|
}
|
|
418
437
|
|
|
419
438
|
// --onion A:B: one picture of the movement between A and B seconds (8 frames overlaid, the later the stronger)
|
package/ai/tools/lookup.mjs
CHANGED
|
@@ -105,9 +105,19 @@ export function searchDocs(root, word) {
|
|
|
105
105
|
const heading = s.heading.toLowerCase();
|
|
106
106
|
const body = s.lines.join('\n').toLowerCase();
|
|
107
107
|
if (!tokens.every(t => body.includes(t))) continue;
|
|
108
|
-
const
|
|
108
|
+
const flat = (x) => x.replace(/[^a-z0-9]/g, ''); // "motionBlur" is the heading "Motion blur"
|
|
109
|
+
const inHeading = tokens.every(t => heading.includes(t) || (flat(t).length > 2 && flat(heading).includes(flat(t))));
|
|
109
110
|
const count = tokens.reduce((n, t) => n + body.split(t).length - 1, 0);
|
|
110
|
-
|
|
111
|
+
// the word as a table row (`| \`path\` | …`, the first cell) or as a call in code (`steps(n)`) says the section is ABOUT it, not that it only mentions it
|
|
112
|
+
const row = tokens.some(t => s.lines.some(l => new RegExp(`^\\|\\s*\\*{0,2}\\x60${t.replace(/[^a-z0-9]/g, '')}\\x60`).test(l.toLowerCase())));
|
|
113
|
+
const calls = (t) => { // the word followed by a bracket INSIDE code: a fenced block or an inline `span` (not prose that has a bracket after it)
|
|
114
|
+
const w = t.replace(/[^a-z0-9]/g, ''), re = new RegExp(`\\b${w}\\s*\\(`);
|
|
115
|
+
const fences = [...body.matchAll(/```[\s\S]*?```/g)].map(m => m[0]);
|
|
116
|
+
const inline = [...body.replace(/```[\s\S]*?```/g, ' ').matchAll(/`([^`\n]+)`/g)].map(m => m[1]);
|
|
117
|
+
return [...fences, ...inline].some(code => re.test(code));
|
|
118
|
+
};
|
|
119
|
+
const call = tokens.some(calls);
|
|
120
|
+
scored.push({ s, score: (inHeading ? 1000 : 0) + (row ? 1100 : 0) + (call ? 300 : 0) + Math.min(count, 50) + bonus });
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
123
|
scored.sort((a, b) => b.score - a.score);
|
|
@@ -155,6 +165,8 @@ export function newPage(name, { cwd = process.cwd(), root = PACKAGE_ROOT, force
|
|
|
155
165
|
.replace('<title>pixi-effects video</title>', `<title>${base.replace(/&/g, '&').replace(/</g, '<')}</title>`)
|
|
156
166
|
// the size sits in two places (the canvas attributes and W / H) and the length in one; the poster follows DURATION
|
|
157
167
|
.replace('<canvas id="stage" width="1280" height="720">', size ? `<canvas id="stage" width="${size.width}" height="${size.height}">` : '<canvas id="stage" width="1280" height="720">')
|
|
168
|
+
// a tall or square page would be 960 px wide and far taller than a laptop screen: its width follows a height of about 760 px
|
|
169
|
+
.replace('.stage { position: relative; width: min(960px, 100%); }', size ? `.stage { position: relative; width: min(${Math.min(960, Math.round(760 * size.width / size.height))}px, 100%); }` : '.stage { position: relative; width: min(960px, 100%); }')
|
|
158
170
|
.replace('const W = 1280, H = 720, FPS = 30, DURATION = 6;', `const W = ${size ? size.width : 1280}, H = ${size ? size.height : 720}, FPS = 30, DURATION = ${duration ?? 6};`);
|
|
159
171
|
fs.writeFileSync(file, html);
|
|
160
172
|
return { file };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Container } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape } from './types-
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape } from './types-B6zDTU2V.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a routed keyframe path to a concrete tween target.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Container } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape } from './types-
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape } from './types-B6zDTU2V.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a routed keyframe path to a concrete tween target.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { CompositionSequence } from './chunk-AZ6AINSW.js';
|
|
2
|
+
import './chunk-G3DX65HO.js';
|
|
3
|
+
import './chunk-WIIU42FH.js';
|
|
4
|
+
import './chunk-2JFFKHXO.js';
|
|
5
|
+
import './chunk-BXOCEZ4U.js';
|
|
6
|
+
//# sourceMappingURL=Composition-BMRCE5FH.js.map
|
|
7
|
+
//# sourceMappingURL=Composition-BMRCE5FH.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-BMRCE5FH.js"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkY5UWFH6Y_cjs = require('./chunk-Y5UWFH6Y.cjs');
|
|
4
|
+
require('./chunk-KJSEQVBV.cjs');
|
|
5
|
+
require('./chunk-NEUPEENI.cjs');
|
|
6
|
+
require('./chunk-42ML5AKN.cjs');
|
|
7
|
+
require('./chunk-TWZNOUM7.cjs');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Object.defineProperty(exports, "CompositionSequence", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: function () { return chunkY5UWFH6Y_cjs.CompositionSequence; }
|
|
14
|
+
});
|
|
15
|
+
//# sourceMappingURL=Composition-XRLYR6GL.cjs.map
|
|
16
|
+
//# sourceMappingURL=Composition-XRLYR6GL.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-XRLYR6GL.cjs"}
|
package/dist/Controller.cjs
CHANGED
|
@@ -63,6 +63,19 @@ var CONTROLLER_CSS = `
|
|
|
63
63
|
.mc-progress:hover .mc-progress-thumb,
|
|
64
64
|
.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }
|
|
65
65
|
|
|
66
|
+
.movie-controller > .mc-preview {
|
|
67
|
+
position: absolute; bottom: 74px; left: 0;
|
|
68
|
+
transform: translateX(-50%);
|
|
69
|
+
display: flex; flex-direction: column; align-items: center; gap: 4px;
|
|
70
|
+
padding: 3px; border-radius: 4px;
|
|
71
|
+
background: rgba(0,0,0,0.75);
|
|
72
|
+
pointer-events: none;
|
|
73
|
+
opacity: 0; transition: opacity 100ms ease;
|
|
74
|
+
}
|
|
75
|
+
.mc-preview[data-show="1"] { opacity: 1; }
|
|
76
|
+
.mc-preview canvas { display: block; width: 160px; height: auto; border-radius: 2px; }
|
|
77
|
+
.mc-preview-time { font-size: 12px; line-height: 1; color: var(--mc-fg, #fff); font-variant-numeric: tabular-nums; }
|
|
78
|
+
|
|
66
79
|
.mc-bar {
|
|
67
80
|
position: relative;
|
|
68
81
|
display: flex;
|
|
@@ -295,7 +308,7 @@ var THEME_PROPERTIES = {
|
|
|
295
308
|
trackHeight: "--mc-track-height",
|
|
296
309
|
font: "--mc-font"
|
|
297
310
|
};
|
|
298
|
-
var CONTROLLER_OPTION_KEYS = ["canvas", "theme", "exportButton", "keyboard", "clickToPlay", "presentButton", "pauseAtStops", "loopButton", "className"];
|
|
311
|
+
var CONTROLLER_OPTION_KEYS = ["canvas", "theme", "exportButton", "keyboard", "clickToPlay", "presentButton", "pauseAtStops", "loopButton", "thumbnails", "className"];
|
|
299
312
|
var Controller = class _Controller {
|
|
300
313
|
movie;
|
|
301
314
|
options;
|
|
@@ -341,6 +354,17 @@ var Controller = class _Controller {
|
|
|
341
354
|
onReady = null;
|
|
342
355
|
/** A rebuild (setVars, resize, rebuild) may have moved the stops: draw their marks again. */
|
|
343
356
|
onRebuilt = null;
|
|
357
|
+
previewEl = null;
|
|
358
|
+
previewCanvas = null;
|
|
359
|
+
previewTimeEl = null;
|
|
360
|
+
/** Pictures already made, by frame bucket (a frame is a pure function of time; cleared on a rebuild). */
|
|
361
|
+
previewCache = /* @__PURE__ */ new Map();
|
|
362
|
+
previewWanted = null;
|
|
363
|
+
previewRunning = false;
|
|
364
|
+
previewRetry = null;
|
|
365
|
+
previewRetries = 0;
|
|
366
|
+
previewCleanup = null;
|
|
367
|
+
static PREVIEW_WIDTH_PX = 160;
|
|
344
368
|
presenter = null;
|
|
345
369
|
/** `pixi-effects/present`, loaded as soon as the movie has stops (a press of play must not wait for it). */
|
|
346
370
|
present = null;
|
|
@@ -387,6 +411,7 @@ var Controller = class _Controller {
|
|
|
387
411
|
presentButton: options.presentButton ?? true,
|
|
388
412
|
pauseAtStops: options.pauseAtStops ?? true,
|
|
389
413
|
loopButton: options.loopButton ?? true,
|
|
414
|
+
thumbnails: options.thumbnails ?? false,
|
|
390
415
|
className: options.className ?? "movie-controller"
|
|
391
416
|
};
|
|
392
417
|
installStyles();
|
|
@@ -409,6 +434,7 @@ var Controller = class _Controller {
|
|
|
409
434
|
this.bindPlayButton();
|
|
410
435
|
if (this.options.clickToPlay) this.bindCanvasClick();
|
|
411
436
|
this.bindScrubbing();
|
|
437
|
+
if (this.options.thumbnails) this.bindPreview();
|
|
412
438
|
this.bindMuteButton();
|
|
413
439
|
this.bindLoopButton();
|
|
414
440
|
this.bindVolumeSlider();
|
|
@@ -588,6 +614,7 @@ var Controller = class _Controller {
|
|
|
588
614
|
};
|
|
589
615
|
this.onRebuilt = () => {
|
|
590
616
|
this.refreshStops();
|
|
617
|
+
this.previewCache.clear();
|
|
591
618
|
};
|
|
592
619
|
this.movie.on("ready", this.onReady);
|
|
593
620
|
this.movie.on("rebuilt", this.onRebuilt);
|
|
@@ -911,6 +938,104 @@ var Controller = class _Controller {
|
|
|
911
938
|
};
|
|
912
939
|
window.addEventListener("blur", this.windowBlurHandler);
|
|
913
940
|
}
|
|
941
|
+
/** The hover preview above the seek bar (`thumbnails: true`). */
|
|
942
|
+
bindPreview() {
|
|
943
|
+
const el = document.createElement("div");
|
|
944
|
+
el.className = "mc-preview";
|
|
945
|
+
el.setAttribute("aria-hidden", "true");
|
|
946
|
+
const canvas = document.createElement("canvas");
|
|
947
|
+
const time = document.createElement("span");
|
|
948
|
+
time.className = "mc-preview-time";
|
|
949
|
+
el.append(canvas, time);
|
|
950
|
+
this.root.appendChild(el);
|
|
951
|
+
this.previewEl = el;
|
|
952
|
+
this.previewCanvas = canvas;
|
|
953
|
+
this.previewTimeEl = time;
|
|
954
|
+
const hide = () => {
|
|
955
|
+
this.previewWanted = null;
|
|
956
|
+
el.removeAttribute("data-show");
|
|
957
|
+
};
|
|
958
|
+
const onMove = (e) => {
|
|
959
|
+
if (this.isScrubbing || this.movie.thumbnailsOff || e.pointerType === "touch") {
|
|
960
|
+
hide();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const rect = this.progressEl.getBoundingClientRect();
|
|
964
|
+
const inset = _Controller.PROGRESS_INSET_PX;
|
|
965
|
+
const frame = pxToFrame(e.clientX, { left: rect.left + inset, width: Math.max(0, rect.width - 2 * inset) }, this.movie.totalFrames);
|
|
966
|
+
const rootRect = this.root.getBoundingClientRect();
|
|
967
|
+
const half = el.getBoundingClientRect().width / 2 + 2;
|
|
968
|
+
const x = Math.min(Math.max(e.clientX - rootRect.left, half), Math.max(half, rootRect.width - half));
|
|
969
|
+
el.style.left = `${rootRect.width > 0 ? x / rootRect.width * 100 : 50}%`;
|
|
970
|
+
this.previewRetries = 0;
|
|
971
|
+
time.textContent = formatTime(frame / (this.movie.frameRate || 1));
|
|
972
|
+
this.previewWanted = frame;
|
|
973
|
+
void this.runPreview();
|
|
974
|
+
};
|
|
975
|
+
this.progressEl.addEventListener("pointermove", onMove);
|
|
976
|
+
this.progressEl.addEventListener("pointerleave", hide);
|
|
977
|
+
this.progressEl.addEventListener("pointerdown", hide);
|
|
978
|
+
this.previewCleanup = () => {
|
|
979
|
+
this.progressEl.removeEventListener("pointermove", onMove);
|
|
980
|
+
this.progressEl.removeEventListener("pointerleave", hide);
|
|
981
|
+
this.progressEl.removeEventListener("pointerdown", hide);
|
|
982
|
+
if (this.previewRetry) {
|
|
983
|
+
clearTimeout(this.previewRetry);
|
|
984
|
+
this.previewRetry = null;
|
|
985
|
+
}
|
|
986
|
+
el.remove();
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/** Draws the picture of the wanted frame into the preview; one at a time, and the last wish wins. */
|
|
990
|
+
async runPreview() {
|
|
991
|
+
if (this.previewRunning) return;
|
|
992
|
+
this.previewRunning = true;
|
|
993
|
+
try {
|
|
994
|
+
while (this.previewWanted !== null && !this.destroyed) {
|
|
995
|
+
const wanted = this.previewWanted;
|
|
996
|
+
const fps = this.movie.frameRate || 30;
|
|
997
|
+
const step = Math.max(1, Math.round(fps / 10));
|
|
998
|
+
const key = Math.min(this.movie.totalFrames, Math.round(wanted / step) * step);
|
|
999
|
+
let pic = this.previewCache.get(key) ?? null;
|
|
1000
|
+
if (!pic) {
|
|
1001
|
+
try {
|
|
1002
|
+
pic = await this.movie.thumbnail(key, { width: _Controller.PREVIEW_WIDTH_PX });
|
|
1003
|
+
} catch {
|
|
1004
|
+
this.previewWanted = null;
|
|
1005
|
+
this.previewEl?.removeAttribute("data-show");
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
if (pic) {
|
|
1009
|
+
if (this.previewCache.size >= 120) this.previewCache.delete(this.previewCache.keys().next().value);
|
|
1010
|
+
this.previewCache.set(key, pic);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (this.previewWanted === null || this.destroyed) break;
|
|
1014
|
+
if (pic) {
|
|
1015
|
+
const c = this.previewCanvas;
|
|
1016
|
+
if (c.width !== pic.width || c.height !== pic.height) {
|
|
1017
|
+
c.width = pic.width;
|
|
1018
|
+
c.height = pic.height;
|
|
1019
|
+
}
|
|
1020
|
+
c.getContext("2d").drawImage(pic, 0, 0);
|
|
1021
|
+
this.previewEl.setAttribute("data-show", "1");
|
|
1022
|
+
if (this.previewWanted === wanted) this.previewWanted = null;
|
|
1023
|
+
} else if (this.previewWanted !== wanted) {
|
|
1024
|
+
} else if (!this.movie.thumbnailsOff && this.previewRetries++ < 10) {
|
|
1025
|
+
this.previewRetry = setTimeout(() => {
|
|
1026
|
+
this.previewRetry = null;
|
|
1027
|
+
void this.runPreview();
|
|
1028
|
+
}, 120);
|
|
1029
|
+
break;
|
|
1030
|
+
} else {
|
|
1031
|
+
this.previewWanted = null;
|
|
1032
|
+
this.previewEl?.removeAttribute("data-show");
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
} finally {
|
|
1036
|
+
this.previewRunning = false;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
914
1039
|
seekFromPointer(clientX) {
|
|
915
1040
|
const rect = this.progressEl.getBoundingClientRect();
|
|
916
1041
|
const inset = _Controller.PROGRESS_INSET_PX;
|
|
@@ -1187,6 +1312,10 @@ var Controller = class _Controller {
|
|
|
1187
1312
|
if (this.destroyed) return;
|
|
1188
1313
|
this.destroyed = true;
|
|
1189
1314
|
this.stopPresenting();
|
|
1315
|
+
if (this.previewCleanup) {
|
|
1316
|
+
this.previewCleanup();
|
|
1317
|
+
this.previewCleanup = null;
|
|
1318
|
+
}
|
|
1190
1319
|
if (this.onReady) {
|
|
1191
1320
|
this.movie.off("ready", this.onReady);
|
|
1192
1321
|
this.onReady = null;
|