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.
- package/LICENSE +202 -202
- package/THIRD_PARTY_NOTICES.md +30 -30
- package/examples/alpha.html +18 -18
- package/examples/animation.html +17 -17
- package/examples/assets/OFL.txt +93 -93
- package/examples/assets/card.svg +6 -6
- package/examples/three-cards.html +17 -17
- package/examples/typography.html +17 -17
- package/licenses/GPL-3.0.txt +232 -232
- package/licenses/LGPL-3.0.txt +304 -304
- package/licenses/acorn-LICENSE +21 -21
- package/licenses/colour-LICENSE.md +82 -82
- package/licenses/detect-libc-LICENSE +201 -201
- package/licenses/entities-LICENSE +11 -11
- package/licenses/nanoid-LICENSE +20 -20
- package/licenses/parse5-LICENSE +19 -19
- package/licenses/picocolors-LICENSE +15 -15
- package/licenses/playwright-NOTICE +5 -5
- package/licenses/playwright-ThirdPartyNotices.txt +14 -14
- package/licenses/playwright-core-ThirdPartyNotices.txt +13 -13
- package/licenses/postcss-LICENSE +20 -20
- package/licenses/postcss-value-parser-LICENSE +22 -22
- package/licenses/semver-LICENSE +15 -15
- package/licenses/sharp-LICENSE +191 -191
- package/licenses/sharp-win32-x64-NOTICES.md +45 -45
- package/licenses/source-map-js-LICENSE +28 -28
- package/licenses/three-LICENSE +21 -21
- package/package.json +92 -92
- package/src/alpha.mjs +69 -69
- package/src/artifacts.mjs +61 -61
- package/src/browser-recovery.mjs +26 -26
- package/src/capture.mjs +12 -12
- package/src/encoder.mjs +16 -16
- package/src/environment.mjs +56 -56
- package/src/events.mjs +5 -5
- package/src/ffmpeg.mjs +32 -32
- package/src/format.mjs +27 -27
- package/src/frame-rate.mjs +28 -28
- package/src/home-migration.mjs +54 -54
- package/src/hosted.mjs +1 -1
- package/src/lint.mjs +248 -248
- package/src/output-alpha.mjs +36 -36
- package/src/output-check.mjs +110 -110
- package/src/platform.mjs +114 -114
- package/src/png-native.mjs +12 -12
- package/src/png.mjs +74 -74
- package/src/post.mjs +38 -37
- package/src/preview.mjs +23 -23
- package/src/process.mjs +9 -9
- package/src/render-pool.mjs +2 -2
- package/src/render-worker.mjs +1 -1
- package/src/resource-path.mjs +29 -29
- package/src/resources.mjs +27 -27
- package/src/runtime/camera.js +37 -37
- package/src/runtime/shot.css +9 -9
- package/src/server.mjs +7 -7
- package/src/setup.mjs +51 -51
- package/src/skills.mjs +128 -128
- package/src/stills.mjs +1 -1
- package/src/three/index.js +39 -31
- package/src/verify.mjs +2 -2
- package/src/video-pipe.mjs +6 -6
- package/tools/bench/shots/canvas-2d.html +1 -1
- package/tools/bench/shots/dom-light.html +1 -1
- package/tools/bench/shots/scene.js +44 -44
- package/tools/bench/shots/webgl.html +1 -1
- 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
|
+
}
|
package/src/render-pool.mjs
CHANGED
|
@@ -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 };
|
package/src/render-worker.mjs
CHANGED
|
@@ -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
|
}
|
package/src/resource-path.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/runtime/camera.js
CHANGED
|
@@ -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
|
+
})();
|
package/src/runtime/shot.css
CHANGED
|
@@ -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
|
+
}
|