pixi-effects 0.5.0 → 0.7.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 +30 -0
- package/README.md +17 -10
- package/ai/SKILL.md +1 -1
- package/ai/reference/cheatsheet.md +8 -2
- package/ai/reference/pitfalls.md +1 -0
- package/ai/reference/recipes.md +36 -1
- package/ai/template.html +4 -3
- package/ai/tools/check.mjs +14 -5
- package/ai/tools/render.mjs +202 -0
- package/ai/tools/view.mjs +269 -0
- package/dist/{Base-DnRAxroQ.d.cts → Base-CSIulyIq.d.cts} +1 -1
- package/dist/{Base-CPSFBkPm.d.ts → Base-DVgwyIUj.d.ts} +1 -1
- package/dist/Composition-3KHQPRBA.cjs +14 -0
- package/dist/{Composition-63EXHNJJ.cjs.map → Composition-3KHQPRBA.cjs.map} +1 -1
- package/dist/Composition-DT657FO5.js +5 -0
- package/dist/{Composition-TUCRSMJU.js.map → Composition-DT657FO5.js.map} +1 -1
- package/dist/Controller.cjs +77 -2
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +16 -2
- package/dist/Controller.d.ts +16 -2
- package/dist/Controller.js +77 -2
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-CuU_aaCm.d.cts → Movie-B0OUKlZI.d.ts} +54 -2
- package/dist/{Movie-VyDiUYOj.d.ts → Movie-BK9daiTt.d.cts} +54 -2
- package/dist/{chunk-5Q5XZG7Z.js → chunk-36Z4RIRK.js} +3 -3
- package/dist/{chunk-5Q5XZG7Z.js.map → chunk-36Z4RIRK.js.map} +1 -1
- package/dist/{chunk-UBPZLGLU.js → chunk-4O2NYDJG.js} +365 -44
- package/dist/chunk-4O2NYDJG.js.map +1 -0
- package/dist/{chunk-23M5QTWW.cjs → chunk-4VSIJLU3.cjs} +21 -21
- package/dist/{chunk-23M5QTWW.cjs.map → chunk-4VSIJLU3.cjs.map} +1 -1
- package/dist/{chunk-2RR43OIN.cjs → chunk-6I55T4OH.cjs} +403 -62
- package/dist/chunk-6I55T4OH.cjs.map +1 -0
- package/dist/index.cjs +234 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +226 -16
- package/dist/index.js.map +1 -1
- package/dist/three.cjs +6 -5
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +2 -1
- package/dist/three.js.map +1 -1
- package/dist/{types-7VtTEKUW.d.ts → types-oJijbn-K.d.cts} +36 -7
- package/dist/{types-7VtTEKUW.d.cts → types-oJijbn-K.d.ts} +36 -7
- package/docs/api.md +7 -0
- package/docs/dsl.md +74 -8
- package/llms-full.txt +127 -12
- package/package.json +7 -4
- package/dist/Composition-63EXHNJJ.cjs +0 -13
- package/dist/Composition-TUCRSMJU.js +0 -4
- package/dist/chunk-2RR43OIN.cjs.map +0 -1
- package/dist/chunk-UBPZLGLU.js.map +0 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pixi-effects view — look at a composition page WITH its timeline, and scrub the one with the other.
|
|
4
|
+
*
|
|
5
|
+
* node ai/tools/view.mjs my-video.html (or: npx pixi-effects-view my-video.html)
|
|
6
|
+
*
|
|
7
|
+
* It serves the page's folder on a private port and opens your browser on a viewer: the page itself on top (its own player
|
|
8
|
+
* works as usual) and, under it, the timeline of every layer (`movie.timelineSvg()`) with a playhead that follows the movie.
|
|
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
|
+
* ← / → step a frame (Shift: a second), Home / End jump to the ends. Stop it with Ctrl-C.
|
|
11
|
+
*
|
|
12
|
+
* Needs: Node >= 18. A browser (the default one is opened; --no-open prints the address instead). The page must follow
|
|
13
|
+
* ai/template.html (`window.movie`, `window.__ready = true`) and use a pixi-effects that has `movie.timelineSvg()` (0.7+).
|
|
14
|
+
* 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/)
|
|
15
|
+
*/
|
|
16
|
+
import http from 'node:http';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import { pathToFileURL } from 'node:url';
|
|
21
|
+
import { findRoot, serve } from './check.mjs';
|
|
22
|
+
|
|
23
|
+
export const VIEWER = '/__viewer';
|
|
24
|
+
|
|
25
|
+
export function parseViewArgs(argv) {
|
|
26
|
+
const o = { page: null, port: 0, open: true, query: null, root: null };
|
|
27
|
+
const need = (i, name) => { if (i + 1 >= argv.length) throw new Error(`${name} needs a value`); return argv[i + 1]; };
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i];
|
|
30
|
+
if (a === '--port') o.port = Math.max(0, Math.round(Number(need(i++, a))) || 0);
|
|
31
|
+
else if (a === '--no-open') o.open = false;
|
|
32
|
+
else if (a === '--query') o.query = need(i++, a).replace(/^\?/, '');
|
|
33
|
+
else if (a === '--root') o.root = need(i++, a);
|
|
34
|
+
else if (a.startsWith('-')) throw new Error(`unknown option ${a} (see the header of view.mjs)`);
|
|
35
|
+
else if (!o.page) o.page = a;
|
|
36
|
+
else throw new Error(`unexpected argument ${a}`);
|
|
37
|
+
}
|
|
38
|
+
return o;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const escHtml = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
42
|
+
|
|
43
|
+
/** The viewer page: the composition in an iframe, its timeline under it (zoomable), a playhead, click / drag / keys to seek. */
|
|
44
|
+
export function viewerHtml({ pageUrl, title }) {
|
|
45
|
+
return `<!doctype html>
|
|
46
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escHtml(title)} — timeline view</title>
|
|
47
|
+
<style>
|
|
48
|
+
:root { --bg: #f7f5f1; --ink: #1d2433; --dim: #6b7587; --line: #d9d5cc; --head: #e0245e; }
|
|
49
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #0e1320; --ink: #e8eefc; --dim: #8fa0bf; --line: #26304a; --head: #ff5c8a; } }
|
|
50
|
+
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink); font: 13px system-ui, sans-serif; }
|
|
51
|
+
body { display: flex; flex-direction: column; }
|
|
52
|
+
header { display: flex; align-items: center; gap: 12px; padding: 8px 14px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
53
|
+
header b { font-size: 14px; font-weight: 600; }
|
|
54
|
+
#time { font-family: ui-monospace, Menlo, monospace; color: var(--dim); }
|
|
55
|
+
#status { color: var(--dim); }
|
|
56
|
+
.zoom { display: inline-flex; align-items: center; gap: 4px; margin-left: auto; }
|
|
57
|
+
#zlabel { min-width: 44px; text-align: center; font-family: ui-monospace, Menlo, monospace; color: var(--dim); }
|
|
58
|
+
button { font: inherit; background: transparent; color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: 3px 12px; cursor: pointer; }
|
|
59
|
+
button:disabled { opacity: .4; cursor: default; }
|
|
60
|
+
#stage { flex: none; overflow: hidden; background: #000; }
|
|
61
|
+
iframe { border: 0; display: block; transform-origin: 0 0; }
|
|
62
|
+
#chart { flex: 1 1 0; min-height: 0; overflow: auto; border-top: 1px solid var(--line); display: flex; align-items: flex-start; }
|
|
63
|
+
#names { position: sticky; left: 0; z-index: 2; flex: none; width: 250px; background: var(--bg); border-right: 1px solid var(--line); }
|
|
64
|
+
#names .name { height: 22px; line-height: 22px; box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 6px; font: 12px/22px ui-monospace, Menlo, monospace; cursor: pointer; }
|
|
65
|
+
#names .name:nth-of-type(odd) { background: color-mix(in srgb, var(--ink) 5%, transparent); }
|
|
66
|
+
#names .name:hover { background: color-mix(in srgb, var(--ink) 12%, transparent); }
|
|
67
|
+
#plot { flex: none; }
|
|
68
|
+
#plot svg { display: block; touch-action: none; user-select: none; cursor: col-resize; }
|
|
69
|
+
#plot svg .grid { stroke: var(--line); } #plot svg .tick { fill: var(--dim); font-size: 11px; }
|
|
70
|
+
#plot svg .band { fill: var(--ink); } #plot svg .key { fill: var(--ink); opacity: .85; } #plot svg .transition { fill: #4cc9f0; opacity: .16; } #plot svg .bar { opacity: .88; }
|
|
71
|
+
#plot svg .row:hover .band { fill-opacity: .1; }
|
|
72
|
+
#head line { stroke: var(--head); stroke-width: 2; } #head path { fill: var(--head); }
|
|
73
|
+
</style></head>
|
|
74
|
+
<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>
|
|
76
|
+
<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
|
+
<div id="stage"><iframe id="page" src="${escHtml(pageUrl)}" allow="autoplay; fullscreen"></iframe></div>
|
|
78
|
+
<div id="chart"><div id="names"></div><div id="plot"></div></div>
|
|
79
|
+
<script>
|
|
80
|
+
(() => {
|
|
81
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
82
|
+
const frame = document.getElementById('page'), chart = document.getElementById('chart'), stage = document.getElementById('stage');
|
|
83
|
+
const names = document.getElementById('names'), plot = document.getElementById('plot');
|
|
84
|
+
const $time = document.getElementById('time'), $status = document.getElementById('status'), $play = document.getElementById('play');
|
|
85
|
+
const $zin = document.getElementById('zin'), $zout = document.getElementById('zout'), $zfit = document.getElementById('zfit'), $zlabel = document.getElementById('zlabel');
|
|
86
|
+
const NAMES_W = 250, MAX_PX = 60000;
|
|
87
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
88
|
+
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
89
|
+
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;
|
|
91
|
+
|
|
92
|
+
(async () => {
|
|
93
|
+
for (let i = 0; i < 600 && !movie; i++) {
|
|
94
|
+
try { const w = frame.contentWindow; if (w && w.__ready === true && w.movie) movie = w.movie; } catch { /* still loading */ }
|
|
95
|
+
if (!movie) await sleep(200);
|
|
96
|
+
}
|
|
97
|
+
if (!movie) { $status.textContent = 'the page did not become ready (window.__ready === true and window.movie are needed)'; return; }
|
|
98
|
+
if (typeof movie.timelineSvg !== 'function') { $status.textContent = 'this page loads a pixi-effects without movie.timelineSvg() (0.7+ needed)'; return; }
|
|
99
|
+
layout();
|
|
100
|
+
mount();
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
// The page is shown whole: as tall as its picture, scaled down (never up) so the timeline stays in view under it.
|
|
104
|
+
function layout() {
|
|
105
|
+
const stageW = stage.clientWidth || innerWidth, headH = document.querySelector('header').offsetHeight;
|
|
106
|
+
let h = 560;
|
|
107
|
+
const fit = k => { frame.style.width = (stageW / k) + 'px'; frame.style.transform = 'scale(' + k + ')'; };
|
|
108
|
+
fit(1);
|
|
109
|
+
try {
|
|
110
|
+
const c = frame.contentDocument.querySelector('canvas');
|
|
111
|
+
if (c) h = Math.round(c.getBoundingClientRect().height + 40);
|
|
112
|
+
} catch { /* a page we cannot read */ }
|
|
113
|
+
const k = Math.min(1, Math.max(200, innerHeight - headH - 240) / h);
|
|
114
|
+
fit(k);
|
|
115
|
+
frame.style.height = h + 'px';
|
|
116
|
+
stage.style.height = Math.round(h * k) + 'px';
|
|
117
|
+
}
|
|
118
|
+
frame.addEventListener('load', layout);
|
|
119
|
+
addEventListener('resize', () => { layout(); if (svg) render(null); });
|
|
120
|
+
|
|
121
|
+
const fitWidth = () => Math.max(300, chart.clientWidth - NAMES_W - 32);
|
|
122
|
+
const maxZoom = () => Math.max(1, Math.min(MAX_PX, movie.totalFrames * 12) / fitWidth()); // at most ~12 px per frame
|
|
123
|
+
const tToX = t => geom.x0 + (t / geom.dur) * (geom.x1 - geom.x0);
|
|
124
|
+
const nowT = () => movie.currentFrame / movie.frameRate;
|
|
125
|
+
function clientToTime(clientX) {
|
|
126
|
+
const r = svg.getBoundingClientRect();
|
|
127
|
+
const ux = ((clientX - r.left) / r.width) * geom.vbw;
|
|
128
|
+
return clamp((ux - geom.x0) / (geom.x1 - geom.x0), 0, 1) * geom.dur;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// seek: only the latest wanted frame is rendered, so dragging never queues up
|
|
132
|
+
let wanted = null, busy = false;
|
|
133
|
+
async function seekFrame(f) {
|
|
134
|
+
wanted = clamp(Math.round(f), 0, movie.totalFrames);
|
|
135
|
+
if (busy) return;
|
|
136
|
+
busy = true;
|
|
137
|
+
try { while (wanted !== null) { const n = wanted; wanted = null; await movie.gotoFrame(n, true); } } finally { busy = false; }
|
|
138
|
+
}
|
|
139
|
+
const seekTime = t => seekFrame(t * movie.frameRate);
|
|
140
|
+
|
|
141
|
+
/** Draw the chart at the current zoom. \`anchor\` = { t, x }: the time that must stay at x pixels from the left of the panel. */
|
|
142
|
+
function render(anchor) {
|
|
143
|
+
const keep = anchor ?? (geom ? { t: nowT(), x: tToX(nowT()) + NAMES_W - chart.scrollLeft } : null);
|
|
144
|
+
plot.innerHTML = movie.timelineSvg({ chartWidth: Math.min(MAX_PX, Math.round(fitWidth() * zoom)), labels: false });
|
|
145
|
+
svg = plot.querySelector('svg');
|
|
146
|
+
const d = svg.dataset;
|
|
147
|
+
geom = { dur: +d.duration, x0: +d.x0, x1: +d.x1, top: +d.top, bottom: +d.bottom, vbw: svg.viewBox.baseVal.width };
|
|
148
|
+
head = document.createElementNS(NS, 'g'); head.id = 'head';
|
|
149
|
+
head.innerHTML = '<line x1="0" x2="0" y1="' + (geom.top - 2) + '" y2="' + geom.bottom + '"/><path d="M -6 ' + (geom.top - 8) + ' l 12 0 l -6 9 z"/>';
|
|
150
|
+
svg.appendChild(head);
|
|
151
|
+
svg.addEventListener('pointerdown', onDown); svg.addEventListener('pointermove', onMove);
|
|
152
|
+
svg.addEventListener('pointerup', onEnd); svg.addEventListener('pointercancel', onEnd);
|
|
153
|
+
if (zoom <= 1) chart.scrollLeft = 0;
|
|
154
|
+
else if (keep) chart.scrollLeft = Math.max(0, NAMES_W + tToX(keep.t) - keep.x);
|
|
155
|
+
$zlabel.textContent = zoom <= 1.001 ? 'fit' : zoom.toFixed(zoom < 10 ? 1 : 0) + '×';
|
|
156
|
+
$zout.disabled = zoom <= 1.001; $zfit.disabled = zoom <= 1.001; $zin.disabled = zoom >= maxZoom() - 0.001;
|
|
157
|
+
}
|
|
158
|
+
function setZoom(z, anchor) {
|
|
159
|
+
const next = clamp(z, 1, maxZoom());
|
|
160
|
+
if (Math.abs(next - zoom) < 1e-6) return;
|
|
161
|
+
zoom = next;
|
|
162
|
+
render(anchor);
|
|
163
|
+
}
|
|
164
|
+
const playheadAnchor = () => {
|
|
165
|
+
const x = tToX(nowT()) + NAMES_W - chart.scrollLeft;
|
|
166
|
+
return { t: nowT(), x: clamp(x, NAMES_W + 20, chart.clientWidth - 20) };
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
function onDown(e) {
|
|
170
|
+
const r = svg.getBoundingClientRect();
|
|
171
|
+
scrubbing = true; wasPlaying = movie.isPlaying; if (wasPlaying) movie.pause();
|
|
172
|
+
try { svg.setPointerCapture(e.pointerId); } catch { /* a synthetic event has no pointer */ }
|
|
173
|
+
seekTime(clientToTime(e.clientX));
|
|
174
|
+
}
|
|
175
|
+
let wasPlaying = false;
|
|
176
|
+
function onMove(e) { if (scrubbing) seekTime(clientToTime(e.clientX)); }
|
|
177
|
+
function onEnd() { if (!scrubbing) return; scrubbing = false; if (wasPlaying) movie.play(); }
|
|
178
|
+
|
|
179
|
+
function mount() {
|
|
180
|
+
const data = movie.timelineData();
|
|
181
|
+
names.innerHTML = '<div class="ruler" style="height:34px"></div>' + data.rows.map(r =>
|
|
182
|
+
'<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
|
+
render(null);
|
|
185
|
+
$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
|
+
$play.disabled = false; $zfit.disabled = true;
|
|
187
|
+
|
|
188
|
+
$play.addEventListener('click', toggle);
|
|
189
|
+
$zin.addEventListener('click', () => setZoom(zoom * 1.6, playheadAnchor()));
|
|
190
|
+
$zout.addEventListener('click', () => setZoom(zoom / 1.6, playheadAnchor()));
|
|
191
|
+
$zfit.addEventListener('click', () => setZoom(1, null));
|
|
192
|
+
chart.addEventListener('wheel', e => {
|
|
193
|
+
if (!(e.ctrlKey || e.metaKey)) return; // a plain wheel scrolls as usual
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
const r = chart.getBoundingClientRect(), overPlot = e.clientX - r.left > NAMES_W;
|
|
196
|
+
const t = overPlot ? clientToTime(e.clientX) : nowT(), x = overPlot ? e.clientX - r.left : playheadAnchor().x;
|
|
197
|
+
setZoom(zoom * Math.exp(-e.deltaY * 0.0025), { t, x });
|
|
198
|
+
}, { passive: false });
|
|
199
|
+
addEventListener('keydown', e => {
|
|
200
|
+
if (e.target && /input|textarea|select/i.test(e.target.tagName)) return;
|
|
201
|
+
const fps = movie.frameRate, f = movie.currentFrame;
|
|
202
|
+
if (e.key === ' ') { e.preventDefault(); toggle(); }
|
|
203
|
+
else if (e.key === 'ArrowLeft') { e.preventDefault(); seekFrame(f - (e.shiftKey ? fps : 1)); }
|
|
204
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); seekFrame(f + (e.shiftKey ? fps : 1)); }
|
|
205
|
+
else if (e.key === 'Home') { e.preventDefault(); seekFrame(0); }
|
|
206
|
+
else if (e.key === 'End') { e.preventDefault(); seekFrame(movie.totalFrames); }
|
|
207
|
+
else if (e.key === '+' || e.key === '=') { e.preventDefault(); setZoom(zoom * 1.6, playheadAnchor()); }
|
|
208
|
+
else if (e.key === '-') { e.preventDefault(); setZoom(zoom / 1.6, playheadAnchor()); }
|
|
209
|
+
else if (e.key === '0') { e.preventDefault(); setZoom(1, null); }
|
|
210
|
+
});
|
|
211
|
+
requestAnimationFrame(tick);
|
|
212
|
+
}
|
|
213
|
+
const toggle = () => { if (movie.isPlaying) movie.pause(); else movie.play(); };
|
|
214
|
+
|
|
215
|
+
function tick() {
|
|
216
|
+
const t = nowT(), x = tToX(t);
|
|
217
|
+
head.setAttribute('transform', 'translate(' + x.toFixed(2) + ',0)');
|
|
218
|
+
$time.textContent = t.toFixed(2) + ' / ' + geom.dur.toFixed(2) + ' s · frame ' + movie.currentFrame;
|
|
219
|
+
$play.textContent = movie.isPlaying ? '❚❚' : '▶';
|
|
220
|
+
if (movie.isPlaying && !scrubbing && zoom > 1) { // follow the playhead when it runs off the visible part
|
|
221
|
+
const screenX = NAMES_W + x - chart.scrollLeft;
|
|
222
|
+
if (screenX > chart.clientWidth - 40 || screenX < NAMES_W) chart.scrollLeft = Math.max(0, NAMES_W + x - NAMES_W - 120);
|
|
223
|
+
}
|
|
224
|
+
requestAnimationFrame(tick);
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
227
|
+
</script>
|
|
228
|
+
</body></html>
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Open an address in the default browser (macOS open, Windows start, otherwise xdg-open). */
|
|
233
|
+
function openBrowser(url) {
|
|
234
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : ['xdg-open', [url]];
|
|
235
|
+
try { spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => {}).unref(); } catch { /* the address is printed anyway */ }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function startViewer(opts) {
|
|
239
|
+
const pagePath = path.resolve(opts.page);
|
|
240
|
+
if (!fs.existsSync(pagePath)) throw new Error(`page not found: ${opts.page}`);
|
|
241
|
+
const root = path.resolve(opts.root ?? findRoot(path.dirname(pagePath)));
|
|
242
|
+
if (!pagePath.startsWith(root)) throw new Error(`the page must be inside the server root (${root}); use --root`);
|
|
243
|
+
const rel = path.relative(root, pagePath).split(path.sep).join('/');
|
|
244
|
+
const pageUrl = '/' + rel + (opts.query ? '?' + opts.query : '');
|
|
245
|
+
const title = path.basename(pagePath).replace(/\.[^.]+$/, '');
|
|
246
|
+
const html = viewerHtml({ pageUrl, title });
|
|
247
|
+
const { server, port } = await serve(root, (req, res) => {
|
|
248
|
+
if (req.method !== 'GET' || !req.url.startsWith(VIEWER)) return false;
|
|
249
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' }).end(html);
|
|
250
|
+
return true;
|
|
251
|
+
}, opts.port);
|
|
252
|
+
return { server, url: `http://127.0.0.1:${port}${VIEWER}` };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
256
|
+
try {
|
|
257
|
+
const opts = parseViewArgs(process.argv.slice(2));
|
|
258
|
+
if (!opts.page) {
|
|
259
|
+
console.error('usage: node ai/tools/view.mjs <page.html> [--port N] [--no-open] [--query a=1] [--root DIR]');
|
|
260
|
+
process.exit(2);
|
|
261
|
+
}
|
|
262
|
+
const { url } = await startViewer(opts);
|
|
263
|
+
console.log(`timeline view: ${url} (Ctrl-C to stop)`);
|
|
264
|
+
if (opts.open) openBrowser(url);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
console.error(`pixi-effects view failed: ${e.message}`);
|
|
267
|
+
process.exit(2);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape, B as AudioDescriptor } from './types-
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape, B as AudioDescriptor } from './types-oJijbn-K.cjs';
|
|
3
3
|
import { gsap } from 'gsap';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape, B as AudioDescriptor } from './types-
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape, B as AudioDescriptor } from './types-oJijbn-K.js';
|
|
3
3
|
import { gsap } from 'gsap';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk4VSIJLU3_cjs = require('./chunk-4VSIJLU3.cjs');
|
|
4
|
+
require('./chunk-6I55T4OH.cjs');
|
|
5
|
+
require('./chunk-UQBGW5DO.cjs');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Object.defineProperty(exports, "CompositionSequence", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: function () { return chunk4VSIJLU3_cjs.CompositionSequence; }
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=Composition-3KHQPRBA.cjs.map
|
|
14
|
+
//# sourceMappingURL=Composition-3KHQPRBA.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-3KHQPRBA.cjs"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-DT657FO5.js"}
|
package/dist/Controller.cjs
CHANGED
|
@@ -279,6 +279,11 @@ var Controller = class _Controller {
|
|
|
279
279
|
root;
|
|
280
280
|
wrapper;
|
|
281
281
|
wrappedHere = false;
|
|
282
|
+
/** The canvas's own inline width / height, as the page wrote them (we override them while the wrapper is fitted). */
|
|
283
|
+
canvasInlineWidth = "";
|
|
284
|
+
canvasInlineHeight = "";
|
|
285
|
+
fitWrapHandler = null;
|
|
286
|
+
fitObserver = null;
|
|
282
287
|
destroyed = false;
|
|
283
288
|
progressEl;
|
|
284
289
|
progressFillEl;
|
|
@@ -309,6 +314,7 @@ var Controller = class _Controller {
|
|
|
309
314
|
settingsOutsideHandler = null;
|
|
310
315
|
onReady = null;
|
|
311
316
|
onFrame = null;
|
|
317
|
+
onPlay = null;
|
|
312
318
|
onPause = null;
|
|
313
319
|
onProgress = null;
|
|
314
320
|
onWrapPointerMove = null;
|
|
@@ -339,6 +345,7 @@ var Controller = class _Controller {
|
|
|
339
345
|
this.root.className = this.options.className;
|
|
340
346
|
this.wrapper.appendChild(this.root);
|
|
341
347
|
this.buildBar();
|
|
348
|
+
this.bindFitWrap();
|
|
342
349
|
this.syncRootToCanvas();
|
|
343
350
|
if (typeof ResizeObserver !== "undefined") {
|
|
344
351
|
this.resizeObserver = new ResizeObserver(() => this.syncRootToCanvas());
|
|
@@ -436,8 +443,56 @@ var Controller = class _Controller {
|
|
|
436
443
|
parent.insertBefore(wrap, canvas);
|
|
437
444
|
wrap.appendChild(canvas);
|
|
438
445
|
this.wrappedHere = true;
|
|
446
|
+
this.canvasInlineWidth = canvas.style.width;
|
|
447
|
+
this.canvasInlineHeight = canvas.style.height;
|
|
439
448
|
return wrap;
|
|
440
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Make the wrapper exactly as wide as the canvas. A shrink-to-fit wrapper around a canvas that is sized in percent (the usual
|
|
452
|
+
* `canvas { width: min(960px, 100%) }`) is as wide as the canvas's width ATTRIBUTE (1280), so the picture sat at its left edge
|
|
453
|
+
* with an empty strip beside it. We measure the canvas as the page styled it (the wrapper made transparent to layout), give
|
|
454
|
+
* the wrapper that width, and let the canvas fill the wrapper. Re-measured when the window or the page's box changes.
|
|
455
|
+
*/
|
|
456
|
+
fitWrap() {
|
|
457
|
+
if (!this.wrappedHere) return;
|
|
458
|
+
const wrap = this.wrapper, canvas = this.options.canvas;
|
|
459
|
+
if (document.fullscreenElement === wrap) {
|
|
460
|
+
wrap.style.width = "";
|
|
461
|
+
canvas.style.width = this.canvasInlineWidth;
|
|
462
|
+
canvas.style.height = this.canvasInlineHeight;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
canvas.style.width = this.canvasInlineWidth;
|
|
466
|
+
canvas.style.height = this.canvasInlineHeight;
|
|
467
|
+
const display = wrap.style.display;
|
|
468
|
+
wrap.style.display = "contents";
|
|
469
|
+
wrap.style.width = "";
|
|
470
|
+
const w = canvas.getBoundingClientRect().width;
|
|
471
|
+
wrap.style.display = display;
|
|
472
|
+
if (!(w > 0)) return;
|
|
473
|
+
wrap.style.width = `${w}px`;
|
|
474
|
+
canvas.style.width = "100%";
|
|
475
|
+
canvas.style.height = "auto";
|
|
476
|
+
}
|
|
477
|
+
bindFitWrap() {
|
|
478
|
+
if (!this.wrappedHere) return;
|
|
479
|
+
this.fitWrap();
|
|
480
|
+
let queued = false;
|
|
481
|
+
this.fitWrapHandler = () => {
|
|
482
|
+
if (queued) return;
|
|
483
|
+
queued = true;
|
|
484
|
+
requestAnimationFrame(() => {
|
|
485
|
+
queued = false;
|
|
486
|
+
this.fitWrap();
|
|
487
|
+
});
|
|
488
|
+
};
|
|
489
|
+
window.addEventListener("resize", this.fitWrapHandler);
|
|
490
|
+
const parent = this.wrapper.parentElement;
|
|
491
|
+
if (parent && typeof ResizeObserver !== "undefined") {
|
|
492
|
+
this.fitObserver = new ResizeObserver(this.fitWrapHandler);
|
|
493
|
+
this.fitObserver.observe(parent);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
441
496
|
bindMovieEvents() {
|
|
442
497
|
this.onReady = () => {
|
|
443
498
|
this.progressEl.setAttribute("aria-valuemax", String(this.movie.totalFrames));
|
|
@@ -451,6 +506,10 @@ var Controller = class _Controller {
|
|
|
451
506
|
}
|
|
452
507
|
this.refreshTime(frame);
|
|
453
508
|
};
|
|
509
|
+
this.onPlay = () => {
|
|
510
|
+
this.refreshPlayIcon();
|
|
511
|
+
this.kickIdleTimer();
|
|
512
|
+
};
|
|
454
513
|
this.onPause = () => {
|
|
455
514
|
this.refreshPlayIcon();
|
|
456
515
|
this.kickIdleTimer();
|
|
@@ -462,6 +521,7 @@ var Controller = class _Controller {
|
|
|
462
521
|
};
|
|
463
522
|
this.movie.on("ready", this.onReady);
|
|
464
523
|
this.movie.on("frame", this.onFrame);
|
|
524
|
+
this.movie.on("play", this.onPlay);
|
|
465
525
|
this.movie.on("pause", this.onPause);
|
|
466
526
|
this.movie.on("progress", this.onProgress);
|
|
467
527
|
}
|
|
@@ -471,8 +531,6 @@ var Controller = class _Controller {
|
|
|
471
531
|
this.movie.pause();
|
|
472
532
|
} else {
|
|
473
533
|
this.movie.play();
|
|
474
|
-
this.refreshPlayIcon();
|
|
475
|
-
this.kickIdleTimer();
|
|
476
534
|
}
|
|
477
535
|
});
|
|
478
536
|
}
|
|
@@ -553,6 +611,7 @@ var Controller = class _Controller {
|
|
|
553
611
|
void this.toggleFullscreen();
|
|
554
612
|
});
|
|
555
613
|
this.fullscreenChangeHandler = () => {
|
|
614
|
+
this.fitWrap();
|
|
556
615
|
this.refreshFullscreenIcon();
|
|
557
616
|
this.syncRootToCanvas();
|
|
558
617
|
};
|
|
@@ -886,6 +945,10 @@ var Controller = class _Controller {
|
|
|
886
945
|
this.movie.off("frame", this.onFrame);
|
|
887
946
|
this.onFrame = null;
|
|
888
947
|
}
|
|
948
|
+
if (this.onPlay) {
|
|
949
|
+
this.movie.off("play", this.onPlay);
|
|
950
|
+
this.onPlay = null;
|
|
951
|
+
}
|
|
889
952
|
if (this.onPause) {
|
|
890
953
|
this.movie.off("pause", this.onPause);
|
|
891
954
|
this.onPause = null;
|
|
@@ -906,6 +969,18 @@ var Controller = class _Controller {
|
|
|
906
969
|
this.resizeObserver.disconnect();
|
|
907
970
|
this.resizeObserver = null;
|
|
908
971
|
}
|
|
972
|
+
if (this.fitObserver) {
|
|
973
|
+
this.fitObserver.disconnect();
|
|
974
|
+
this.fitObserver = null;
|
|
975
|
+
}
|
|
976
|
+
if (this.fitWrapHandler) {
|
|
977
|
+
window.removeEventListener("resize", this.fitWrapHandler);
|
|
978
|
+
this.fitWrapHandler = null;
|
|
979
|
+
}
|
|
980
|
+
if (this.wrappedHere) {
|
|
981
|
+
this.options.canvas.style.width = this.canvasInlineWidth;
|
|
982
|
+
this.options.canvas.style.height = this.canvasInlineHeight;
|
|
983
|
+
}
|
|
909
984
|
this.root.remove();
|
|
910
985
|
if (this.wrappedHere) {
|
|
911
986
|
const parent = this.wrapper.parentElement;
|