pixi-effects 0.6.0 → 0.8.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +18 -11
  3. package/ai/SKILL.md +1 -1
  4. package/ai/reference/cheatsheet.md +9 -1
  5. package/ai/template.html +4 -3
  6. package/ai/tools/check.mjs +14 -5
  7. package/ai/tools/render.mjs +202 -0
  8. package/ai/tools/view.mjs +269 -0
  9. package/dist/{Base-4vNLl4tH.d.cts → Base-CSIulyIq.d.cts} +1 -1
  10. package/dist/{Base-CD4AoT5-.d.ts → Base-DVgwyIUj.d.ts} +1 -1
  11. package/dist/Composition-CM62XJKT.cjs +14 -0
  12. package/dist/{Composition-Y5VZSDDP.cjs.map → Composition-CM62XJKT.cjs.map} +1 -1
  13. package/dist/Composition-RJDSQYTV.js +5 -0
  14. package/dist/{Composition-7SO5WYHU.js.map → Composition-RJDSQYTV.js.map} +1 -1
  15. package/dist/Controller.cjs +122 -20
  16. package/dist/Controller.cjs.map +1 -1
  17. package/dist/Controller.d.cts +45 -3
  18. package/dist/Controller.d.ts +45 -3
  19. package/dist/Controller.js +122 -21
  20. package/dist/Controller.js.map +1 -1
  21. package/dist/{Movie-Ch2SUI1h.d.ts → Movie-CQOhUmko.d.cts} +75 -2
  22. package/dist/{Movie-CADDBfpl.d.cts → Movie-HF3pSGjQ.d.ts} +75 -2
  23. package/dist/{chunk-KQM2ZZMK.cjs → chunk-AGMNSJJH.cjs} +175 -29
  24. package/dist/chunk-AGMNSJJH.cjs.map +1 -0
  25. package/dist/{chunk-7IJHJXDI.js → chunk-FNKSXBKB.js} +136 -10
  26. package/dist/chunk-FNKSXBKB.js.map +1 -0
  27. package/dist/{chunk-B2FTJLNB.js → chunk-O4AJYUPK.js} +3 -3
  28. package/dist/{chunk-B2FTJLNB.js.map → chunk-O4AJYUPK.js.map} +1 -1
  29. package/dist/{chunk-4AGJF5RK.cjs → chunk-PDBDDGG5.cjs} +21 -21
  30. package/dist/{chunk-4AGJF5RK.cjs.map → chunk-PDBDDGG5.cjs.map} +1 -1
  31. package/dist/index.cjs +255 -25
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +4 -4
  34. package/dist/index.d.ts +4 -4
  35. package/dist/index.js +248 -18
  36. package/dist/index.js.map +1 -1
  37. package/dist/three.cjs +6 -5
  38. package/dist/three.cjs.map +1 -1
  39. package/dist/three.d.cts +2 -2
  40. package/dist/three.d.ts +2 -2
  41. package/dist/three.js +2 -1
  42. package/dist/three.js.map +1 -1
  43. package/dist/{types-DrW9QeKU.d.ts → types-oJijbn-K.d.cts} +14 -1
  44. package/dist/{types-DrW9QeKU.d.cts → types-oJijbn-K.d.ts} +14 -1
  45. package/docs/api.md +57 -0
  46. package/docs/dsl.md +60 -1
  47. package/llms-full.txt +127 -3
  48. package/package.json +7 -4
  49. package/dist/Composition-7SO5WYHU.js +0 -4
  50. package/dist/Composition-Y5VZSDDP.cjs +0 -13
  51. package/dist/chunk-7IJHJXDI.js.map +0 -1
  52. package/dist/chunk-KQM2ZZMK.cjs.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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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-DrW9QeKU.cjs';
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-DrW9QeKU.js';
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 chunkPDBDDGG5_cjs = require('./chunk-PDBDDGG5.cjs');
4
+ require('./chunk-AGMNSJJH.cjs');
5
+ require('./chunk-UQBGW5DO.cjs');
6
+
7
+
8
+
9
+ Object.defineProperty(exports, "CompositionSequence", {
10
+ enumerable: true,
11
+ get: function () { return chunkPDBDDGG5_cjs.CompositionSequence; }
12
+ });
13
+ //# sourceMappingURL=Composition-CM62XJKT.cjs.map
14
+ //# sourceMappingURL=Composition-CM62XJKT.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-Y5VZSDDP.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-CM62XJKT.cjs"}
@@ -0,0 +1,5 @@
1
+ export { CompositionSequence } from './chunk-O4AJYUPK.js';
2
+ import './chunk-FNKSXBKB.js';
3
+ import './chunk-DKGG7WD4.js';
4
+ //# sourceMappingURL=Composition-RJDSQYTV.js.map
5
+ //# sourceMappingURL=Composition-RJDSQYTV.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-7SO5WYHU.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-RJDSQYTV.js"}
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var chunkUQBGW5DO_cjs = require('./chunk-UQBGW5DO.cjs');
4
+
3
5
  // src/Controller.ts
