pixi-effects 0.28.0 → 0.28.2
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 +22 -1
- package/README.md +6 -6
- package/ai/chat-template.html +3 -3
- package/ai/tools/view.mjs +102 -14
- package/dist/Composition-H7UNR5F5.js +7 -0
- package/dist/{Composition-HFEVIYHN.js.map → Composition-H7UNR5F5.js.map} +1 -1
- package/dist/Composition-K6B3APM5.cjs +16 -0
- package/dist/{Composition-NI74UJT3.cjs.map → Composition-K6B3APM5.cjs.map} +1 -1
- package/dist/{chunk-WEJAQAFX.js → chunk-5MZWXVTT.js} +3 -3
- package/dist/{chunk-WEJAQAFX.js.map → chunk-5MZWXVTT.js.map} +1 -1
- package/dist/{chunk-AK54FIWM.js → chunk-7VN2KI3U.js} +6 -3
- package/dist/chunk-7VN2KI3U.js.map +1 -0
- package/dist/{chunk-SBOVUZZY.cjs → chunk-KBH33IQX.cjs} +61 -61
- package/dist/{chunk-SBOVUZZY.cjs.map → chunk-KBH33IQX.cjs.map} +1 -1
- package/dist/{chunk-MZCDBRTY.cjs → chunk-MJW7KILM.cjs} +6 -3
- package/dist/chunk-MJW7KILM.cjs.map +1 -0
- package/dist/index.cjs +35 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/dist/three.cjs +5 -5
- package/dist/three.js +1 -1
- package/llms-full.txt +180 -2
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/skills/pixi-effects/SKILL.md +12 -2
- package/skills/pixi-effects/reference/direction.md +136 -0
- package/skills/pixi-effects/reference/pitfalls.md +3 -0
- package/skills/pixi-effects/reference/recipes.md +25 -0
- package/skills/pixi-effects/template.html +6 -6
- package/dist/Composition-HFEVIYHN.js +0 -7
- package/dist/Composition-NI74UJT3.cjs +0 -16
- package/dist/chunk-AK54FIWM.js.map +0 -1
- package/dist/chunk-MZCDBRTY.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.28.
|
|
3
|
+
## 0.28.2
|
|
4
|
+
|
|
5
|
+
**Added**
|
|
6
|
+
|
|
7
|
+
- **`pixi-effects view` tells you the work changed, and never switches by itself.** It watches the page's folder (not `node_modules`, `out`, `reviews`, `check-out`, hidden files); when something changes it shows "Updated hh:mm:ss — reload (R)" and nothing else. Reload (the button or R) loads the page again and returns to the frame you were on; the switch "auto" (off by default, remembered) reloads only while the movie is paused. The skill now says to start the viewer right after copying the template (before the brief is written) and to give the human the address in the first message, so the first address goes out within two minutes and they watch the film grow.
|
|
8
|
+
|
|
9
|
+
**Fixed**
|
|
10
|
+
|
|
11
|
+
- **`pixi-effects view` centres the page, and the timeline legend no longer gets cut off.** A page that keeps its picture at the left edge (the template does) sat against the left of a wide viewer; the iframe is now as wide as the page's own content and centred. The legend under the timeline (text, shape, image, …) is wrapped onto more lines when the chart is narrow instead of running past the right edge (`timelineSvg()` and the `timeline.html` that `check` writes).
|
|
12
|
+
- **A text layer whose `style` has an undefined value is no longer blanked.** `style: { fontSize: undefined }` (a variable that was never set) erased the default size and the text was not drawn; the check said only "has no size". The undefined keys are now skipped (Pixi keeps its default) and one warning names them: `style.fontSize is undefined … omit the key instead`.
|
|
13
|
+
|
|
14
|
+
**Docs**
|
|
15
|
+
|
|
16
|
+
- Pitfall 103: load a web font before `measureText` / `splitText` / `animateText` (measured: a Latin headline is 21 % wider in the fallback font). `direction.md` asks the reviewer to write the three defects into `reviews/notes.md`, to measure the smallest text, and to look at both ends of every cut. Both came from a fresh agent making a company film from the published skill alone.
|
|
17
|
+
|
|
18
|
+
## 0.28.1
|
|
19
|
+
|
|
20
|
+
**Changed**
|
|
21
|
+
|
|
22
|
+
- **A guide for directing a film, `reference/direction.md`** (renderer-neutral; linked from `SKILL.md` "For a film", pitfalls 101–102, recipe `two-formats`). Write `brief.md` (real assets with paths; if a needed asset is missing, stop and ask instead of drawing a fake), `style-guide.md` (taken from the real source, a motion rule per class of object) and `shotlist.md` (a row per beat with entry and exit state) before any composition; keep the files in a studio folder; put every cut on a bar line; review the rendered frames cold (the three largest defects, each with a timestamp, evidence and a local fix); make the formats the human asked for (one if they did not say; a vertical cut is its own composition, `?f=9x16`, `--query f=9x16`). Came from making two 32–45 s company films (16:9 and 9:16) from real websites' photos and copy, and from a published breakdown of building a code-rendered video studio (@0xwhrrari, rewritten, credited in the file).
|
|
23
|
+
|
|
24
|
+
## 0.28.1
|
|
4
25
|
|
|
5
26
|
**Added**
|
|
6
27
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.28.
|
|
3
|
+
> **Status**: experimental — current release `0.28.2`. 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.28.
|
|
53
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.
|
|
52
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/index.js",
|
|
53
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/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.28.
|
|
64
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.28.2/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.28.
|
|
80
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/three.js"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
</script>
|
|
@@ -174,7 +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
|
-
- **Look at a page with its timeline, and scrub one with the other:** `npx pixi-effects view my-video.html` (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.
|
|
177
|
+
- **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
178
|
- **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), `--fail-on-warn` (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
179
|
- **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.
|
|
180
180
|
- **One-command review for the AI that wrote the page:** `npx pixi-effects check my-video.html` (or `node ai/tools/check.mjs my-video.html`; Node ≥ 22 and Chrome installed, no dependencies). It opens the page in its own headless Chrome and reports warnings, layout (`movie.inspect` over the whole timeline), the soundtrack (`movie.inspectAudio`) and a real export decoded again, and writes a contact sheet PNG and a `timeline.html` (every layer as a bar on a time axis) to look at. Exit code 0 / 1.
|
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.28.
|
|
63
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.
|
|
64
|
-
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.
|
|
62
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/index.js",
|
|
63
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/Controller.js",
|
|
64
|
+
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/Presenter.js"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
</script>
|
package/ai/tools/view.mjs
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* Click or drag on the timeline to seek the movie; click a layer's name to jump to where it starts; Space plays / pauses,
|
|
10
10
|
* ← / → step a frame (Shift: a second), Home / End jump to the ends. Stop it with Ctrl-C.
|
|
11
11
|
*
|
|
12
|
+
* It watches the page's folder (not `node_modules`, `out`, `reviews`, `check-out`, hidden files). When the work changes it shows "Updated hh:mm:ss — reload (R)"
|
|
13
|
+
* and NOTHING else: the picture you are looking at never switches by itself. Reload (the button, or R) loads the page again and goes back to the frame you were on.
|
|
14
|
+
* The switch "auto" (off by default, remembered) reloads for you, but only while the movie is paused and you are not dragging the timeline.
|
|
15
|
+
*
|
|
12
16
|
* Needs: Node >= 18. A browser (the default one is opened; --no-open prints the address instead). The page must follow
|
|
13
17
|
* skills/pixi-effects/template.html (`window.movie`, `window.__ready = true`) and use a pixi-effects that has `movie.timelineSvg()` (0.7+).
|
|
14
18
|
* Options: --port N (default: a free one) · --no-open · --query "a=1" (added to the page URL) · --root DIR (default: the nearest folder above the page with dist/)
|
|
@@ -38,6 +42,26 @@ export function parseViewArgs(argv) {
|
|
|
38
42
|
return o;
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
const IGNORED = new Set(['node_modules', 'out', 'reviews', 'check-out', '.git', 'dist']);
|
|
46
|
+
|
|
47
|
+
/** A short fingerprint of the work in a folder (file count, newest edit, total size): it changes when something is edited or added. Outputs, installs and hidden files are left out. */
|
|
48
|
+
export function folderSignature(dir, { ignore = IGNORED, maxFiles = 4000, maxDepth = 5 } = {}) {
|
|
49
|
+
let count = 0, newest = 0, bytes = 0;
|
|
50
|
+
const walk = (d, depth) => {
|
|
51
|
+
let entries;
|
|
52
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
53
|
+
for (const e of entries) {
|
|
54
|
+
if (count >= maxFiles) return;
|
|
55
|
+
if (e.name.startsWith('.')) continue;
|
|
56
|
+
const full = path.join(d, e.name);
|
|
57
|
+
if (e.isDirectory()) { if (!ignore.has(e.name) && depth < maxDepth) walk(full, depth + 1); continue; }
|
|
58
|
+
try { const st = fs.statSync(full); count++; newest = Math.max(newest, st.mtimeMs); bytes += st.size; } catch { /* removed while we looked */ }
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
walk(dir, 0);
|
|
62
|
+
return count + ':' + Math.round(newest) + ':' + bytes;
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
const escHtml = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
42
66
|
|
|
43
67
|
/** The viewer page: the composition in an iframe, its timeline under it (zoomable), a playhead, click / drag / keys to seek. */
|
|
@@ -53,6 +77,8 @@ export function viewerHtml({ pageUrl, title }) {
|
|
|
53
77
|
header b { font-size: 14px; font-weight: 600; }
|
|
54
78
|
#time { font-family: ui-monospace, Menlo, monospace; color: var(--dim); }
|
|
55
79
|
#status { color: var(--dim); }
|
|
80
|
+
#update button { background: var(--head); color: #fff; border-color: var(--head); font-weight: 600; }
|
|
81
|
+
#autolabel { color: var(--dim); user-select: none; }
|
|
56
82
|
.zoom { display: inline-flex; align-items: center; gap: 4px; margin-left: auto; }
|
|
57
83
|
#zlabel { min-width: 44px; text-align: center; font-family: ui-monospace, Menlo, monospace; color: var(--dim); }
|
|
58
84
|
button { font: inherit; background: transparent; color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: 3px 12px; cursor: pointer; }
|
|
@@ -72,7 +98,7 @@ export function viewerHtml({ pageUrl, title }) {
|
|
|
72
98
|
#head line { stroke: var(--head); stroke-width: 2; } #head path { fill: var(--head); }
|
|
73
99
|
</style></head>
|
|
74
100
|
<body>
|
|
75
|
-
<header><b>${escHtml(title)}</b><button id="play" type="button" disabled>▶</button><span id="time">0.00 / 0.00 s · frame 0</span><span id="status">loading the page…</span>
|
|
101
|
+
<header><b>${escHtml(title)}</b><button id="play" type="button" disabled>▶</button><span id="time">0.00 / 0.00 s · frame 0</span><span id="status">loading the page…</span><span id="update" hidden><button id="reload" type="button" title="load the page again, on the frame you are on (R)">Updated — reload (R)</button></span><label id="autolabel" title="reload by itself, but only while the movie is paused"><input type="checkbox" id="auto"> auto</label>
|
|
76
102
|
<span class="zoom"><button id="zout" type="button" title="zoom out (-)" disabled>−</button><span id="zlabel">fit</span><button id="zin" type="button" title="zoom in (+, or Ctrl/⌘ + wheel)" disabled>+</button><button id="zfit" type="button" title="fit the whole movie (0)" disabled>fit</button></span></header>
|
|
77
103
|
<div id="stage"><iframe id="page" src="${escHtml(pageUrl)}" allow="autoplay; fullscreen"></iframe></div>
|
|
78
104
|
<div id="chart"><div id="names"></div><div id="plot"></div></div>
|
|
@@ -87,31 +113,75 @@ export function viewerHtml({ pageUrl, title }) {
|
|
|
87
113
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
88
114
|
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
89
115
|
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
90
|
-
let movie = null, svg = null, head = null, geom = null, zoom = 1, scrubbing = false;
|
|
116
|
+
let movie = null, svg = null, head = null, geom = null, zoom = 1, scrubbing = false, bound = false;
|
|
117
|
+
const $update = document.getElementById('update'), $reload = document.getElementById('reload'), $auto = document.getElementById('auto');
|
|
118
|
+
let seen = null, pending = false;
|
|
119
|
+
try { $auto.checked = localStorage.getItem('pe-view-auto') === '1'; } catch { /* storage may be blocked */ }
|
|
91
120
|
|
|
92
|
-
|
|
121
|
+
/** Wait for the page inside to be a ready movie, then (re)build the timeline. \`restore\` = { frame, playing }: go back where the viewer was. */
|
|
122
|
+
async function attach(restore) {
|
|
123
|
+
movie = null; svg = null; head = null; geom = null;
|
|
93
124
|
for (let i = 0; i < 600 && !movie; i++) {
|
|
94
125
|
try { const w = frame.contentWindow; if (w && w.__ready === true && w.movie) movie = w.movie; } catch { /* still loading */ }
|
|
95
126
|
if (!movie) await sleep(200);
|
|
96
127
|
}
|
|
97
|
-
if (!movie) {
|
|
128
|
+
if (!movie) {
|
|
129
|
+
let logs = '';
|
|
130
|
+
try { logs = (frame.contentWindow.__logs || []).slice(-3).join(' | '); } catch { /* unreadable */ }
|
|
131
|
+
$status.textContent = 'the page did not become ready (window.__ready === true and window.movie are needed)' + (logs ? ' — ' + logs : '');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
98
134
|
if (typeof movie.timelineSvg !== 'function') { $status.textContent = 'this page loads a pixi-effects without movie.timelineSvg() (0.7+ needed)'; return; }
|
|
99
135
|
layout();
|
|
100
136
|
mount();
|
|
101
|
-
|
|
137
|
+
if (restore) { await seekFrame(restore.frame); if (restore.playing) movie.play(); }
|
|
138
|
+
}
|
|
139
|
+
attach(null);
|
|
140
|
+
|
|
141
|
+
/** Load the page again and come back to the same frame (and playing, if it was). Only ever on a request. */
|
|
142
|
+
async function reload() {
|
|
143
|
+
const restore = movie ? { frame: movie.currentFrame, playing: movie.isPlaying } : null;
|
|
144
|
+
pending = false; $update.hidden = true;
|
|
145
|
+
movie = null; svg = null; head = null; geom = null; plot.innerHTML = ''; names.innerHTML = '';
|
|
146
|
+
const loaded = new Promise(r => frame.addEventListener('load', r, { once: true }));
|
|
147
|
+
try { frame.contentWindow.location.reload(); } catch { frame.src = frame.src; }
|
|
148
|
+
await loaded;
|
|
149
|
+
await attach(restore);
|
|
150
|
+
}
|
|
151
|
+
$reload.addEventListener('click', reload);
|
|
152
|
+
$auto.addEventListener('change', () => { try { localStorage.setItem('pe-view-auto', $auto.checked ? '1' : '0'); } catch { /* storage may be blocked */ } });
|
|
153
|
+
// auto only while the movie is paused and nobody is dragging: otherwise the notice waits
|
|
154
|
+
const maybeAuto = () => { if (pending && $auto.checked && movie && !movie.isPlaying && !scrubbing) reload(); };
|
|
102
155
|
|
|
103
|
-
//
|
|
156
|
+
// does the work still look the same? (the server's number goes up when a file in the folder changes)
|
|
157
|
+
async function poll() {
|
|
158
|
+
try {
|
|
159
|
+
const v = await (await fetch('/__viewer/version', { cache: 'no-store' })).json();
|
|
160
|
+
if (seen === null) seen = v.version;
|
|
161
|
+
else if (v.version !== seen) {
|
|
162
|
+
seen = v.version; pending = true;
|
|
163
|
+
const d = new Date(v.at), p2 = n => String(n).padStart(2, '0');
|
|
164
|
+
$reload.textContent = 'Updated ' + p2(d.getHours()) + ':' + p2(d.getMinutes()) + ':' + p2(d.getSeconds()) + ' — reload (R)';
|
|
165
|
+
$update.hidden = false;
|
|
166
|
+
}
|
|
167
|
+
} catch { /* the viewer is stopped */ }
|
|
168
|
+
}
|
|
169
|
+
poll(); setInterval(poll, 800);
|
|
170
|
+
|
|
171
|
+
// The page is shown whole and in the MIDDLE: as tall as its picture, scaled down (never up) so the timeline stays in view under it. A page that
|
|
172
|
+
// keeps its picture at the left edge (the template does) is still centred: the iframe is as wide as the page's own content, then centred.
|
|
104
173
|
function layout() {
|
|
105
174
|
const stageW = stage.clientWidth || innerWidth, headH = document.querySelector('header').offsetHeight;
|
|
106
|
-
let h = 560;
|
|
107
|
-
|
|
108
|
-
fit(1);
|
|
175
|
+
let h = 560, cw = stageW;
|
|
176
|
+
frame.style.width = stageW + 'px'; frame.style.transform = 'none'; frame.style.marginLeft = '0px';
|
|
109
177
|
try {
|
|
110
178
|
const c = frame.contentDocument.querySelector('canvas');
|
|
111
|
-
if (c) h = Math.round(
|
|
179
|
+
if (c) { const r = c.getBoundingClientRect(); h = Math.round(r.height + 40); cw = Math.min(stageW, Math.round(r.right + r.left)); }
|
|
112
180
|
} catch { /* a page we cannot read */ }
|
|
113
181
|
const k = Math.min(1, Math.max(200, innerHeight - headH - 240) / h);
|
|
114
|
-
|
|
182
|
+
frame.style.width = cw + 'px';
|
|
183
|
+
frame.style.transform = 'scale(' + k + ')';
|
|
184
|
+
frame.style.marginLeft = Math.max(0, Math.round((stageW - cw * k) / 2)) + 'px';
|
|
115
185
|
frame.style.height = h + 'px';
|
|
116
186
|
stage.style.height = Math.round(h * k) + 'px';
|
|
117
187
|
}
|
|
@@ -180,10 +250,12 @@ export function viewerHtml({ pageUrl, title }) {
|
|
|
180
250
|
const data = movie.timelineData();
|
|
181
251
|
names.innerHTML = '<div class="ruler" style="height:34px"></div>' + data.rows.map(r =>
|
|
182
252
|
'<div class="name" data-start="' + r.start + '" style="padding-left:' + (8 + r.depth * 14) + 'px" title="' + esc(r.path + ' · ' + r.type) + '">' + esc(r.name) + '</div>').join('');
|
|
183
|
-
names.addEventListener('click', e => { const n = e.target.closest('.name'); if (n) seekTime(parseFloat(n.dataset.start) || 0); });
|
|
184
253
|
render(null);
|
|
185
254
|
$status.textContent = 'click or drag the timeline · Space plays · ← → step a frame (Shift: a second) · + / − zoom (Ctrl/⌘ + wheel) · click a layer name to jump to its start';
|
|
186
255
|
$play.disabled = false; $zfit.disabled = true;
|
|
256
|
+
if (bound) return; // the controls below are bound once; a reload only rebuilds the timeline
|
|
257
|
+
bound = true;
|
|
258
|
+
names.addEventListener('click', e => { const n = e.target.closest('.name'); if (n && movie) seekTime(parseFloat(n.dataset.start) || 0); });
|
|
187
259
|
|
|
188
260
|
$play.addEventListener('click', toggle);
|
|
189
261
|
$zin.addEventListener('click', () => setZoom(zoom * 1.6, playheadAnchor()));
|
|
@@ -198,6 +270,8 @@ export function viewerHtml({ pageUrl, title }) {
|
|
|
198
270
|
}, { passive: false });
|
|
199
271
|
addEventListener('keydown', e => {
|
|
200
272
|
if (e.target && /input|textarea|select/i.test(e.target.tagName)) return;
|
|
273
|
+
if ((e.key === 'r' || e.key === 'R') && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); reload(); return; }
|
|
274
|
+
if (!movie) return;
|
|
201
275
|
const fps = movie.frameRate, f = movie.currentFrame;
|
|
202
276
|
if (e.key === ' ') { e.preventDefault(); toggle(); }
|
|
203
277
|
else if (e.key === 'ArrowLeft') { e.preventDefault(); seekFrame(f - (e.shiftKey ? fps : 1)); }
|
|
@@ -210,9 +284,11 @@ export function viewerHtml({ pageUrl, title }) {
|
|
|
210
284
|
});
|
|
211
285
|
requestAnimationFrame(tick);
|
|
212
286
|
}
|
|
213
|
-
const toggle = () => { if (movie.isPlaying) movie.pause(); else movie.play(); };
|
|
287
|
+
const toggle = () => { if (!movie) return; if (movie.isPlaying) movie.pause(); else movie.play(); };
|
|
214
288
|
|
|
215
289
|
function tick() {
|
|
290
|
+
if (!movie || !head || !geom) { maybeAuto(); requestAnimationFrame(tick); return; }
|
|
291
|
+
maybeAuto();
|
|
216
292
|
const t = nowT(), x = tToX(t);
|
|
217
293
|
head.setAttribute('transform', 'translate(' + x.toFixed(2) + ',0)');
|
|
218
294
|
$time.textContent = t.toFixed(2) + ' / ' + geom.dur.toFixed(2) + ' s · frame ' + movie.currentFrame;
|
|
@@ -244,11 +320,23 @@ export async function startViewer(opts) {
|
|
|
244
320
|
const pageUrl = '/' + rel + (opts.query ? '?' + opts.query : '');
|
|
245
321
|
const title = path.basename(pagePath).replace(/\.[^.]+$/, '');
|
|
246
322
|
const html = viewerHtml({ pageUrl, title });
|
|
323
|
+
// The work's version: a number that goes up when a file in the page's folder changes. The viewer only SHOWS it; it never reloads by itself.
|
|
324
|
+
const dir = path.dirname(pagePath);
|
|
325
|
+
let version = 1, changedAt = Date.now(), signature = folderSignature(dir);
|
|
326
|
+
const timer = setInterval(() => { const now = folderSignature(dir); if (now !== signature) { signature = now; version++; changedAt = Date.now(); } }, opts.watchEvery ?? 400);
|
|
327
|
+
timer.unref();
|
|
247
328
|
const { server, port } = await serve(root, (req, res) => {
|
|
248
|
-
if (req.method !== 'GET'
|
|
329
|
+
if (req.method !== 'GET') return false;
|
|
330
|
+
const pathname = req.url.split('?')[0];
|
|
331
|
+
if (pathname === VIEWER + '/version') {
|
|
332
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify({ version, at: changedAt }));
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (pathname !== VIEWER) return false;
|
|
249
336
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' }).end(html);
|
|
250
337
|
return true;
|
|
251
338
|
}, opts.port);
|
|
339
|
+
server.on('close', () => clearInterval(timer));
|
|
252
340
|
return { server, url: `http://127.0.0.1:${port}${VIEWER}` };
|
|
253
341
|
}
|
|
254
342
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { CompositionSequence } from './chunk-5MZWXVTT.js';
|
|
2
|
+
import './chunk-7VN2KI3U.js';
|
|
3
|
+
import './chunk-TYWFKCPY.js';
|
|
4
|
+
import './chunk-45QKLD6M.js';
|
|
5
|
+
import './chunk-BXOCEZ4U.js';
|
|
6
|
+
//# sourceMappingURL=Composition-H7UNR5F5.js.map
|
|
7
|
+
//# sourceMappingURL=Composition-H7UNR5F5.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-H7UNR5F5.js"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkKBH33IQX_cjs = require('./chunk-KBH33IQX.cjs');
|
|
4
|
+
require('./chunk-MJW7KILM.cjs');
|
|
5
|
+
require('./chunk-P4GY3Z4G.cjs');
|
|
6
|
+
require('./chunk-ZMAD3KJU.cjs');
|
|
7
|
+
require('./chunk-TWZNOUM7.cjs');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Object.defineProperty(exports, "CompositionSequence", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: function () { return chunkKBH33IQX_cjs.CompositionSequence; }
|
|
14
|
+
});
|
|
15
|
+
//# sourceMappingURL=Composition-K6B3APM5.cjs.map
|
|
16
|
+
//# sourceMappingURL=Composition-K6B3APM5.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-K6B3APM5.cjs"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_CASTERS, Sequence, timeRemapOf, contentLength, remapOf, buildSequenceTree, CameraSequence, LightSequence, layerInitialZ, createTimeline, bindClock, clockTable, NullSequence, lightLevel, homeCamera, cameraBasis, packLights, projectPoint, NEAR, blurRadius, MANY_BLURRED, focusProblems, MAX_LIGHTS, projectLayer, layerToWorld, FALLOFFS, LIGHT_PROPS, LIGHT_KINDS, MIN_BLUR, MAX_BLUR, DEG, SHADOW_TAPS } from './chunk-
|
|
1
|
+
import { MAX_CASTERS, Sequence, timeRemapOf, contentLength, remapOf, buildSequenceTree, CameraSequence, LightSequence, layerInitialZ, createTimeline, bindClock, clockTable, NullSequence, lightLevel, homeCamera, cameraBasis, packLights, projectPoint, NEAR, blurRadius, MANY_BLURRED, focusProblems, MAX_LIGHTS, projectLayer, layerToWorld, FALLOFFS, LIGHT_PROPS, LIGHT_KINDS, MIN_BLUR, MAX_BLUR, DEG, SHADOW_TAPS } from './chunk-7VN2KI3U.js';
|
|
2
2
|
import { maxConcurrentAdvanced, MANY_ADVANCED, applyBlendMode, isAdvancedBlend, blendFilterFor } from './chunk-TYWFKCPY.js';
|
|
3
3
|
import { describeLayer, lintTiming, lintText, lintKeys, summarizeWarnings, collectPropKeys } from './chunk-45QKLD6M.js';
|
|
4
4
|
import { suggestName } from './chunk-BXOCEZ4U.js';
|
|
@@ -1716,5 +1716,5 @@ function frameOf(child) {
|
|
|
1716
1716
|
}
|
|
1717
1717
|
|
|
1718
1718
|
export { CompositionSequence, maskSourceOf };
|
|
1719
|
-
//# sourceMappingURL=chunk-
|
|
1720
|
-
//# sourceMappingURL=chunk-
|
|
1719
|
+
//# sourceMappingURL=chunk-5MZWXVTT.js.map
|
|
1720
|
+
//# sourceMappingURL=chunk-5MZWXVTT.js.map
|