multiplane 0.3.3 → 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/README.en.md +327 -326
- package/README.md +327 -326
- 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 +1 -1
- package/skills/multiplane/SKILL.md +215 -213
- package/src/alpha.mjs +69 -69
- package/src/artifacts.mjs +61 -61
- package/src/browser-recovery.mjs +26 -26
- package/src/capture.mjs +16 -6
- package/src/encoder.mjs +16 -16
- package/src/environment.mjs +56 -56
- package/src/events.mjs +3 -0
- 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 +55 -21
- package/src/preview.mjs +23 -23
- package/src/process.mjs +9 -9
- package/src/render-pool.mjs +16 -6
- package/src/render-worker.mjs +5 -3
- package/src/render.mjs +5 -5
- 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 -1
- package/src/setup.mjs +51 -51
- package/src/skills.mjs +128 -128
- package/src/stills.mjs +5 -4
- package/src/three/index.js +44 -14
- package/src/verify.mjs +3 -1
- package/src/video-pipe.mjs +6 -6
- package/src/workers.mjs +5 -1
- 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/format.mjs
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { MultiplaneError, invalidOption } from './errors.mjs';
|
|
3
|
-
import { encoderArgs } from './encoder.mjs';
|
|
4
|
-
|
|
5
|
-
export function resolveFormat(options = {}) {
|
|
6
|
-
const extension = path.extname(options.out ?? '').slice(1).toLowerCase();
|
|
7
|
-
const formats = ['mp4', 'webm', 'mov'];
|
|
8
|
-
const format = options.format ?? (formats.includes(extension) ? extension : 'mp4');
|
|
9
|
-
if (!formats.includes(format)) throw invalidOption('--format', '--format must be mp4, webm or mov.');
|
|
10
|
-
// TODO(design): Document rejection of unknown output extensions as well as conflicts.
|
|
11
|
-
if (extension && extension !== format) throw invalidOption('--out', '--out extension must match --format.');
|
|
12
|
-
if (format !== 'mp4' && options.encoder !== undefined) throw invalidOption('--encoder', '--encoder only applies to mp4; omit it for webm or mov.');
|
|
13
|
-
// TODO(design): Define bitrate controls for the fixed-codec formats if needed.
|
|
14
|
-
if (format !== 'mp4' && options['max-bitrate'] !== undefined) throw invalidOption('--max-bitrate', '--max-bitrate currently only applies to mp4.');
|
|
15
|
-
if (format === 'mp4' && options.alpha) throw new MultiplaneError('invalid_option', 'MP4 does not support alpha.', { details: { option: '--alpha' }, hint: 'Use --format webm or --format mov and a matching --out extension.' });
|
|
16
|
-
return format;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function videoEncoding(options) {
|
|
20
|
-
const { format, alpha } = options;
|
|
21
|
-
if (format === 'webm') {
|
|
22
|
-
// TODO(design): Document the fixed VP9 quality preset and audio codec.
|
|
23
|
-
return { encoder: 'libvpx-vp9', args: ['-c:v', 'libvpx-vp9', '-crf', alpha ? '0' : '16', '-b:v', '0', '-auto-alt-ref', '0', ...(alpha ? ['-lossless', '1'] : [])], pixelFormat: alpha ? 'yuva420p' : 'yuv420p', mux: [], audio: ['-c:a', 'libopus', '-b:a', '320k', '-ac', '2'] };
|
|
24
|
-
}
|
|
25
|
-
if (format === 'mov') return { encoder: 'prores_ks', args: ['-c:v', 'prores_ks', '-profile:v', '4'], pixelFormat: alpha ? 'yuva444p10le' : 'yuv444p10le', mux: ['-movflags', '+faststart'], audio: ['-c:a', 'aac', '-b:a', '320k', '-ac', '2'] };
|
|
26
|
-
return { encoder: options.encoder ?? 'x264', args: encoderArgs(options.encoder ?? 'x264', undefined, options['max-bitrate']), pixelFormat: 'yuv420p', mux: ['-movflags', '+faststart'], audio: ['-c:a', 'aac', '-b:a', '320k', '-ac', '2'] };
|
|
27
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { MultiplaneError, invalidOption } from './errors.mjs';
|
|
3
|
+
import { encoderArgs } from './encoder.mjs';
|
|
4
|
+
|
|
5
|
+
export function resolveFormat(options = {}) {
|
|
6
|
+
const extension = path.extname(options.out ?? '').slice(1).toLowerCase();
|
|
7
|
+
const formats = ['mp4', 'webm', 'mov'];
|
|
8
|
+
const format = options.format ?? (formats.includes(extension) ? extension : 'mp4');
|
|
9
|
+
if (!formats.includes(format)) throw invalidOption('--format', '--format must be mp4, webm or mov.');
|
|
10
|
+
// TODO(design): Document rejection of unknown output extensions as well as conflicts.
|
|
11
|
+
if (extension && extension !== format) throw invalidOption('--out', '--out extension must match --format.');
|
|
12
|
+
if (format !== 'mp4' && options.encoder !== undefined) throw invalidOption('--encoder', '--encoder only applies to mp4; omit it for webm or mov.');
|
|
13
|
+
// TODO(design): Define bitrate controls for the fixed-codec formats if needed.
|
|
14
|
+
if (format !== 'mp4' && options['max-bitrate'] !== undefined) throw invalidOption('--max-bitrate', '--max-bitrate currently only applies to mp4.');
|
|
15
|
+
if (format === 'mp4' && options.alpha) throw new MultiplaneError('invalid_option', 'MP4 does not support alpha.', { details: { option: '--alpha' }, hint: 'Use --format webm or --format mov and a matching --out extension.' });
|
|
16
|
+
return format;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function videoEncoding(options) {
|
|
20
|
+
const { format, alpha } = options;
|
|
21
|
+
if (format === 'webm') {
|
|
22
|
+
// TODO(design): Document the fixed VP9 quality preset and audio codec.
|
|
23
|
+
return { encoder: 'libvpx-vp9', args: ['-c:v', 'libvpx-vp9', '-crf', alpha ? '0' : '16', '-b:v', '0', '-auto-alt-ref', '0', ...(alpha ? ['-lossless', '1'] : [])], pixelFormat: alpha ? 'yuva420p' : 'yuv420p', mux: [], audio: ['-c:a', 'libopus', '-b:a', '320k', '-ac', '2'] };
|
|
24
|
+
}
|
|
25
|
+
if (format === 'mov') return { encoder: 'prores_ks', args: ['-c:v', 'prores_ks', '-profile:v', '4'], pixelFormat: alpha ? 'yuva444p10le' : 'yuv444p10le', mux: ['-movflags', '+faststart'], audio: ['-c:a', 'aac', '-b:a', '320k', '-ac', '2'] };
|
|
26
|
+
return { encoder: options.encoder ?? 'x264', args: encoderArgs(options.encoder ?? 'x264', undefined, options['max-bitrate']), pixelFormat: 'yuv420p', mux: ['-movflags', '+faststart'], audio: ['-c:a', 'aac', '-b:a', '320k', '-ac', '2'] };
|
|
27
|
+
}
|
package/src/frame-rate.mjs
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { invalidOption } from './errors.mjs';
|
|
2
|
-
|
|
3
|
-
// Encoding time only: capture and post-processing must keep the requested fps.
|
|
4
|
-
// Snap NTSC shorthand within 0.0001 fps of n * 1000 / 1001. Otherwise round
|
|
5
|
-
// to six decimal places (at most 0.0000005 fps error) and reduce the fraction.
|
|
6
|
-
// FFmpeg's AVRational stores both terms as signed 32-bit integers.
|
|
7
|
-
export function frameRate(fps) {
|
|
8
|
-
if (!Number.isFinite(fps) || fps <= 0) throw invalidOption('--fps', '--fps must be positive and finite.');
|
|
9
|
-
const nominal = Math.round(fps * 1001 / 1000);
|
|
10
|
-
let numerator, denominator;
|
|
11
|
-
if (nominal > 0 && Math.abs(fps - nominal * 1000 / 1001) <= 0.0001) {
|
|
12
|
-
numerator = nominal * 1000;
|
|
13
|
-
denominator = 1001;
|
|
14
|
-
} else {
|
|
15
|
-
numerator = Math.round(fps * 1e6);
|
|
16
|
-
denominator = 1e6;
|
|
17
|
-
}
|
|
18
|
-
if (!Number.isSafeInteger(numerator) || numerator <= 0) throw invalidOption('--fps', 'Frame rate cannot be represented at micro-fps precision.');
|
|
19
|
-
let a = numerator, b = denominator;
|
|
20
|
-
while (b) [a, b] = [b, a % b];
|
|
21
|
-
numerator /= a; denominator /= a;
|
|
22
|
-
if (numerator > 2147483647) throw invalidOption('--fps', 'Frame rate exceeds FFmpeg rational precision.');
|
|
23
|
-
return {
|
|
24
|
-
numerator, denominator, argument: `${numerator}/${denominator}`,
|
|
25
|
-
fps: numerator / denominator,
|
|
26
|
-
seconds: frames => frames * denominator / numerator,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
1
|
+
import { invalidOption } from './errors.mjs';
|
|
2
|
+
|
|
3
|
+
// Encoding time only: capture and post-processing must keep the requested fps.
|
|
4
|
+
// Snap NTSC shorthand within 0.0001 fps of n * 1000 / 1001. Otherwise round
|
|
5
|
+
// to six decimal places (at most 0.0000005 fps error) and reduce the fraction.
|
|
6
|
+
// FFmpeg's AVRational stores both terms as signed 32-bit integers.
|
|
7
|
+
export function frameRate(fps) {
|
|
8
|
+
if (!Number.isFinite(fps) || fps <= 0) throw invalidOption('--fps', '--fps must be positive and finite.');
|
|
9
|
+
const nominal = Math.round(fps * 1001 / 1000);
|
|
10
|
+
let numerator, denominator;
|
|
11
|
+
if (nominal > 0 && Math.abs(fps - nominal * 1000 / 1001) <= 0.0001) {
|
|
12
|
+
numerator = nominal * 1000;
|
|
13
|
+
denominator = 1001;
|
|
14
|
+
} else {
|
|
15
|
+
numerator = Math.round(fps * 1e6);
|
|
16
|
+
denominator = 1e6;
|
|
17
|
+
}
|
|
18
|
+
if (!Number.isSafeInteger(numerator) || numerator <= 0) throw invalidOption('--fps', 'Frame rate cannot be represented at micro-fps precision.');
|
|
19
|
+
let a = numerator, b = denominator;
|
|
20
|
+
while (b) [a, b] = [b, a % b];
|
|
21
|
+
numerator /= a; denominator /= a;
|
|
22
|
+
if (numerator > 2147483647) throw invalidOption('--fps', 'Frame rate exceeds FFmpeg rational precision.');
|
|
23
|
+
return {
|
|
24
|
+
numerator, denominator, argument: `${numerator}/${denominator}`,
|
|
25
|
+
fps: numerator / denominator,
|
|
26
|
+
seconds: frames => frames * denominator / numerator,
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/home-migration.mjs
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { lstatSync, mkdirSync, openSync, closeSync, copyFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
5
|
-
import { userHome } from './platform.mjs';
|
|
6
|
-
|
|
7
|
-
const present = file => {
|
|
8
|
-
try { lstatSync(file); return true; }
|
|
9
|
-
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// Use the writers' locks, including the config lock introduced in 038.
|
|
13
|
-
// Publish only complete copies, and recheck the destination under the lock.
|
|
14
|
-
export function migrateUserHome(env = process.env, platform = process.platform, home = os.homedir()) {
|
|
15
|
-
const result = { migrated: [], warnings: [] };
|
|
16
|
-
if (platform !== 'win32' || env.MULTIPLANE_HOME) return result;
|
|
17
|
-
const directory = userHome(env, platform, home);
|
|
18
|
-
const previous = path.win32.join(env.APPDATA || path.win32.join(env.USERPROFILE || home, 'AppData', 'Roaming'), 'multiplane');
|
|
19
|
-
if (path.win32.resolve(previous).toLowerCase() === path.win32.resolve(directory).toLowerCase()) return result;
|
|
20
|
-
for (const [lockName, names] of [['config.json.lock', ['config.json']], ['jobs.lock', ['jobs.jsonl', 'jobs.1.jsonl']]]) {
|
|
21
|
-
let handle;
|
|
22
|
-
const lock = path.join(directory, lockName);
|
|
23
|
-
try {
|
|
24
|
-
if (!names.some(name => !present(path.join(directory, name)) && present(path.join(previous, name)))) continue;
|
|
25
|
-
mkdirSync(directory, { recursive: true });
|
|
26
|
-
const deadline = performance.now() + 10000;
|
|
27
|
-
for (;;) {
|
|
28
|
-
try { handle = openSync(lock, 'wx'); break; }
|
|
29
|
-
catch (error) {
|
|
30
|
-
if (error.code !== 'EEXIST' || performance.now() >= deadline) throw error;
|
|
31
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
for (const name of names) {
|
|
35
|
-
const target = path.join(directory, name), source = path.join(previous, name);
|
|
36
|
-
if (present(target) || !present(source)) continue;
|
|
37
|
-
const temporary = target + '.' + randomUUID() + '.tmp';
|
|
38
|
-
try {
|
|
39
|
-
copyFileSync(source, temporary);
|
|
40
|
-
renameSync(temporary, target);
|
|
41
|
-
result.migrated.push({ name, source });
|
|
42
|
-
} finally { try { unlinkSync(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
result.warnings.push({ code: 'home_unavailable', message: 'User directory migration could not be completed.' });
|
|
46
|
-
} finally {
|
|
47
|
-
if (handle !== undefined) {
|
|
48
|
-
closeSync(handle);
|
|
49
|
-
try { unlinkSync(lock); } catch { result.warnings.push({ code: 'home_unavailable', message: 'User directory migration lock could not be removed.' }); }
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { lstatSync, mkdirSync, openSync, closeSync, copyFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
5
|
+
import { userHome } from './platform.mjs';
|
|
6
|
+
|
|
7
|
+
const present = file => {
|
|
8
|
+
try { lstatSync(file); return true; }
|
|
9
|
+
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Use the writers' locks, including the config lock introduced in 038.
|
|
13
|
+
// Publish only complete copies, and recheck the destination under the lock.
|
|
14
|
+
export function migrateUserHome(env = process.env, platform = process.platform, home = os.homedir()) {
|
|
15
|
+
const result = { migrated: [], warnings: [] };
|
|
16
|
+
if (platform !== 'win32' || env.MULTIPLANE_HOME) return result;
|
|
17
|
+
const directory = userHome(env, platform, home);
|
|
18
|
+
const previous = path.win32.join(env.APPDATA || path.win32.join(env.USERPROFILE || home, 'AppData', 'Roaming'), 'multiplane');
|
|
19
|
+
if (path.win32.resolve(previous).toLowerCase() === path.win32.resolve(directory).toLowerCase()) return result;
|
|
20
|
+
for (const [lockName, names] of [['config.json.lock', ['config.json']], ['jobs.lock', ['jobs.jsonl', 'jobs.1.jsonl']]]) {
|
|
21
|
+
let handle;
|
|
22
|
+
const lock = path.join(directory, lockName);
|
|
23
|
+
try {
|
|
24
|
+
if (!names.some(name => !present(path.join(directory, name)) && present(path.join(previous, name)))) continue;
|
|
25
|
+
mkdirSync(directory, { recursive: true });
|
|
26
|
+
const deadline = performance.now() + 10000;
|
|
27
|
+
for (;;) {
|
|
28
|
+
try { handle = openSync(lock, 'wx'); break; }
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== 'EEXIST' || performance.now() >= deadline) throw error;
|
|
31
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const name of names) {
|
|
35
|
+
const target = path.join(directory, name), source = path.join(previous, name);
|
|
36
|
+
if (present(target) || !present(source)) continue;
|
|
37
|
+
const temporary = target + '.' + randomUUID() + '.tmp';
|
|
38
|
+
try {
|
|
39
|
+
copyFileSync(source, temporary);
|
|
40
|
+
renameSync(temporary, target);
|
|
41
|
+
result.migrated.push({ name, source });
|
|
42
|
+
} finally { try { unlinkSync(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
result.warnings.push({ code: 'home_unavailable', message: 'User directory migration could not be completed.' });
|
|
46
|
+
} finally {
|
|
47
|
+
if (handle !== undefined) {
|
|
48
|
+
closeSync(handle);
|
|
49
|
+
try { unlinkSync(lock); } catch { result.warnings.push({ code: 'home_unavailable', message: 'User directory migration lock could not be removed.' }); }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
package/src/hosted.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export async function hostedAvailable(command, options, env = process.env) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function hostedEnvironment(env) {
|
|
31
|
-
const allowed = /^(PATH|MULTIPLANE_.*|HTTPS?_PROXY|ALL_PROXY|NO_PROXY|TEMP|TMP|TMPDIR|HOME|USERPROFILE|APPDATA|LOCALAPPDATA|SystemRoot|ComSpec|PATHEXT|LANG|LC_.*)$/i;
|
|
31
|
+
const allowed = /^(PATH|MULTIPLANE_.*|ELECTRON_RUN_AS_NODE|HTTPS?_PROXY|ALL_PROXY|NO_PROXY|TEMP|TMP|TMPDIR|HOME|USERPROFILE|APPDATA|LOCALAPPDATA|SystemRoot|ComSpec|PATHEXT|LANG|LC_.*)$/i;
|
|
32
32
|
const secret = /TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH|BEARER|COOKIE|SESSION|API_?KEY|PRIVATE_?KEY|ACCESS_?KEY/i;
|
|
33
33
|
return Object.fromEntries(Object.entries(env).filter(([key, value]) => {
|
|
34
34
|
if (value === undefined || !allowed.test(key) || secret.test(key)) return false;
|