4
6
  var ICONS = {
5
7
  play: '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3 2 L13 8 L3 14 Z"/></svg>',
@@ -11,12 +13,12 @@ var ICONS = {
11
13
  fullscreenExit: '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 V6 H2 M10 2 V6 H14 M6 14 V10 H2 M10 14 V10 H14"/></svg>'
12
14
  };
13
15
  var STYLE_ATTR = "data-movie-controller";
14
- var STYLE_CSS = `
16
+ var CONTROLLER_CSS = `
15
17
  .movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }
16
18
  .movie-controller {
17
19
  position: absolute;
18
20
  pointer-events: none;
19
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
21
+ font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
20
22
  display: flex;
21
23
  flex-direction: column;
22
24
  justify-content: flex-end;
@@ -36,31 +38,31 @@ var STYLE_CSS = `
36
38
  outline: none;
37
39
  }
38
40
  .mc-progress:focus-visible {
39
- outline: 2px solid #007AFF;
41
+ outline: 2px solid var(--mc-accent, #007AFF);
40
42
  outline-offset: 2px;
41
43
  }
42
44
  .mc-progress::before {
43
45
  content: ""; position: absolute; left: 12px; right: 12px;
44
- height: 3px; background: rgba(255,255,255,0.25);
46
+ height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));
45
47
  transition: height 120ms ease;
46
48
  }
47
- .mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: 5px; }
49
+ .mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }
48
50
  .mc-progress-fill {
49
51
  position: absolute; left: 12px; top: 50%;
50
- height: 3px;
52
+ height: var(--mc-track-height, 3px);
51
53
  width: calc((100% - 24px) * var(--mc-fill, 0));
52
- background: #007AFF;
54
+ background: var(--mc-accent, #007AFF);
53
55
  transform: translateY(-50%);
54
56
  transition: height 120ms ease;
55
57
  pointer-events: none;
56
58
  }
57
59
  .mc-progress:hover .mc-progress-fill,
58
- .mc-progress.mc-scrubbing .mc-progress-fill { height: 5px; }
60
+ .mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }
59
61
  .mc-progress-thumb {
60
62
  position: absolute; top: 50%;
61
63
  left: calc(12px + (100% - 24px) * var(--mc-fill, 0));
62
64
  width: 12px; height: 12px; border-radius: 50%;
63
- background: #007AFF;
65
+ background: var(--mc-accent, #007AFF);
64
66
  transform: translate(-50%, -50%) scale(0);
65
67
  transition: transform 120ms ease;
66
68
  pointer-events: none;
@@ -74,22 +76,22 @@ var STYLE_CSS = `
74
76
  align-items: center;
75
77
  gap: 12px;
76
78
  padding: 6px 12px 8px 12px;
77
- background: linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%);
78
- color: #fff;
79
+ background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));
80
+ color: var(--mc-fg, #fff);
79
81
  line-height: 1;
80
82
  }
81
83
  .mc-btn {
82
84
  background: none; border: 0; padding: 0; margin: 0;
83
85
  width: 28px; height: 28px;
84
86
  display: inline-flex; align-items: center; justify-content: center;
85
- color: #fff; opacity: 0.85; cursor: pointer;
87
+ color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;
86
88
  transition: opacity 120ms ease;
87
89
  }
88
90
  .mc-btn:hover { opacity: 1; }
89
91
  .mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }
90
92
  .mc-time {
91
93
  font-size: 12px; font-family: Menlo, Monaco, monospace;
92
- color: #fff; opacity: 0.85;
94
+ color: var(--mc-fg, #fff); opacity: 0.85;
93
95
  min-width: 90px;
94
96
  }
95
97
  .mc-spacer { flex: 1; }
@@ -113,16 +115,16 @@ var STYLE_CSS = `
113
115
  width: 70px;
114
116
  margin-left: 6px;
115
117
  }
116
- .mc-vol-slider:focus-visible { outline: 2px solid #007AFF; outline-offset: 2px; }
118
+ .mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }
117
119
  .mc-vol-slider::before {
118
120
  content: ""; position: absolute; left: 4px; right: 4px;
119
- height: 3px; background: rgba(255,255,255,0.25); border-radius: 2px;
121
+ height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;
120
122
  }
121
123
  .mc-vol-fill {
122
124
  position: absolute; left: 4px; top: 50%;
123
125
  height: 3px;
124
126
  width: calc((100% - 8px) * var(--mc-volume, 1));
125
- background: #fff;
127
+ background: var(--mc-fg, #fff);
126
128
  transform: translateY(-50%);
127
129
  border-radius: 2px;
128
130
  pointer-events: none;
@@ -131,7 +133,7 @@ var STYLE_CSS = `
131
133
  position: absolute; top: 50%;
132
134
  left: calc(4px + (100% - 8px) * var(--mc-volume, 1));
133
135
  width: 10px; height: 10px; border-radius: 50%;
134
- background: #fff;
136
+ background: var(--mc-fg, #fff);
135
137
  transform: translate(-50%, -50%);
136
138
  pointer-events: none;
137
139
  }
@@ -185,7 +187,7 @@ var STYLE_CSS = `
185
187
  .mc-export-confirm {
186
188
  margin-top: 4px;
187
189
  width: 100%;
188
- background: #007AFF;
190
+ background: var(--mc-accent, #007AFF);
189
191
  color: #fff;
190
192
  border: 0;
191
193
  border-radius: 4px;
@@ -218,7 +220,7 @@ var STYLE_CSS = `
218
220
  }
