pixi-effects 0.29.0 → 0.30.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 +7 -0
- package/README.md +6 -5
- package/ai/chat-template.html +3 -3
- package/ai/tools/lookup.mjs +208 -0
- package/ai/tools/pixi-effects.mjs +15 -4
- package/dist/loader.css +1 -1
- package/llms-full.txt +36 -3
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/skills/pixi-effects/SKILL.md +3 -3
- package/skills/pixi-effects/reference/quickstart.md +29 -0
- package/skills/pixi-effects/template.html +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.30.0
|
|
4
|
+
|
|
5
|
+
**Added**
|
|
6
|
+
|
|
7
|
+
- **`pixi-effects new <name>`, `recipe [id]` and `help <word>`: read what you need, not 100 KB.** `new` writes `<name>.html` from the template, titled with the name and pinned to the installed version (it will not overwrite without `--force`). `recipe` lists the tested recipes (id and title); with an id (or a unique part of one) it prints that recipe's text and code; an id nobody has says the nearest one. `help <word>` prints the sections of the docs about a word (`docs/dsl.md`, the cheatsheet, the pitfalls, `docs/api.md`, `direction.md`), headings first, each cut to 30 lines with where the rest is, never more than 170 lines in all.
|
|
8
|
+
- **`skills/pixi-effects/reference/quickstart.md`** (under 2 KB, a test holds it there): the five commands, a title card that the recipe tests build and lint, and the rules that cause most failures. `SKILL.md`, `AGENTS.md`, `llms.txt` and the README start an agent there; the 35 KB cheatsheet and the 41 KB pitfalls are for looking things up.
|
|
9
|
+
|
|
3
10
|
## 0.29.0
|
|
4
11
|
|
|
5
12
|
**Changed (breaking): presentation leaves the core**
|
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.30.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.30.0/dist/index.js",
|
|
53
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.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.30.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.30.0/dist/three.js"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
</script>
|
|
@@ -174,6 +174,7 @@ This library is designed to be written by AI: a video is plain data, and every m
|
|
|
174
174
|
- [`skills/pixi-effects/SKILL.md`](./skills/pixi-effects/SKILL.md) — a [skill](https://docs.claude.com/en/docs/claude-code/skills) (workflow, rules, verification loop). Install it with `npx skills add yjmtmtk/pixi-effects` (or copy the `skills/pixi-effects/` folder to `~/.claude/skills/pixi-effects/` or your project's `.claude/skills/`) and your agent loads it by itself when you ask for a video.
|
|
175
175
|
- [`skills/pixi-effects/reference/cheatsheet.md`](./skills/pixi-effects/reference/cheatsheet.md), [`recipes.md`](./skills/pixi-effects/reference/recipes.md) (tested), [`pitfalls.md`](./skills/pixi-effects/reference/pitfalls.md), and a starter [`skills/pixi-effects/template.html`](./skills/pixi-effects/template.html).
|
|
176
176
|
|
|
177
|
+
- **Start a page, and read only what you need:** `npx pixi-effects new my-video` writes `my-video.html` from the template; `npx pixi-effects recipe [id]` lists the tested recipes and prints one with its code; `npx pixi-effects help <word>` prints the sections of the docs about a word (`mask`, `threeD`, `keyframes`, `music` …). An AI starts from the 2 KB [quick start](./skills/pixi-effects/reference/quickstart.md), not from the cheatsheet.
|
|
177
178
|
- **Look at a page with its timeline, and scrub one with the other:** `npx pixi-effects view my-video.html` (it also watches the folder: when the work changes it shows "Updated hh:mm:ss — reload (R)" and never switches by itself; reload comes back to the same frame) (or `node ai/tools/view.mjs …`) opens your browser on a viewer: the page on top, the timeline of every layer under it with a playhead that follows the movie. Click or drag the timeline to seek, click a layer's name to jump to where it starts, Space plays, ← / → step a frame, + / − (or Ctrl/⌘ + wheel) zoom, the names stay in place. For the human who wants to see what the AI made.
|
|
178
179
|
- **Render a page to a video file, headless:** `npx pixi-effects render my-video.html -o my-video.mp4` (or `node ai/tools/render.mjs …`; Node ≥ 22 and Chrome installed, no dependencies). The container follows the extension (`mp4 webm mov mkv`); `--quality very-low…very-high`, `--query lang=ja` (added to the page URL), `--strict` (exit 1 when the page logged a warning), `--quiet`. It waits for `window.__ready`, runs `movie.render()` and streams the file to disk; exit 0 = written, 1 = the page or the render failed (no file). For scripts, CI and batches; `pixi-effects check` below is the review.
|
|
179
180
|
- **A talk as a PDF:** `npx pixi-effects render my-talk.html -o my-talk.pdf` (one page per page of a `deck()` / `stops` movie; `--all-stops` makes a page of every step). The review tool writes `stops.png` for such a page, one picture per stop.
|
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.30.0/dist/index.js",
|
|
63
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.0/dist/Controller.js",
|
|
64
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.0/dist/present.js"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
</script>
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// pixi-effects recipe | help | new: let an agent read what it needs instead of a 35 KB cheatsheet and a 69 KB list of recipes.
|
|
2
|
+
//
|
|
3
|
+
// npx pixi-effects recipe the recipes (id and title)
|
|
4
|
+
// npx pixi-effects recipe <id> one recipe: its text and its code (a part of an id works when it is unique)
|
|
5
|
+
// npx pixi-effects help <word> the sections of the docs about a word (mask, threeD, keyframes, music …)
|
|
6
|
+
// npx pixi-effects new <name> <name>.html from the template, pinned to this package's version
|
|
7
|
+
//
|
|
8
|
+
// Zero dependencies. The texts are the ones the package ships (docs/dsl.md, docs/api.md, skills/pixi-effects/reference/*.md, skills/pixi-effects/template.html).
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
14
|
+
const RECIPES = 'skills/pixi-effects/reference/recipes.md';
|
|
15
|
+
/** What `help` reads, most useful first (the number is a small bonus in the ranking). */
|
|
16
|
+
const CORPUS = [
|
|
17
|
+
['docs/dsl.md', 10], ['skills/pixi-effects/reference/cheatsheet.md', 8], ['skills/pixi-effects/reference/pitfalls.md', 6],
|
|
18
|
+
['docs/api.md', 4], ['skills/pixi-effects/reference/direction.md', 2],
|
|
19
|
+
];
|
|
20
|
+
const MAX_HITS = 5, MAX_LINES_PER_HIT = 30, MAX_OUTPUT_LINES = 170;
|
|
21
|
+
|
|
22
|
+
function distance(a, b) {
|
|
23
|
+
const m = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
24
|
+
for (let j = 1; j <= b.length; j++) m[0][j] = j;
|
|
25
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) {
|
|
26
|
+
m[i][j] = Math.min(m[i - 1][j] + 1, m[i][j - 1] + 1, m[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
27
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) m[i][j] = Math.min(m[i][j], m[i - 2][j - 2] + 1);
|
|
28
|
+
}
|
|
29
|
+
return m[a.length][b.length];
|
|
30
|
+
}
|
|
31
|
+
const nearestOf = (word, list) => {
|
|
32
|
+
let best = null;
|
|
33
|
+
for (const c of list) { const d = distance(word.toLowerCase(), c.toLowerCase()); if (d <= Math.max(2, Math.floor(c.length / 3)) && (!best || d < best.d)) best = { c, d }; }
|
|
34
|
+
return best ? best.c : undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The "## " sections of a markdown text (a heading inside a code fence is not one). Each: { title, text, line (1-based), ids (the names after // @recipe or // @docs-only) }. */
|
|
38
|
+
export function parseRecipes(md) {
|
|
39
|
+
const lines = md.split('\n');
|
|
40
|
+
const sections = [];
|
|
41
|
+
let fence = false, cur = null;
|
|
42
|
+
lines.forEach((line, i) => {
|
|
43
|
+
if (/^```/.test(line)) fence = !fence;
|
|
44
|
+
const h = !fence && /^## (.+)$/.exec(line);
|
|
45
|
+
if (h) { cur = { title: h[1].trim(), lines: [line], line: i + 1 }; sections.push(cur); }
|
|
46
|
+
else if (cur) cur.lines.push(line);
|
|
47
|
+
});
|
|
48
|
+
return sections.map(s => {
|
|
49
|
+
const body = [...s.lines];
|
|
50
|
+
while (body.length && /^(---|\s*)$/.test(body[body.length - 1])) body.pop(); // the separator belongs to nobody
|
|
51
|
+
const text = body.join('\n');
|
|
52
|
+
return { title: s.title, text, line: s.line, ids: [...text.matchAll(/\/\/ @(?:recipe|docs-only)[ \t]+([A-Za-z0-9_-]+)/g)].map(m => m[1]) };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** { section, id } for an exact id or a unique part of one (or of a title); { many } when several match; { nearest } (maybe undefined) when none does. */
|
|
57
|
+
export function findRecipe(sections, query) {
|
|
58
|
+
const q = query.trim().toLowerCase();
|
|
59
|
+
const exact = sections.find(s => s.ids.some(id => id.toLowerCase() === q));
|
|
60
|
+
if (exact) return { section: exact, id: exact.ids.find(id => id.toLowerCase() === q) };
|
|
61
|
+
const part = sections.filter(s => s.ids.some(id => id.toLowerCase().includes(q)) || s.title.toLowerCase().includes(q));
|
|
62
|
+
if (part.length === 1) return { section: part[0] };
|
|
63
|
+
if (part.length > 1) return { many: part };
|
|
64
|
+
return { nearest: nearestOf(q, sections.flatMap(s => s.ids)) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const readIf = (file) => { try { return fs.readFileSync(file, 'utf8'); } catch { return null; } };
|
|
68
|
+
|
|
69
|
+
/** Sections of a doc: a "## / ### / ####" heading each; for the pitfalls, a numbered item each ("84. **Subtract …**"). */
|
|
70
|
+
function docSections(text, file) {
|
|
71
|
+
const out = [];
|
|
72
|
+
const lines = text.split('\n');
|
|
73
|
+
const pit = /pitfalls\.md$/.test(file);
|
|
74
|
+
let fence = false, cur = null;
|
|
75
|
+
lines.forEach((line, i) => {
|
|
76
|
+
if (/^```/.test(line)) fence = !fence;
|
|
77
|
+
let heading = null;
|
|
78
|
+
if (!fence) {
|
|
79
|
+
const h = /^#{2,4} (.+)$/.exec(line);
|
|
80
|
+
const p = pit && /^(\d+[a-z]?)\. \*\*(.+?)\*\*/.exec(line);
|
|
81
|
+
if (h) heading = h[1].trim();
|
|
82
|
+
else if (p) heading = `pitfall ${p[1]}: ${p[2].replace(/[`*]/g, '')}`.slice(0, 100);
|
|
83
|
+
}
|
|
84
|
+
if (heading) { cur = { file, heading, line: i + 1, lines: [line] }; out.push(cur); }
|
|
85
|
+
else if (cur) cur.lines.push(line);
|
|
86
|
+
});
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The sections of the docs about `word` (all its words must appear), best first: [{ file, line, heading, lines, more }]. */
|
|
91
|
+
export function searchDocs(root, word) {
|
|
92
|
+
const tokens = String(word).toLowerCase().split(/\s+/).filter(Boolean);
|
|
93
|
+
if (!tokens.length) return [];
|
|
94
|
+
const scored = [];
|
|
95
|
+
for (const [file, bonus] of CORPUS) {
|
|
96
|
+
const text = readIf(path.join(root, file));
|
|
97
|
+
if (!text) continue;
|
|
98
|
+
for (const s of docSections(text, file)) {
|
|
99
|
+
const heading = s.heading.toLowerCase();
|
|
100
|
+
const body = s.lines.join('\n').toLowerCase();
|
|
101
|
+
if (!tokens.every(t => body.includes(t))) continue;
|
|
102
|
+
const inHeading = tokens.every(t => heading.includes(t));
|
|
103
|
+
const count = tokens.reduce((n, t) => n + body.split(t).length - 1, 0);
|
|
104
|
+
scored.push({ s, score: (inHeading ? 1000 : 0) + Math.min(count, 50) + bonus });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
scored.sort((a, b) => b.score - a.score);
|
|
108
|
+
return scored.slice(0, MAX_HITS).map(({ s }) => {
|
|
109
|
+
const body = s.lines.filter((l, i) => i === 0 || l.trim() !== '' || true);
|
|
110
|
+
const shown = body.slice(0, MAX_LINES_PER_HIT);
|
|
111
|
+
return { file: s.file, line: s.line, heading: s.heading, lines: shown, more: Math.max(0, body.length - shown.length) };
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The lines `help` prints for those hits (the whole answer stays under MAX_OUTPUT_LINES), with the next step at the end. */
|
|
116
|
+
export function formatHelp(hits, word, root = PACKAGE_ROOT) {
|
|
117
|
+
const out = [];
|
|
118
|
+
const share = Math.max(8, Math.floor((MAX_OUTPUT_LINES - 6 - hits.length * 3) / Math.max(1, hits.length)));
|
|
119
|
+
for (const h of hits) {
|
|
120
|
+
out.push(`── ${h.file}:${h.line} ${h.heading}`);
|
|
121
|
+
const lines = h.lines.slice(0, share);
|
|
122
|
+
out.push(...lines);
|
|
123
|
+
const more = h.more + (h.lines.length - lines.length);
|
|
124
|
+
if (more > 0) out.push(`… ${more} more lines: ${h.file}:${h.line}`);
|
|
125
|
+
out.push('');
|
|
126
|
+
}
|
|
127
|
+
const md = readIf(path.join(root, RECIPES));
|
|
128
|
+
const ids = md ? parseRecipes(md).filter(s => String(word).toLowerCase().split(/\s+/).every(t => s.title.toLowerCase().includes(t) || s.ids.some(id => id.includes(t)))).flatMap(s => s.ids).slice(0, 6) : [];
|
|
129
|
+
out.push(`next: pixi-effects help <another word>${ids.length ? ` · recipes about it: ${ids.map(i => `pixi-effects recipe ${i}`).join(', ')}` : ' · pixi-effects recipe (the tested pieces to copy)'}`);
|
|
130
|
+
return out.slice(0, MAX_OUTPUT_LINES);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** <name>.html from the template, titled with the name, its CDN pins set to this package's version. Throws if the file exists (without `force`). */
|
|
134
|
+
export function newPage(name, { cwd = process.cwd(), root = PACKAGE_ROOT, force = false } = {}) {
|
|
135
|
+
if (!name || !String(name).trim()) throw new Error('give the page a name: pixi-effects new my-video');
|
|
136
|
+
const raw = String(name).trim();
|
|
137
|
+
const base = path.basename(raw).replace(/\.html?$/i, '');
|
|
138
|
+
const dir = path.dirname(raw);
|
|
139
|
+
const file = path.resolve(cwd, dir, base.replace(/[\\/:*?"<>|]/g, '').replace(/\s+/g, '-') + '.html');
|
|
140
|
+
if (!fs.existsSync(path.dirname(file))) throw new Error(`the folder ${path.dirname(file)} does not exist`);
|
|
141
|
+
if (fs.existsSync(file) && !force) throw new Error(`${file} already exists (give another name, or --force to replace it)`);
|
|
142
|
+
const template = readIf(path.join(root, 'skills/pixi-effects/template.html'));
|
|
143
|
+
if (!template) throw new Error('the template is missing from this package (skills/pixi-effects/template.html)');
|
|
144
|
+
const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
|
|
145
|
+
const html = template
|
|
146
|
+
.replace(/pixi-effects@\d+\.\d+\.\d+/g, `pixi-effects@${version}`)
|
|
147
|
+
.replace('<title>pixi-effects video</title>', `<title>${base.replace(/&/g, '&').replace(/</g, '<')}</title>`);
|
|
148
|
+
fs.writeFileSync(file, html);
|
|
149
|
+
return { file };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const shownPath = (file, cwd) => { const r = path.relative(cwd, file); return r.startsWith('..') || path.isAbsolute(r) ? file : r; };
|
|
153
|
+
|
|
154
|
+
export const LOOKUP_COMMANDS = ['recipe', 'help', 'new'];
|
|
155
|
+
|
|
156
|
+
/** Run one of the commands: { code, stdout, stderr } (nothing is printed here). */
|
|
157
|
+
export function runLookup(cmd, args, { cwd = process.cwd(), root = PACKAGE_ROOT } = {}) {
|
|
158
|
+
const ok = (stdout) => ({ code: 0, stdout: stdout.endsWith('\n') ? stdout : stdout + '\n', stderr: '' });
|
|
159
|
+
const fail = (stderr, code = 1) => ({ code, stdout: '', stderr: stderr.endsWith('\n') ? stderr : stderr + '\n' });
|
|
160
|
+
if (cmd === 'recipe') {
|
|
161
|
+
const md = readIf(path.join(root, RECIPES));
|
|
162
|
+
if (!md) return fail(`the recipes are missing from this package (${RECIPES})`);
|
|
163
|
+
const sections = parseRecipes(md);
|
|
164
|
+
const query = args.join(' ').trim();
|
|
165
|
+
if (!query) {
|
|
166
|
+
const lines = ['recipes (pixi-effects recipe <id> prints one with its code):'];
|
|
167
|
+
for (const s of sections) for (const id of s.ids) lines.push(` ${id.padEnd(28)} ${s.title}`);
|
|
168
|
+
return ok(lines.join('\n'));
|
|
169
|
+
}
|
|
170
|
+
const r = findRecipe(sections, query);
|
|
171
|
+
if (r.section) return ok(r.section.text);
|
|
172
|
+
if (r.many) return fail(`several recipes match "${query}": ${r.many.map(s => s.ids[0] ?? s.title).join(', ')}. Give one id (pixi-effects recipe lists them).`);
|
|
173
|
+
return fail(`no recipe "${query}"${r.nearest ? `; did you mean "${r.nearest}"?` : ''} (pixi-effects recipe lists them)`);
|
|
174
|
+
}
|
|
175
|
+
if (cmd === 'help') {
|
|
176
|
+
const word = args.join(' ').trim();
|
|
177
|
+
if (!word) {
|
|
178
|
+
return ok([
|
|
179
|
+
'pixi-effects help <word>: the sections of the docs about a word. Try:',
|
|
180
|
+
' layers text shape image video audio composition camera light shader null',
|
|
181
|
+
' motion keyframes ease stagger wiggle spring speed time expression',
|
|
182
|
+
' look mask filters blendMode gradient transition threeD depth of field',
|
|
183
|
+
' sound sfx music audioEnvelope',
|
|
184
|
+
' checking check inspect contactSheet render stops',
|
|
185
|
+
'pixi-effects recipe lists the tested pieces to copy; pixi-effects check <page.html> reviews your work.',
|
|
186
|
+
].join('\n'));
|
|
187
|
+
}
|
|
188
|
+
const hits = searchDocs(root, word);
|
|
189
|
+
if (!hits.length) return fail(`nothing in the docs about "${word}": try another word (pixi-effects help lists some), or pixi-effects recipe`);
|
|
190
|
+
return ok(formatHelp(hits, word, root).join('\n'));
|
|
191
|
+
}
|
|
192
|
+
if (cmd === 'new') {
|
|
193
|
+
const force = args.includes('--force');
|
|
194
|
+
const name = args.filter(a => a !== '--force')[0];
|
|
195
|
+
if (!name) return fail('give the page a name: pixi-effects new my-video [--force]', 2);
|
|
196
|
+
try {
|
|
197
|
+
const { file } = newPage(name, { cwd, root, force });
|
|
198
|
+
const f = shownPath(file, cwd);
|
|
199
|
+
return ok([
|
|
200
|
+
`created ${f} (the template: a 1280x720 page that collects every warning in window.__logs)`,
|
|
201
|
+
`next: npx pixi-effects view ${f} look at it as you edit (it says when the file changed)`,
|
|
202
|
+
` npx pixi-effects check ${f} review it: warnings, layout, sound, a contact sheet, a real export`,
|
|
203
|
+
' npx pixi-effects recipe tested pieces to copy · npx pixi-effects help <word>',
|
|
204
|
+
].join('\n'));
|
|
205
|
+
} catch (e) { return fail(`pixi-effects new: ${e.message}`); }
|
|
206
|
+
}
|
|
207
|
+
return fail(`unknown command ${cmd}`, 2);
|
|
208
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* pixi-effects — one command for the three tools, named like the package so `npx pixi-effects check my-video.html` works with nothing installed.
|
|
3
|
+
* pixi-effects — one command for the three tools (and three small lookups: new, recipe, help), named like the package so `npx pixi-effects check my-video.html` works with nothing installed.
|
|
4
4
|
*
|
|
5
5
|
* npx pixi-effects check my-video.html [--at 3,6] [--out dir] review it: warnings, layout, sound, a contact sheet, a real export
|
|
6
6
|
* npx pixi-effects render my-video.html -o my-video.mp4 export the file
|
|
@@ -13,28 +13,33 @@ import { spawn } from 'node:child_process';
|
|
|
13
13
|
import { realpathSync } from 'node:fs';
|
|
14
14
|
import { dirname, resolve } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { runLookup, LOOKUP_COMMANDS } from './lookup.mjs';
|
|
16
17
|
|
|
17
18
|
const TOOLS = { check: 'check.mjs', render: 'render.mjs', view: 'view.mjs' };
|
|
18
19
|
const USAGE = `usage:
|
|
19
20
|
pixi-effects check <page.html> [--at 3,6] [--out dir] review a video page: warnings, layout, sound, contact sheet, real export
|
|
20
21
|
pixi-effects render <page.html> -o <file.mp4> export the video
|
|
21
22
|
pixi-effects view <page.html> look at it in a browser, with a timeline
|
|
23
|
+
pixi-effects new <name> a page to start from (<name>.html)
|
|
24
|
+
pixi-effects recipe [id] the tested recipes; with an id, one of them with its code
|
|
25
|
+
pixi-effects help <word> the docs about a word (mask, threeD, keyframes, music …)
|
|
22
26
|
(every option of a command is listed in the header of its file: node_modules/pixi-effects/ai/tools/<command>.mjs)`;
|
|
23
27
|
|
|
24
28
|
function nearest(word) {
|
|
25
29
|
const d = (a, b) => { const m = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]); for (let j = 1; j <= b.length; j++) m[0][j] = j;
|
|
26
30
|
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) m[i][j] = Math.min(m[i - 1][j] + 1, m[i][j - 1] + 1, m[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)); return m[a.length][b.length]; };
|
|
27
|
-
const best = Object.keys(TOOLS).map(k => [k, d(word, k)]).sort((x, y) => x[1] - y[1])[0];
|
|
31
|
+
const best = [...Object.keys(TOOLS), ...LOOKUP_COMMANDS].map(k => [k, d(word, k)]).sort((x, y) => x[1] - y[1])[0];
|
|
28
32
|
return best[1] <= 2 ? best[0] : null;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/** Which tool a command line asks for: `{ tool, args }`, or `{ usage }` (no command / --help), or `{ error }`. */
|
|
32
36
|
export function route(argv) {
|
|
33
37
|
const [cmd, ...args] = argv;
|
|
34
|
-
if (!cmd || cmd === '--help' || cmd === '-h'
|
|
38
|
+
if (!cmd || cmd === '--help' || cmd === '-h') return { usage: USAGE };
|
|
35
39
|
if (TOOLS[cmd]) return { tool: TOOLS[cmd], args };
|
|
40
|
+
if (LOOKUP_COMMANDS.includes(cmd)) return { inline: cmd, args };
|
|
36
41
|
const guess = /\.html?$/i.test(cmd) ? 'check' : nearest(cmd);
|
|
37
|
-
return { error: `pixi-effects: unknown command "${cmd}"${guess ? `; did you mean "${guess}"?` : ''} The commands are: ${Object.keys(TOOLS).join(', ')}.\n${USAGE}` };
|
|
42
|
+
return { error: `pixi-effects: unknown command "${cmd}"${guess ? `; did you mean "${guess}"?` : ''} The commands are: ${[...Object.keys(TOOLS), ...LOOKUP_COMMANDS].join(', ')}.\n${USAGE}` };
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
const self = fileURLToPath(import.meta.url);
|
|
@@ -42,6 +47,12 @@ if (process.argv[1] && realpathSync(resolve(process.argv[1])) === realpathSync(s
|
|
|
42
47
|
const r = route(process.argv.slice(2));
|
|
43
48
|
if (r.usage) { console.log(r.usage); process.exit(process.argv.length > 2 ? 0 : 2); }
|
|
44
49
|
if (r.error) { console.error(r.error); process.exit(2); }
|
|
50
|
+
if (r.inline) {
|
|
51
|
+
const out = runLookup(r.inline, r.args);
|
|
52
|
+
if (out.stdout) process.stdout.write(out.stdout);
|
|
53
|
+
if (out.stderr) process.stderr.write(out.stderr);
|
|
54
|
+
process.exit(out.code);
|
|
55
|
+
}
|
|
45
56
|
const child = spawn(process.execPath, [resolve(dirname(self), r.tool), ...r.args], { stdio: 'inherit' });
|
|
46
57
|
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => child.kill(sig)); // a harness that stops this process stops the tool (and its Chrome) too
|
|
47
58
|
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 128 : 1)));
|
package/dist/loader.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* pixi-effects loader: one line of HTML on the page, a few lines of CSS, no JavaScript of your own.
|
|
3
3
|
*
|
|
4
|
-
* <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
4
|
+
* <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.30.0/dist/loader.css">
|
|
5
5
|
* <div class="stage" style="position: relative"> <!-- any box the canvas sits in, with position: relative -->
|
|
6
6
|
* <canvas id="stage" width="1280" height="720"></canvas>
|
|
7
7
|
* <div class="pe-loader" data-label="LOADING"></div> <!-- or class="pe-loader pe-loader--bar" -->
|
package/llms-full.txt
CHANGED
|
@@ -6,15 +6,15 @@ 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.30.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
|
|
|
13
|
-
You describe a video as a tree of plain objects; the library plays it in a canvas and exports it (MP4/WebM/MOV). You never write per-frame code. **Read `reference/
|
|
13
|
+
You describe a video as a tree of plain objects; the library plays it in a canvas and exports it (MP4/WebM/MOV). You never write per-frame code. **Read `reference/quickstart.md` first (2 KB): the commands, a working title card, and what goes wrong most.** For anything it does not answer, ask the docs: `npx pixi-effects help <word>` prints the sections about a word (mask, threeD, keyframes, music …); `npx pixi-effects recipe` lists the tested recipes (a title, a lower third, a chart, kinetic type, a 2.5D scene …) and `npx pixi-effects recipe <id>` prints one with its code. `reference/cheatsheet.md` (35 KB) and `reference/pitfalls.md` (41 KB, numbered) are for looking things up when a warning or the picture surprises you: search them for the word, do not read them end to end. A simple title needs the quick start and one recipe, nothing more.
|
|
14
14
|
|
|
15
15
|
## Workflow
|
|
16
16
|
|
|
17
|
-
1. **Start from `
|
|
17
|
+
1. **Start from the template:** `npx pixi-effects new my-video` writes `my-video.html` (or copy `template.html` by hand); it also has the shared loader (`<div class="pe-loader" data-label="LOADING">` + `pixi-effects/loader.css`: HTML and CSS only, animating just `transform` / `opacity`, so it keeps moving while the page builds the video; `movie.init()` fades it out). Keep it; a loader of your own is fine (`pe-loader--custom`) as long as it only animates `transform` / `opacity`, never with JavaScript or `requestAnimationFrame`; it loads the library from a CDN, collects every console warning into `window.__logs`, and exposes `window.movie`). One HTML file, plain JS. Default canvas 1280×720 @ 30 fps.
|
|
18
18
|
2. **Give the movie a poster time.** In `movie.init` add `poster: <seconds>`: the most striking moment (a title fully on screen, not the first frame, which is often black). The canvas shows that picture before play (the playhead stays at 0 and play starts from 0), and the check writes it as `poster.jpg`.
|
|
19
19
|
3. **Write the composition as data.** Anything repeated (letters, bars, particles, scenes) is a JS function or loop that returns specs — never copy-paste objects. Derive coordinates and times from constants/data, not magic numbers.
|
|
20
20
|
4. **Show the human, EARLY: the first address goes out within two minutes.** Do it right after step 1, BEFORE you write the brief or collect assets: the copied template is already a page. Run `npx pixi-effects view my-video.html` (`--no-open` only prints the address; in the background, it keeps running), tell the human the address in your first message, and keep editing the same file: it opens their browser on the page with a scrubbable, zoomable timeline under it (click / drag to seek, + / − or Ctrl/⌘ + wheel to zoom) and runs until Ctrl-C. When the folder changes the viewer shows "Updated hh:mm:ss — reload (R)" and nothing else: the picture they are looking at never switches by itself, a reload returns to the frame they were on, and the switch "auto" (off by default) reloads only while the movie is paused. Do not restart it after every edit; they see the work grow instead of waiting for the end. **Deliver the file:** `npx pixi-effects render my-video.html -o my-video.mp4` renders the page headless to mp4 / webm / mov / mkv and exits 0 only when the file is written; run it AFTER the check below passes. **Run the check — one command does steps 3, 4 and the export:** `npx pixi-effects check my-video.html` (Node ≥ 22 and Chrome installed). **If `npx` says command not found, or prints nothing** (your npm may hold back a release that is a few days old, or an old release is cached): `npm i pixi-effects`, then `node node_modules/pixi-effects/ai/tools/check.mjs my-video.html` (the same for `render.mjs` and `view.mjs`). It starts its own private Chrome (nothing is shared with other sessions), waits for `window.__ready`, and writes `timeline.html` (every layer as a bar on a time axis: ◆ keyframes, shaded transitions; hand it to the human to open) and prints: console warnings, `movie.inspect` over the WHOLE timeline (grouped), `movie.inspectAudio()`, then exports an mp4 and decodes it again (video size and length, audio length, loudness per second, silence). It writes `check-out/<name>/sheet.png` — **open that picture and look at it** — plus the mp4 and `report.json`. Exit 0 = nothing to fix; exit 1 = a list of problems; fix them and run it again. A key a layer does not have (a typo in a layer key, a keyframe property or a text style key; `anchorX` outside `initial`; `pitch` outside `sfx`) is a warning that names the key and what you probably meant: treat it like any other warning. Text that overlaps text is listed under `review` (not a failure: ghost layers, glow copies and per-letter boxes are often intentional; look at the sheet; `--strict` makes them fail). The manual steps below are for looking at one frame closely. The same judgement is in the page: `await movie.review({ at: 'title@end' })` returns `{ frames, problems, review, fonts, audio, at }` (what `check` prints, as data) and `movie.resolveAt('3.5, title@end')` turns moments into frames.
|
|
@@ -72,6 +72,39 @@ Centred text must be positioned by `anchorX/anchorY: 0.5`; estimate text width a
|
|
|
72
72
|
- Full reference: https://yjmtmtk.github.io/pixi-effects/docs/dsl.md and https://yjmtmtk.github.io/pixi-effects/docs/api.md
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
<!-- ===== skills/pixi-effects/reference/quickstart.md ===== -->
|
|
76
|
+
|
|
77
|
+
# pixi-effects quick start
|
|
78
|
+
|
|
79
|
+
npx pixi-effects new my-video # a page to edit
|
|
80
|
+
npx pixi-effects view my-video.html # look at it while you edit
|
|
81
|
+
npx pixi-effects check my-video.html # warnings, layout, sound, a contact sheet: LOOK at the PNG
|
|
82
|
+
npx pixi-effects help <word> # the docs about a word: mask, threeD, keyframes, music
|
|
83
|
+
npx pixi-effects recipe [id] # tested pieces to copy
|
|
84
|
+
|
|
85
|
+
A video is data: `sequences` (text, shape, image, video, audio, composition, camera), each with `at`, `duration`, `initial`, `keyframes`.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
// @recipe quickstart
|
|
89
|
+
return [
|
|
90
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#10162b' } },
|
|
91
|
+
{ type: 'text', text: 'Hello', at: 0.3, duration: 4, style: { fontSize: 140, fill: '#ffffff', fontWeight: '800' },
|
|
92
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
93
|
+
keyframes: [{ at: 0, from: { y: 'GH/2 + 40', alpha: 0 }, to: { y: 'GH/2', alpha: 1 }, duration: 0.6, ease: 'power3.out' }] },
|
|
94
|
+
];
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
What goes wrong most:
|
|
98
|
+
- A keyframe `at` is seconds from the layer's own start; a layer's `at` and a transition's `at` are movie seconds.
|
|
99
|
+
- A layer stays hidden after its time; z-order is array order; a scene is layers with `at`/`duration` over an opaque rect.
|
|
100
|
+
- Numbers may be expressions (`'GW/2 - w/2'`); angles are degrees.
|
|
101
|
+
- Anchors: rect and circle are centred on x,y; text and image are top-left (use `anchorX: 0.5`).
|
|
102
|
+
- A warning is an instruction: `window.__logs` must be `[]`. Fix what it names.
|
|
103
|
+
- Sound needs no files: `{ type: 'audio', sfx: 'swoosh', at: 2 }`, or `music: { bpm, tracks }`.
|
|
104
|
+
- Never say done before you have looked at the check's PNG.
|
|
105
|
+
- Anything else: `help <word>` first; the cheatsheet is for grep, not for reading.
|
|
106
|
+
|
|
107
|
+
|
|
75
108
|
<!-- ===== skills/pixi-effects/reference/cheatsheet.md ===== -->
|
|
76
109
|
|
|
77
110
|
# pixi-effects cheatsheet
|
package/llms.txt
CHANGED
|
@@ -15,7 +15,8 @@ Rules that cause most failures (details in the skill and pitfalls):
|
|
|
15
15
|
- **If you are an AI in a chat with no shell and no files** (a browser chat, a preview window): read only [Chat guide](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/CHAT.md) and copy [chat-template.html](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/chat-template.html); everything below is for an AI that can run commands.
|
|
16
16
|
- **If your agent supports skills:** `npx skills add yjmtmtk/pixi-effects` registers the workflow below as a skill (Claude Code also: `/plugin install pixi-effects --marketplace yjmtmtk/pixi-effects`)
|
|
17
17
|
- [Skill: workflow and rules](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/SKILL.md): how to build, check and export a video; read first
|
|
18
|
-
- [
|
|
18
|
+
- [Quick start](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/quickstart.md): 2 KB: the commands (`npx pixi-effects new | view | check | help <word> | recipe [id]`), a working title card, and what goes wrong most; read this before the cheatsheet
|
|
19
|
+
- [Cheatsheet](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/cheatsheet.md): every layer type, prop, default and convention on one page (look things up here, or `npx pixi-effects help <word>`)
|
|
19
20
|
- [Recipes](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/recipes.md): tested building blocks (kinetic type, lower-third, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions, sound effects without files, music written as text, three.js, a presentation deck, visuals that follow music)
|
|
20
21
|
- [Pitfalls](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/pitfalls.md): real mistakes made by AI authors, with fixes
|
|
21
22
|
- [Directing a film](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/direction.md): for an AI with a shell and files: a product film or company intro, before building: brief, style guide, shot list, assets rule, review protocol (any renderer; a browser chat skips it)
|
package/package.json
CHANGED
|
@@ -3,15 +3,15 @@ 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.30.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
|
|
|
10
|
-
You describe a video as a tree of plain objects; the library plays it in a canvas and exports it (MP4/WebM/MOV). You never write per-frame code. **Read `reference/
|
|
10
|
+
You describe a video as a tree of plain objects; the library plays it in a canvas and exports it (MP4/WebM/MOV). You never write per-frame code. **Read `reference/quickstart.md` first (2 KB): the commands, a working title card, and what goes wrong most.** For anything it does not answer, ask the docs: `npx pixi-effects help <word>` prints the sections about a word (mask, threeD, keyframes, music …); `npx pixi-effects recipe` lists the tested recipes (a title, a lower third, a chart, kinetic type, a 2.5D scene …) and `npx pixi-effects recipe <id>` prints one with its code. `reference/cheatsheet.md` (35 KB) and `reference/pitfalls.md` (41 KB, numbered) are for looking things up when a warning or the picture surprises you: search them for the word, do not read them end to end. A simple title needs the quick start and one recipe, nothing more.
|
|
11
11
|
|
|
12
12
|
## Workflow
|
|
13
13
|
|
|
14
|
-
1. **Start from `
|
|
14
|
+
1. **Start from the template:** `npx pixi-effects new my-video` writes `my-video.html` (or copy `template.html` by hand); it also has the shared loader (`<div class="pe-loader" data-label="LOADING">` + `pixi-effects/loader.css`: HTML and CSS only, animating just `transform` / `opacity`, so it keeps moving while the page builds the video; `movie.init()` fades it out). Keep it; a loader of your own is fine (`pe-loader--custom`) as long as it only animates `transform` / `opacity`, never with JavaScript or `requestAnimationFrame`; it loads the library from a CDN, collects every console warning into `window.__logs`, and exposes `window.movie`). One HTML file, plain JS. Default canvas 1280×720 @ 30 fps.
|
|
15
15
|
2. **Give the movie a poster time.** In `movie.init` add `poster: <seconds>`: the most striking moment (a title fully on screen, not the first frame, which is often black). The canvas shows that picture before play (the playhead stays at 0 and play starts from 0), and the check writes it as `poster.jpg`.
|
|
16
16
|
3. **Write the composition as data.** Anything repeated (letters, bars, particles, scenes) is a JS function or loop that returns specs — never copy-paste objects. Derive coordinates and times from constants/data, not magic numbers.
|
|
17
17
|
4. **Show the human, EARLY: the first address goes out within two minutes.** Do it right after step 1, BEFORE you write the brief or collect assets: the copied template is already a page. Run `npx pixi-effects view my-video.html` (`--no-open` only prints the address; in the background, it keeps running), tell the human the address in your first message, and keep editing the same file: it opens their browser on the page with a scrubbable, zoomable timeline under it (click / drag to seek, + / − or Ctrl/⌘ + wheel to zoom) and runs until Ctrl-C. When the folder changes the viewer shows "Updated hh:mm:ss — reload (R)" and nothing else: the picture they are looking at never switches by itself, a reload returns to the frame they were on, and the switch "auto" (off by default) reloads only while the movie is paused. Do not restart it after every edit; they see the work grow instead of waiting for the end. **Deliver the file:** `npx pixi-effects render my-video.html -o my-video.mp4` renders the page headless to mp4 / webm / mov / mkv and exits 0 only when the file is written; run it AFTER the check below passes. **Run the check — one command does steps 3, 4 and the export:** `npx pixi-effects check my-video.html` (Node ≥ 22 and Chrome installed). **If `npx` says command not found, or prints nothing** (your npm may hold back a release that is a few days old, or an old release is cached): `npm i pixi-effects`, then `node node_modules/pixi-effects/ai/tools/check.mjs my-video.html` (the same for `render.mjs` and `view.mjs`). It starts its own private Chrome (nothing is shared with other sessions), waits for `window.__ready`, and writes `timeline.html` (every layer as a bar on a time axis: ◆ keyframes, shaded transitions; hand it to the human to open) and prints: console warnings, `movie.inspect` over the WHOLE timeline (grouped), `movie.inspectAudio()`, then exports an mp4 and decodes it again (video size and length, audio length, loudness per second, silence). It writes `check-out/<name>/sheet.png` — **open that picture and look at it** — plus the mp4 and `report.json`. Exit 0 = nothing to fix; exit 1 = a list of problems; fix them and run it again. A key a layer does not have (a typo in a layer key, a keyframe property or a text style key; `anchorX` outside `initial`; `pitch` outside `sfx`) is a warning that names the key and what you probably meant: treat it like any other warning. Text that overlaps text is listed under `review` (not a failure: ghost layers, glow copies and per-letter boxes are often intentional; look at the sheet; `--strict` makes them fail). The manual steps below are for looking at one frame closely. The same judgement is in the page: `await movie.review({ at: 'title@end' })` returns `{ frames, problems, review, fonts, audio, at }` (what `check` prints, as data) and `movie.resolveAt('3.5, title@end')` turns moments into frames.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# pixi-effects quick start
|
|
2
|
+
|
|
3
|
+
npx pixi-effects new my-video # a page to edit
|
|
4
|
+
npx pixi-effects view my-video.html # look at it while you edit
|
|
5
|
+
npx pixi-effects check my-video.html # warnings, layout, sound, a contact sheet: LOOK at the PNG
|
|
6
|
+
npx pixi-effects help <word> # the docs about a word: mask, threeD, keyframes, music
|
|
7
|
+
npx pixi-effects recipe [id] # tested pieces to copy
|
|
8
|
+
|
|
9
|
+
A video is data: `sequences` (text, shape, image, video, audio, composition, camera), each with `at`, `duration`, `initial`, `keyframes`.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// @recipe quickstart
|
|
13
|
+
return [
|
|
14
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#10162b' } },
|
|
15
|
+
{ type: 'text', text: 'Hello', at: 0.3, duration: 4, style: { fontSize: 140, fill: '#ffffff', fontWeight: '800' },
|
|
16
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
17
|
+
keyframes: [{ at: 0, from: { y: 'GH/2 + 40', alpha: 0 }, to: { y: 'GH/2', alpha: 1 }, duration: 0.6, ease: 'power3.out' }] },
|
|
18
|
+
];
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
What goes wrong most:
|
|
22
|
+
- A keyframe `at` is seconds from the layer's own start; a layer's `at` and a transition's `at` are movie seconds.
|
|
23
|
+
- A layer stays hidden after its time; z-order is array order; a scene is layers with `at`/`duration` over an opaque rect.
|
|
24
|
+
- Numbers may be expressions (`'GW/2 - w/2'`); angles are degrees.
|
|
25
|
+
- Anchors: rect and circle are centred on x,y; text and image are top-left (use `anchorX: 0.5`).
|
|
26
|
+
- A warning is an instruction: `window.__logs` must be `[]`. Fix what it names.
|
|
27
|
+
- Sound needs no files: `{ type: 'audio', sfx: 'swoosh', at: 2 }`, or `music: { bpm, tracks }`.
|
|
28
|
+
- Never say done before you have looked at the check's PNG.
|
|
29
|
+
- Anything else: `help <word>` first; the cheatsheet is for grep, not for reading.
|
|
@@ -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.30.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.30.0/dist/index.js",
|
|
41
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.0/dist/Controller.js",
|
|
42
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.0/dist/present.js",
|
|
43
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.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.30.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';
|