multiplane 0.3.6 → 0.3.7

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 (67) hide show
  1. package/LICENSE +202 -202
  2. package/THIRD_PARTY_NOTICES.md +30 -30
  3. package/examples/alpha.html +18 -18
  4. package/examples/animation.html +17 -17
  5. package/examples/assets/OFL.txt +93 -93
  6. package/examples/assets/card.svg +6 -6
  7. package/examples/three-cards.html +17 -17
  8. package/examples/typography.html +17 -17
  9. package/licenses/GPL-3.0.txt +232 -232
  10. package/licenses/LGPL-3.0.txt +304 -304
  11. package/licenses/acorn-LICENSE +21 -21
  12. package/licenses/colour-LICENSE.md +82 -82
  13. package/licenses/detect-libc-LICENSE +201 -201
  14. package/licenses/entities-LICENSE +11 -11
  15. package/licenses/nanoid-LICENSE +20 -20
  16. package/licenses/parse5-LICENSE +19 -19
  17. package/licenses/picocolors-LICENSE +15 -15
  18. package/licenses/playwright-NOTICE +5 -5
  19. package/licenses/playwright-ThirdPartyNotices.txt +14 -14
  20. package/licenses/playwright-core-ThirdPartyNotices.txt +13 -13
  21. package/licenses/postcss-LICENSE +20 -20
  22. package/licenses/postcss-value-parser-LICENSE +22 -22
  23. package/licenses/semver-LICENSE +15 -15
  24. package/licenses/sharp-LICENSE +191 -191
  25. package/licenses/sharp-win32-x64-NOTICES.md +45 -45
  26. package/licenses/source-map-js-LICENSE +28 -28
  27. package/licenses/three-LICENSE +21 -21
  28. package/package.json +92 -92
  29. package/src/alpha.mjs +69 -69
  30. package/src/artifacts.mjs +61 -61
  31. package/src/browser-recovery.mjs +26 -26
  32. package/src/capture.mjs +12 -12
  33. package/src/encoder.mjs +16 -16
  34. package/src/environment.mjs +56 -56
  35. package/src/events.mjs +5 -5
  36. package/src/ffmpeg.mjs +32 -32
  37. package/src/format.mjs +27 -27
  38. package/src/frame-rate.mjs +28 -28
  39. package/src/home-migration.mjs +54 -54
  40. package/src/hosted.mjs +1 -1
  41. package/src/lint.mjs +248 -248
  42. package/src/output-alpha.mjs +36 -36
  43. package/src/output-check.mjs +110 -110
  44. package/src/platform.mjs +114 -114
  45. package/src/png-native.mjs +12 -12
  46. package/src/png.mjs +74 -74
  47. package/src/post.mjs +38 -37
  48. package/src/preview.mjs +23 -23
  49. package/src/process.mjs +9 -9
  50. package/src/render-pool.mjs +2 -2
  51. package/src/render-worker.mjs +1 -1
  52. package/src/resource-path.mjs +29 -29
  53. package/src/resources.mjs +27 -27
  54. package/src/runtime/camera.js +37 -37
  55. package/src/runtime/shot.css +9 -9
  56. package/src/server.mjs +7 -7
  57. package/src/setup.mjs +51 -51
  58. package/src/skills.mjs +128 -128
  59. package/src/stills.mjs +1 -1
  60. package/src/three/index.js +39 -31
  61. package/src/verify.mjs +2 -2
  62. package/src/video-pipe.mjs +6 -6
  63. package/tools/bench/shots/canvas-2d.html +1 -1
  64. package/tools/bench/shots/dom-light.html +1 -1
  65. package/tools/bench/shots/scene.js +44 -44
  66. package/tools/bench/shots/webgl.html +1 -1
  67. package/tools/install-browser.mjs +18 -18