219
221
  .mc-export-fill {
220
222
  height: 100%; width: 0%;
221
- background: linear-gradient(90deg, #007AFF, #0056CC);
223
+ background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));
222
224
  transition: width 0.3s ease; border-radius: 3px;
223
225
  }
224
226
  .mc-export-text {
@@ -231,7 +233,7 @@ function installStyles() {
231
233
  if (styleRefCount === 0) {
232
234
  const el = document.createElement("style");
233
235
  el.setAttribute(STYLE_ATTR, "");
234
- el.textContent = STYLE_CSS;
236
+ el.textContent = CONTROLLER_CSS;
235
237
  document.head.appendChild(el);
236
238
  }
237
239
  styleRefCount++;
@@ -273,12 +275,25 @@ function extensionForMimeType(mime) {
273
275
  if (t.includes("matroska") || t.includes("mkv")) return "mkv";
274
276
  return "mp4";
275
277
  }
278
+ var THEME_PROPERTIES = {
279
+ accent: "--mc-accent",
280
+ foreground: "--mc-fg",
281
+ track: "--mc-track",
282
+ barBackground: "--mc-bar-bg",
283
+ trackHeight: "--mc-track-height",
284
+ font: "--mc-font"
285
+ };
276
286
  var Controller = class _Controller {
277
287
  movie;
278
288
  options;
279
289
  root;
280
290
  wrapper;
281
291
  wrappedHere = false;
292
+ /** The canvas's own inline width / height, as the page wrote them (we override them while the wrapper is fitted). */
293
+ canvasInlineWidth = "";
294
+ canvasInlineHeight = "";
295
+ fitWrapHandler = null;
296
+ fitObserver = null;
282
297
  destroyed = false;
283
298
  progressEl;
284
299
  progressFillEl;
@@ -340,6 +355,8 @@ var Controller = class _Controller {
340
355
  this.root.className = this.options.className;
341
356
  this.wrapper.appendChild(this.root);
342
357
  this.buildBar();
358
+ this.setTheme(options.theme ?? {});
359
+ this.bindFitWrap();
343
360
  this.syncRootToCanvas();
344
361
  if (typeof ResizeObserver !== "undefined") {
345
362
  this.resizeObserver = new ResizeObserver(() => this.syncRootToCanvas());
@@ -418,6 +435,17 @@ var Controller = class _Controller {
418
435
  if (this.settingsFormatSelect) this.settingsFormatSelect.value = this.exportFormat;
419
436
  if (this.settingsQualitySelect) this.settingsQualitySelect.value = this.exportQuality;
420
437
  }
438
+ /** Change the look of the bar (see {@link ControllerTheme}): only the keys you give change, `null` restores the default. */
439
+ setTheme(theme) {
440
+ chunkUQBGW5DO_cjs.warnUnknownOptions("Controller theme", theme, Object.keys(THEME_PROPERTIES));
441
+ for (const key of Object.keys(THEME_PROPERTIES)) {
442
+ if (!(key in theme)) continue;
443
+ const value = theme[key];
444
+ const prop = THEME_PROPERTIES[key];
445
+ if (value === null || value === void 0 || value === "") this.root.style.removeProperty(prop);
446
+ else this.root.style.setProperty(prop, typeof value === "number" ? `${value}px` : String(value));
447
+ }
448
+ }
421
449
  ensurePositioningContext(canvas) {
422
450
  const parent = canvas.parentElement;
423
451
  if (!parent) {
@@ -437,8 +465,56 @@ var Controller = class _Controller {
437
465
  parent.insertBefore(wrap, canvas);
438
466
  wrap.appendChild(canvas);
439
467
  this.wrappedHere = true;
468
+ this.canvasInlineWidth = canvas.style.width;
469
+ this.canvasInlineHeight = canvas.style.height;
440
470
  return wrap;
441
471
  }
472
+ /**
473
+ * Make the wrapper exactly as wide as the canvas. A shrink-to-fit wrapper around a canvas that is sized in percent (the usual
474
+ * `canvas { width: min(960px, 100%) }`) is as wide as the canvas's width ATTRIBUTE (1280), so the picture sat at its left edge
475
+ * with an empty strip beside it. We measure the canvas as the page styled it (the wrapper made transparent to layout), give
476
+ * the wrapper that width, and let the canvas fill the wrapper. Re-measured when the window or the page's box changes.
477
+ */
478
+ fitWrap() {
479
+ if (!this.wrappedHere) return;
480
+ const wrap = this.wrapper, canvas = this.options.canvas;
481
+ if (document.fullscreenElement === wrap) {
482
+ wrap.style.width = "";
483
+ canvas.style.width = this.canvasInlineWidth;
484
+ canvas.style.height = this.canvasInlineHeight;
485
+ return;
486
+ }
487
+ canvas.style.width = this.canvasInlineWidth;
488
+ canvas.style.height = this.canvasInlineHeight;
489
+ const display = wrap.style.display;
490
+ wrap.style.display = "contents";
491
+ wrap.style.width = "";
492
+ const w = canvas.getBoundingClientRect().width;
493
+ wrap.style.display = display;
494
+ if (!(w > 0)) return;
495
+ wrap.style.width = `${w}px`;
496
+ canvas.style.width = "100%";
497
+ canvas.style.height = "auto";
498
+ }
499
+ bindFitWrap() {
500
+ if (!this.wrappedHere) return;
501
+ this.fitWrap();
502
+ let queued = false;
503
+ this.fitWrapHandler = () => {
504
+ if (queued) return;
505
+ queued = true;
506
+ requestAnimationFrame(() => {
507
+ queued = false;
508
+ this.fitWrap();
509
+ });
510
+ };
511
+ window.addEventListener("resize", this.fitWrapHandler);
512
+ const parent = this.wrapper.parentElement;
513
+ if (parent && typeof ResizeObserver !== "undefined") {
514
+ this.fitObserver = new ResizeObserver(this.fitWrapHandler);
515
+ this.fitObserver.observe(parent);
516
+ }
517
+ }
442
518
  bindMovieEvents() {
443
519
  this.onReady = () => {
444
520
  this.progressEl.setAttribute("aria-valuemax", String(this.movie.totalFrames));
@@ -551,12 +627,25 @@ var Controller = class _Controller {
551
627
  };
552
628
  container.addEventListener("wheel", this.wheelHandler, { passive: false });
553
629
  }
630
+ /**
631
+ * Does this browser have the Fullscreen API for an element? iPhone Safari does not (it can only put a <video> in fullscreen),
632
+ * and a button that does nothing is worse than none.
633
+ */
634
+ supportsFullscreen() {
635
+ return typeof this.wrapper.requestFullscreen === "function" && document.fullscreenEnabled !== false;
636
+ }
554
637
  bindFullscreen() {
555
638
  if (!this.fullscreenBtn) return;
639
+ if (!this.supportsFullscreen()) {
640
+ this.fullscreenBtn.remove();
641
+ this.fullscreenBtn = null;
642
+ return;
643
+ }
556
644
  this.fullscreenBtn.addEventListener("click", () => {
557
645
  void this.toggleFullscreen();
558
646
  });
559
647
  this.fullscreenChangeHandler = () => {
648
+ this.fitWrap();
560
649
  this.refreshFullscreenIcon();
561
650
  this.syncRootToCanvas();
562
651
  };
@@ -914,6 +1003,18 @@ var Controller = class _Controller {
914
1003
  this.resizeObserver.disconnect();
915
1004
  this.resizeObserver = null;
916
1005
  }
1006
+ if (this.fitObserver) {
1007
+ this.fitObserver.disconnect();
1008
+ this.fitObserver = null;
1009
+ }
1010
+ if (this.fitWrapHandler) {
1011
+ window.removeEventListener("resize", this.fitWrapHandler);
1012
+ this.fitWrapHandler = null;
1013
+ }
1014
+ if (this.wrappedHere) {
1015
+ this.options.canvas.style.width = this.canvasInlineWidth;
1016
+ this.options.canvas.style.height = this.canvasInlineHeight;
1017
+ }
917
1018
  this.root.remove();
918
1019
  if (this.wrappedHere) {
919
1020
  const parent = this.wrapper.parentElement;
@@ -962,6 +1063,7 @@ var Controller = class _Controller {
962
1063
  }
963
1064
  };
964
1065
 
1066
+ exports.CONTROLLER_CSS = CONTROLLER_CSS;
965
1067
  exports.Controller = Controller;
966
1068
  exports.extensionForMimeType = extensionForMimeType;
967
1069
  exports.formatTime = formatTime;