package/src/post.mjs CHANGED
@@ -54,7 +54,7 @@ export function resolvePost(options = {}, post = {}) {
54
54
  if (options['no-post']) return { motionBlur: 1, maxMotionBlur: 1, shutter: 0.5, grain: 0 };
55
55
  let motionBlur = number(options['motion-blur'] ?? post.motionBlur ?? 1, 'motionBlur', { positive: true });
56
56
  if (!Number.isSafeInteger(motionBlur)) throw invalidOption('motionBlur', 'motionBlur must be a positive integer.');
57
- const maxMotionBlur = number(quality === 'draft' ? 1 : options['max-motion-blur'] ?? post.maxMotionBlur ?? (quality === 'high' ? 256 : quality === 'fast' ? 16 : 32), 'maxMotionBlur', { positive: true });
57
+ const maxMotionBlur = number(quality === 'draft' ? 1 : options['max-motion-blur'] ?? post.maxMotionBlur ?? (quality === 'high' ? 256 : quality === 'fast' ? 16 : 32), 'maxMotionBlur', { positive: true });
58
58
  if (!Number.isSafeInteger(maxMotionBlur)) throw invalidOption('maxMotionBlur', 'maxMotionBlur must be a positive integer.');
59
59
  // Preserve explicit bases and fixed sampling before applying the preset cap.
60
60
  if ((quality === 'standard' || quality === 'fast') && options['motion-blur'] == null && maxMotionBlur > motionBlur) motionBlur = Math.min(motionBlur, 4);
@@ -276,19 +276,20 @@ export function createPostCapture(capture, options = {}, fps = capture.fps) {
276
276
  }
277
277
  const verifyReuse = candidate && (audit || previousRaw.run === 0 || frame % REUSE_INTERVAL === 0);
278
278
  const cache = new Map();
279
- let contextUsed = false, motionProbeUsed = false;
280
- let finalCapture = hooked || motionBlur === 1;
281
- let selectedTimes = times;
282
- let dofMotionSamples = new Set(times).size;
283
- const context = observed => ({ motionBlur, motionBlurBase, observed,
284
- motionProbeTime: finalCapture ? undefined : t,
285
- dofMotionSamples: finalCapture ? dofMotionSamples : undefined });
279
+ let contextUsed = false, motionProbeUsed = false;
280
+ let finalCapture = hooked || motionBlur === 1;
281
+ let selectedTimes = times;
282
+ let dofMotionSamples = new Set(times).size;
283
+ const context = (observed, time) => ({ motionBlur, motionBlurBase, observed,
284
+ motionProbeTime: finalCapture ? undefined : t,
285
+ dofMotionSamples: finalCapture ? dofMotionSamples : undefined,
286
+ dofMotionIndex: finalCapture ? [...new Set(selectedTimes)].sort((a, b) => a - b).indexOf(time) : undefined });
286
287
  const sample = async (time, reuse = false) => {
287
288
  if (!cache.has(time)) {
288
289
  const start = performance.now();
289
- const observed = time === observedTime && !(motionBlur > 1 && capture.motionProbeUsed);
290
+ const observed = time === observedTime && !(motionBlur > 1 && capture.motionProbeUsed);
290
291
  observedTime = undefined;
291
- const png = await capture.frame(time, context(observed));
292
+ const png = await capture.frame(time, context(observed, time));
292
293
  contextUsed ||= capture.sampleContextUsed === true;
293
294
  motionProbeUsed ||= capture.motionProbeUsed === true;
294
295
  stats.captureMs += performance.now() - start;
@@ -305,9 +306,9 @@ export function createPostCapture(capture, options = {}, fps = capture.fps) {
305
306
  return;
306
307
  }
307
308
  const begin = async time => {
308
- const start = performance.now(), observed = time === observedTime && !(motionBlur > 1 && capture.motionProbeUsed);
309
+ const start = performance.now(), observed = time === observedTime && !(motionBlur > 1 && capture.motionProbeUsed);
309
310
  observedTime = undefined;
310
- const pending = await capture.beginFrame(time, context(observed));
311
+ const pending = await capture.beginFrame(time, context(observed, time));
311
312
  contextUsed ||= capture.sampleContextUsed === true;
312
313
  motionProbeUsed ||= capture.motionProbeUsed === true;
313
314
  const png = pending.png.then(buffer => { stats.captureMs += performance.now() - start; return buffer; });
@@ -315,9 +316,9 @@ export function createPostCapture(capture, options = {}, fps = capture.fps) {
315
316
  return { png, dofSamples: capture.dofSamples ?? 0, probe: capture.motionProbeUsed === true && !finalCapture };
316
317
  };
317
318
  let pending = await begin(missing[0]);
318
- for (let i = 0; i < missing.length; i++) {
319
- const png = await pending.png;
320
- const { dofSamples, probe } = pending;
319
+ for (let i = 0; i < missing.length; i++) {
320
+ const png = await pending.png;
321
+ const { dofSamples, probe } = pending;
321
322
  const next = i + 1 < missing.length ? await begin(missing[i + 1]) : null;
322
323
  stats.samples++;
323
324
  cache.set(missing[i], { png, dofSamples, probe, image: await decode(png, reuse) });
@@ -356,15 +357,15 @@ export function createPostCapture(capture, options = {}, fps = capture.fps) {
356
357
  }
357
358
  let difference = 0;
358
359
  for (let i = 1; i < images.length && difference <= threshold; i++) {
359
- // All decision images share the output frame's fixed lens phase.
360
- difference = Math.max(difference, localMotionDifference(images[i - 1], images[i], alpha, threshold));
360
+ // All decision images share the output frame's fixed lens phase.
361
+ difference = Math.max(difference, localMotionDifference(images[i - 1], images[i], alpha, threshold));
361
362
  }
362
- if (difference <= threshold || motionBlur >= post.maxMotionBlur) {
363
- usedSamples = motionBlur; selectedTimes = grid;
363
+ if (difference <= threshold || motionBlur >= post.maxMotionBlur) {
364
+ usedSamples = motionBlur; selectedTimes = grid;
364
365
  result = timed('averageMs', () => average(images)); break;
365
366
  }
366
367
  motionBlur = Math.min(post.maxMotionBlur, motionBlur * 2);
367
- if (legacy) cache.clear();
368
+ if (legacy) cache.clear();
368
369
  observedTime = undefined;
369
370
  grid = legacy ? sampleTimes(t, fps, capture.duration, capture.cuts, { ...post, motionBlur }) : grids.get(motionBlur);
370
371
  }
@@ -395,22 +396,22 @@ export function createPostCapture(capture, options = {}, fps = capture.fps) {
395
396
  result = timed('averageMs', () => average(images));
396
397
  }
397
398
  }
398
- let usedDofSamples = previousRaw?.usedDofSamples ?? 0;
399
- if (!(candidate && !verifyReuse)) {
400
- // Only lens-dependent probe captures need replacing. DOM captures and
401
- // final hook captures remain reusable at the same time and context.
402
- if (motionProbeUsed && !finalCapture) {
403
- finalCapture = true;
404
- dofMotionSamples = new Set(selectedTimes).size;
405
- for (const [time, item] of cache) if (item.probe) cache.delete(time);
406
- observedTime = undefined;
407
- await sampleMany(selectedTimes, true);
408
- const images = selectedTimes.map(time => cache.get(time).image);
409
- png = selectedTimes.length === 1 ? cache.get(selectedTimes[0]).png : undefined;
410
- result = selectedTimes.length === 1 ? images[0] : timed('averageMs', () => average(images));
411
- }
412
- usedDofSamples = [...new Set(selectedTimes)].reduce((sum, time) => sum + (cache.get(time)?.dofSamples ?? 0), 0);
413
- }
399
+ let usedDofSamples = previousRaw?.usedDofSamples ?? 0;
400
+ if (!(candidate && !verifyReuse)) {
401
+ // Only lens-dependent probe captures need replacing. DOM captures and
402
+ // final hook captures remain reusable at the same time and context.
403
+ if (motionProbeUsed && !finalCapture) {
404
+ finalCapture = true;
405
+ dofMotionSamples = new Set(selectedTimes).size;
406
+ for (const [time, item] of cache) if (item.probe) cache.delete(time);
407
+ observedTime = undefined;
408
+ await sampleMany(selectedTimes, true);
409
+ const images = selectedTimes.map(time => cache.get(time).image);
410
+ png = selectedTimes.length === 1 ? cache.get(selectedTimes[0]).png : undefined;
411
+ result = selectedTimes.length === 1 ? images[0] : timed('averageMs', () => average(images));
412
+ }
413
+ usedDofSamples = [...new Set(selectedTimes)].reduce((sum, time) => sum + (cache.get(time)?.dofSamples ?? 0), 0);
414
+ }
414
415
  if (verifyReuse) {
415
416
  stats.auditMs += performance.now() - auditStart;
416
417
  Atomics.add(state, 2, 1);
@@ -428,7 +429,7 @@ export function createPostCapture(capture, options = {}, fps = capture.fps) {
428
429
  if (enabled && options.transport === 'raw') result ??= await decode(png);
429
430
  previousRaw = { t, frame, motionBlur, uniform, png, image: result, usedSamples, usedDofSamples, run: candidate ? previousRaw.run + 1 : 0 };
430
431
  // Contributing temporal samples, excluding probes and failed attempts.
431
- api.lastBlurSamples = usedSamples;
432
+ api.lastBlurSamples = usedSamples;
432
433
  api.lastDofSamples = usedDofSamples;
433
434
  stats.frames++;
434
435
  if (sizes.renderScale !== sizes.scale) {
package/src/preview.mjs CHANGED
@@ -1,23 +1,23 @@
1
- import path from 'node:path';
2
- import { writeFile, rename, rm } from 'node:fs/promises';
3
- import sharp from 'sharp';
4
-
5
- export function createPreview(events, width, height, { enabled = true, now = () => performance.now() } = {}) {
6
- let last = -Infinity, failed = false;
7
- return async (pixels, frame) => {
8
- if (!enabled || failed || now() - last < 2000) return;
9
- const file = path.join(events.directory, 'preview.png'), temporary = file + '.tmp';
10
- try {
11
- const png = await sharp(pixels, { raw: { width, height, channels: 4 } })
12
- .resize({ width: 480, height: 480, fit: 'inside', withoutEnlargement: true }).png().toBuffer();
13
- await writeFile(temporary, png);
14
- await rename(temporary, file);
15
- events.preview(frame);
16
- last = now();
17
- } catch {
18
- failed = true;
19
- await rm(temporary, { force: true }).catch(() => {});
20
- events.warning({ code: 'preview_failed', message: 'Preview could not be written; further preview updates are disabled.' });
21
- }
22
- };
23
- }
1
+ import path from 'node:path';
2
+ import { writeFile, rename, rm } from 'node:fs/promises';
3
+ import sharp from 'sharp';
4
+
5
+ export function createPreview(events, width, height, { enabled = true, now = () => performance.now() } = {}) {
6
+ let last = -Infinity, failed = false;
7
+ return async (pixels, frame) => {
8
+ if (!enabled || failed || now() - last < 2000) return;
9
+ const file = path.join(events.directory, 'preview.png'), temporary = file + '.tmp';
10
+ try {
11
+ const png = await sharp(pixels, { raw: { width, height, channels: 4 } })
12
+ .resize({ width: 480, height: 480, fit: 'inside', withoutEnlargement: true }).png().toBuffer();
13
+ await writeFile(temporary, png);
14
+ await rename(temporary, file);
15
+ events.preview(frame);
16
+ last = now();
17
+ } catch {
18
+ failed = true;
19
+ await rm(temporary, { force: true }).catch(() => {});
20
+ events.warning({ code: 'preview_failed', message: 'Preview could not be written; further preview updates are disabled.' });
21
+ }
22
+ };
23
+ }
package/src/process.mjs CHANGED
@@ -1,9 +1,9 @@
1
- // FFmpeg has no descendants; wait for its streams to close before cleaning files.
2
- export function stopChild(child, graceMs = 2000) {
3
- return new Promise(resolve => {
4
- if (child.exitCode !== null || child.signalCode !== null) { resolve(); return; }
5
- const timer = setTimeout(() => child.kill('SIGKILL'), graceMs);
6
- child.once('close', () => { clearTimeout(timer); resolve(); });
7
- child.kill('SIGTERM');
8
- });
9
- }
1
+ // FFmpeg has no descendants; wait for its streams to close before cleaning files.
2
+ export function stopChild(child, graceMs = 2000) {
3
+ return new Promise(resolve => {
4
+ if (child.exitCode !== null || child.signalCode !== null) { resolve(); return; }
5
+ const timer = setTimeout(() => child.kill('SIGKILL'), graceMs);
6
+ child.once('close', () => { clearTimeout(timer); resolve(); });
7
+ child.kill('SIGTERM');
8
+ });
9
+ }
@@ -51,7 +51,7 @@ export function createRenderPool(shot, options, fps, workers, size, onWarning, o
51
51
  }
52
52
  const api = {
53
53
  stats,
54
- get blurSamples() { return blurSampleReport(histogram); },
54
+ get blurSamples() { return blurSampleReport(histogram); },
55
55
  get dofSamples() { return blurSampleReport(dofHistogram); },
56
56
  get staticReuse() { return reuseReport(options); },
57
57
  progress: () => `Samples: ${stats.samples}; static: ${stats.staticFrames}; adaptive: ${stats.adaptiveFrames ?? 0}; hooks: ${stats.hookFrames ?? 0}`,
@@ -78,7 +78,7 @@ export function createRenderPool(shot, options, fps, workers, size, onWarning, o
78
78
  if (options.transport === 'raw') schedule(slot);
79
79
  // Worker audits have already replaced invalid frames in this batch.
80
80
  for (const [i, png] of batch.frames.entries()) {
81
- const count = batch.blurSamples[i], dofCount = batch.dofSamples[i];
81
+ const count = batch.blurSamples[i], dofCount = batch.dofSamples[i];
82
82
  dofHistogram[dofCount] = (dofHistogram[dofCount] ?? 0) + 1;
83
83
  histogram[count] = (histogram[count] ?? 0) + 1;
84
84
  yield { frame: batch.first + i, png, hash: batch.hashes?.[i], blurSamples: count, dofSamples: dofCount };
@@ -41,7 +41,7 @@ try {
41
41
  const png = result.byteOffset === 0 && result.byteLength === result.buffer.byteLength && !isMarkedAsUntransferable(result.buffer)
42
42
  ? new Uint8Array(result.buffer) : Uint8Array.from(result);
43
43
  frames.push(png);
44
- blurSamples.push(item.blurSamples);
44
+ blurSamples.push(item.blurSamples);
45
45
  dofSamples.push(item.dofSamples);
46
46
  if (workerData.options.verify) hashes.push(item.hash);
47
47
  }
@@ -1,29 +1,29 @@
1
- import path from 'node:path';
2
- import { realpath } from 'node:fs';
3
- import { promisify } from 'node:util';
4
-
5
- const nativeRealpath = promisify(realpath.native);
6
-
7
- // Resolve aliases (including Windows 8.3 names) even for missing descendants.
8
- export async function normalizeResourcePath(file) {
9
- const absolute = path.resolve(file);
10
- try { return await nativeRealpath(absolute); }
11
- catch (error) {
12
- if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error;
13
- const parent = path.dirname(absolute);
14
- if (parent === absolute) return absolute;
15
- return path.join(await normalizeResourcePath(parent), path.basename(absolute));
16
- }
17
- }
18
-
19
- export function relativeResourcePath(root, target, platform = process.platform) {
20
- const paths = platform === 'win32' ? path.win32 : path.posix;
21
- return paths.relative(platform === 'win32' ? root.toLowerCase() : root,
22
- platform === 'win32' ? target.toLowerCase() : target);
23
- }
24
-
25
- export async function isInsideResourceRoot(root, target) {
26
- const [base, file] = await Promise.all([normalizeResourcePath(root), normalizeResourcePath(target)]);
27
- const relative = relativeResourcePath(base, file);
28
- return relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
29
- }
1
+ import path from 'node:path';
2
+ import { realpath } from 'node:fs';
3
+ import { promisify } from 'node:util';
4
+
5
+ const nativeRealpath = promisify(realpath.native);
6
+
7
+ // Resolve aliases (including Windows 8.3 names) even for missing descendants.
8
+ export async function normalizeResourcePath(file) {
9
+ const absolute = path.resolve(file);
10
+ try { return await nativeRealpath(absolute); }
11
+ catch (error) {
12
+ if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error;
13
+ const parent = path.dirname(absolute);
14
+ if (parent === absolute) return absolute;
15
+ return path.join(await normalizeResourcePath(parent), path.basename(absolute));
16
+ }
17
+ }
18
+
19
+ export function relativeResourcePath(root, target, platform = process.platform) {
20
+ const paths = platform === 'win32' ? path.win32 : path.posix;
21
+ return paths.relative(platform === 'win32' ? root.toLowerCase() : root,
22
+ platform === 'win32' ? target.toLowerCase() : target);
23
+ }
24
+
25
+ export async function isInsideResourceRoot(root, target) {
26
+ const [base, file] = await Promise.all([normalizeResourcePath(root), normalizeResourcePath(target)]);
27
+ const relative = relativeResourcePath(base, file);
28
+ return relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
29
+ }
package/src/resources.mjs CHANGED
@@ -1,27 +1,27 @@
1
- import { cpus } from 'node:os';
2
- import { createProcessTreeReader } from './platform.mjs';
3
-
4
- // Diagnostics only: never pass these values to frame generation.
5
- export function createResourceSampler({ sample, intervalMs = 1000, onSample = () => {}, enabled = true } = {}) {
6
- const reader = !sample && enabled ? createProcessTreeReader() : undefined;
7
- sample ??= reader?.sample;
8
- let pending, timer, stopped = false, peakMemoryBytes = null, memorySamples = 0, workers = 1;
9
- const take = () => {
10
- if (pending || stopped || !enabled) return pending ?? Promise.resolve();
11
- clearTimeout(timer);
12
- pending = (async () => {
13
- let bytes = null;
14
- try { const value = await sample(); if (Number.isSafeInteger(value) && value > 0) bytes = value; } catch {}
15
- if (bytes !== null) { peakMemoryBytes = Math.max(peakMemoryBytes ?? 0, bytes); memorySamples++; }
16
- try { onSample(bytes); } catch {}
17
- })().finally(() => { pending = undefined; if (!stopped) { timer = setTimeout(take, intervalMs); timer.unref(); } });
18
- return pending;
19
- };
20
- void take();
21
- return {
22
- sample: take,
23
- setWorkers(value) { workers = value; },
24
- snapshot: () => ({ peakMemoryBytes, memorySamples, workers, cpuCount: cpus().length || null }),
25
- async stop() { stopped = true; clearTimeout(timer); await pending; await reader?.close(); },
26
- };
27
- }
1
+ import { cpus } from 'node:os';
2
+ import { createProcessTreeReader } from './platform.mjs';
3
+
4
+ // Diagnostics only: never pass these values to frame generation.
5
+ export function createResourceSampler({ sample, intervalMs = 1000, onSample = () => {}, enabled = true } = {}) {
6
+ const reader = !sample && enabled ? createProcessTreeReader() : undefined;
7
+ sample ??= reader?.sample;
8
+ let pending, timer, stopped = false, peakMemoryBytes = null, memorySamples = 0, workers = 1;
9
+ const take = () => {
10
+ if (pending || stopped || !enabled) return pending ?? Promise.resolve();
11
+ clearTimeout(timer);
12
+ pending = (async () => {
13
+ let bytes = null;
14
+ try { const value = await sample(); if (Number.isSafeInteger(value) && value > 0) bytes = value; } catch {}
15
+ if (bytes !== null) { peakMemoryBytes = Math.max(peakMemoryBytes ?? 0, bytes); memorySamples++; }
16
+ try { onSample(bytes); } catch {}
17
+ })().finally(() => { pending = undefined; if (!stopped) { timer = setTimeout(take, intervalMs); timer.unref(); } });
18
+ return pending;
19
+ };
20
+ void take();
21
+ return {
22
+ sample: take,
23
+ setWorkers(value) { workers = value; },
24
+ snapshot: () => ({ peakMemoryBytes, memorySamples, workers, cpuCount: cpus().length || null }),
25
+ async stop() { stopped = true; clearTimeout(timer); await pending; await reader?.close(); },
26
+ };
27
+ }
@@ -1,37 +1,37 @@
1
- (function () {
2
- const E = Shot.ease;
3
-
4
- function interp(a, b, k) {
5
- const s = a.s * Math.pow(b.s / a.s, k);
6
- if (b.s >= a.s) {
7
- const px = (b.f[0] - a.f[0]) * a.s * (1 - k);
8
- const py = (b.f[1] - a.f[1]) * a.s * (1 - k);
9
- return { f: [b.f[0] - px / s, b.f[1] - py / s], s };
10
- }
11
- const px = (a.f[0] - b.f[0]) * b.s * k;
12
- const py = (a.f[1] - b.f[1]) * b.s * k;
13
- return { f: [a.f[0] - px / s, a.f[1] - py / s], s };
14
- }
15
-
16
- function track(keys) {
17
- function at(t) {
18
- if (t <= keys[0].t) return { f: keys[0].f, s: keys[0].s };
19
- for (let i = 1; i < keys.length; i++) {
20
- const a = keys[i - 1], b = keys[i];
21
- if (b.t === a.t) continue;
22
- if (t < b.t) return interp(a, b, (b.ease || E.inOutCubic)((t - a.t) / (b.t - a.t)));
23
- }
24
- const z = keys[keys.length - 1];
25
- return { f: z.f, s: z.s };
26
- }
27
- return {
28
- at,
29
- apply(node, t, w = Shot.size.w, h = Shot.size.h) {
30
- const { f, s } = at(t);
31
- node.setAttribute('transform', `translate(${w / 2} ${h / 2}) scale(${s}) translate(${-f[0]} ${-f[1]})`);
32
- },
33
- };
34
- }
35
-
36
- window.Camera = { track, linear: k => k };
37
- })();
1
+ (function () {
2
+ const E = Shot.ease;
3
+
4
+ function interp(a, b, k) {
5
+ const s = a.s * Math.pow(b.s / a.s, k);
6
+ if (b.s >= a.s) {
7
+ const px = (b.f[0] - a.f[0]) * a.s * (1 - k);
8
+ const py = (b.f[1] - a.f[1]) * a.s * (1 - k);
9
+ return { f: [b.f[0] - px / s, b.f[1] - py / s], s };
10
+ }
11
+ const px = (a.f[0] - b.f[0]) * b.s * k;
12
+ const py = (a.f[1] - b.f[1]) * b.s * k;
13
+ return { f: [a.f[0] - px / s, a.f[1] - py / s], s };
14
+ }
15
+
16
+ function track(keys) {
17
+ function at(t) {
18
+ if (t <= keys[0].t) return { f: keys[0].f, s: keys[0].s };
19
+ for (let i = 1; i < keys.length; i++) {
20
+ const a = keys[i - 1], b = keys[i];
21
+ if (b.t === a.t) continue;
22
+ if (t < b.t) return interp(a, b, (b.ease || E.inOutCubic)((t - a.t) / (b.t - a.t)));
23
+ }
24
+ const z = keys[keys.length - 1];
25
+ return { f: z.f, s: z.s };
26
+ }
27
+ return {
28
+ at,
29
+ apply(node, t, w = Shot.size.w, h = Shot.size.h) {
30
+ const { f, s } = at(t);
31
+ node.setAttribute('transform', `translate(${w / 2} ${h / 2}) scale(${s}) translate(${-f[0]} ${-f[1]})`);
32
+ },
33
+ };
34
+ }
35
+
36
+ window.Camera = { track, linear: k => k };
37
+ })();
@@ -1,9 +1,9 @@
1
- html, body { margin: 0; height: 100%; overflow: hidden; background: var(--multiplane-page-background, #000); }
2
- #stage {
3
- position: absolute; left: 0; top: 0;
4
- width: 1920px; height: 1080px;
5
- transform-origin: 0 0;
6
- overflow: hidden;
7
- background: var(--paper, #fff);
8
- }
9
- #stage > svg { display: block; }
1
+ html, body { margin: 0; height: 100%; overflow: hidden; background: var(--multiplane-page-background, #000); }
2
+ #stage {
3
+ position: absolute; left: 0; top: 0;
4
+ width: 1920px; height: 1080px;
5
+ transform-origin: 0 0;
6
+ overflow: hidden;
7
+ background: var(--paper, #fff);
8
+ }
9
+ #stage > svg { display: block; }
package/src/server.mjs CHANGED
@@ -61,13 +61,13 @@ export async function startServer(port = 0, root = process.cwd()) {
61
61
  }
62
62
  let target = path.resolve(resourceRoot, `.${resourcePath}`);
63
63
  if (!await isInsideResourceRoot(resourceRoot, target)) { res.writeHead(403); return res.end(); }
64
- let targetInfo;
65
- try { targetInfo = await stat(target); }
66
- catch (error) {
67
- if (pathname === '/favicon.ico' && error.code === 'ENOENT') { res.writeHead(204); return res.end(); }
68
- throw error;
69
- }
70
- if (targetInfo.isDirectory()) target = path.join(target, 'index.html');
64
+ let targetInfo;
65
+ try { targetInfo = await stat(target); }
66
+ catch (error) {
67
+ if (pathname === '/favicon.ico' && error.code === 'ENOENT') { res.writeHead(204); return res.end(); }
68
+ throw error;
69
+ }
70
+ if (targetInfo.isDirectory()) target = path.join(target, 'index.html');
71
71
  target = await normalizeResourcePath(target);
72
72
  if (!await isInsideResourceRoot(resourceBase, target)) { res.writeHead(403); return res.end(); }
73
73
  const info = await stat(target);
package/src/setup.mjs CHANGED
@@ -1,51 +1,51 @@
1
- import { fileURLToPath } from 'node:url';
2
- import { Configuration } from './config.mjs';
3
- import { Skills } from './skills.mjs';
4
- import { createEvents, version } from './events.mjs';
5
- import { asError, invalidOption, MultiplaneError } from './errors.mjs';
6
-
7
- export async function setup(command, args, onEvent) {
8
- const events = createEvents(command, null, { onEvent });
9
- events.start();
10
- try {
11
- const positional = [], options = {};
12
- for (let i = 0; i < args.length; i++) {
13
- const token = args[i];
14
- if (!token.startsWith('--')) { positional.push(token); continue; }
15
- const key = token.slice(2);
16
- if (!['json', ...(command === 'skill' ? ['host', 'force', 'expect'] : [])].includes(key) || key in options) throw invalidOption(token, 'Invalid or duplicate option.');
17
- if (['json', 'force'].includes(key)) options[key] = true;
18
- else { if (args[i + 1] === undefined || args[i + 1].startsWith('--')) throw invalidOption(token, 'Missing option value.'); options[key] = args[++i]; }
19
- }
20
- const [action, key, value] = positional;
21
- let data;
22
- if (command === 'config') {
23
- if (!['get', 'set', 'unset'].includes(action) || (action === 'get' ? positional.length > 2 : positional.length !== (action === 'set' ? 3 : 2))) throw invalidOption('action', 'Use config get [key], set <key> <value>, or unset <key>.');
24
- const config = new Configuration(undefined, process.env, warning => events.warning(warning));
25
- data = action === 'get' ? await config.read(key) : await config.save(key, action === 'unset' ? null : value);
26
- if (data.check && !data.check.usable) events.warning({ code: 'ffmpeg_unusable', message: 'The saved FFmpeg path is unusable.', details: { reason: data.check.reason } });
27
- } else {
28
- if (!['status', 'install', 'remove'].includes(action) || positional.length !== 1 || (action !== 'status' && !options.host) || !['all', 'claude', 'codex'].includes(options.host ?? 'all') || (action === 'status' && ('force' in options || 'expect' in options))) throw invalidOption('host', 'Use skill status, install, or remove with --host claude|codex|all.');
29
- const skills = new Skills(fileURLToPath(new URL('../skills/multiplane/', import.meta.url)), version);
30
- const hosts = await skills.list(!options.host || options.host === 'all' ? ['claude', 'codex'] : [options.host]);
31
- if (action === 'status') data = { hosts };
32
- else {
33
- data = { hosts: [] }; let first;
34
- for (const status of hosts) {
35
- const item = { host: status.host, action, ok: true, code: null, message: null, target: status.target, version: action === 'install' ? version : null, backup: null };
36
- try {
37
- if (status.blocked) throw new MultiplaneError('skill_blocked', status.error);
38
- // TODO(design): Document --expect null for the missing-copy signature.
39
- const expected = options.expect === 'null' ? null : options.expect;
40
- Object.assign(item, await skills.change(status.host, action, expected, undefined, !!options.force));
41
- } catch (failure) { const error = asError(failure); first ??= error; Object.assign(item, { ok: false, code: error.code, message: error.message, version: status.version }); }
42
- data.hosts.push(item);
43
- }
44
- if (first) { first.result = { data }; throw first; }
45
- }
46
- }
47
- return events.finish(data);
48
- } catch (failure) {
49
- const error = asError(failure); error.result = events.finish(error.result?.data ?? {}, error); throw error;
50
- } finally { events.dispose(); }
51
- }
1
+ import { fileURLToPath } from 'node:url';
2
+ import { Configuration } from './config.mjs';
3
+ import { Skills } from './skills.mjs';
4
+ import { createEvents, version } from './events.mjs';
5
+ import { asError, invalidOption, MultiplaneError } from './errors.mjs';
6
+
7
+ export async function setup(command, args, onEvent) {
8
+ const events = createEvents(command, null, { onEvent });
9
+ events.start();
10
+ try {
11
+ const positional = [], options = {};
12
+ for (let i = 0; i < args.length; i++) {
13
+ const token = args[i];
14
+ if (!token.startsWith('--')) { positional.push(token); continue; }
15
+ const key = token.slice(2);
16
+ if (!['json', ...(command === 'skill' ? ['host', 'force', 'expect'] : [])].includes(key) || key in options) throw invalidOption(token, 'Invalid or duplicate option.');
17
+ if (['json', 'force'].includes(key)) options[key] = true;
18
+ else { if (args[i + 1] === undefined || args[i + 1].startsWith('--')) throw invalidOption(token, 'Missing option value.'); options[key] = args[++i]; }
19
+ }
20
+ const [action, key, value] = positional;
21
+ let data;
22
+ if (command === 'config') {
23
+ if (!['get', 'set', 'unset'].includes(action) || (action === 'get' ? positional.length > 2 : positional.length !== (action === 'set' ? 3 : 2))) throw invalidOption('action', 'Use config get [key], set <key> <value>, or unset <key>.');
24
+ const config = new Configuration(undefined, process.env, warning => events.warning(warning));
25
+ data = action === 'get' ? await config.read(key) : await config.save(key, action === 'unset' ? null : value);
26
+ if (data.check && !data.check.usable) events.warning({ code: 'ffmpeg_unusable', message: 'The saved FFmpeg path is unusable.', details: { reason: data.check.reason } });
27
+ } else {
28
+ if (!['status', 'install', 'remove'].includes(action) || positional.length !== 1 || (action !== 'status' && !options.host) || !['all', 'claude', 'codex'].includes(options.host ?? 'all') || (action === 'status' && ('force' in options || 'expect' in options))) throw invalidOption('host', 'Use skill status, install, or remove with --host claude|codex|all.');
29
+ const skills = new Skills(fileURLToPath(new URL('../skills/multiplane/', import.meta.url)), version);
30
+ const hosts = await skills.list(!options.host || options.host === 'all' ? ['claude', 'codex'] : [options.host]);
31
+ if (action === 'status') data = { hosts };
32
+ else {
33
+ data = { hosts: [] }; let first;
34
+ for (const status of hosts) {
35
+ const item = { host: status.host, action, ok: true, code: null, message: null, target: status.target, version: action === 'install' ? version : null, backup: null };
36
+ try {
37
+ if (status.blocked) throw new MultiplaneError('skill_blocked', status.error);
38
+ // TODO(design): Document --expect null for the missing-copy signature.
39
+ const expected = options.expect === 'null' ? null : options.expect;
40
+ Object.assign(item, await skills.change(status.host, action, expected, undefined, !!options.force));
41
+ } catch (failure) { const error = asError(failure); first ??= error; Object.assign(item, { ok: false, code: error.code, message: error.message, version: status.version }); }
42
+ data.hosts.push(item);
43
+ }
44
+ if (first) { first.result = { data }; throw first; }
45
+ }
46
+ }
47
+ return events.finish(data);
48
+ } catch (failure) {
49
+ const error = asError(failure); error.result = events.finish(error.result?.data ?? {}, error); throw error;
50
+ } finally { events.dispose(); }
51
+ }