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.
Files changed (72) hide show
  1. package/LICENSE +202 -202
  2. package/README.en.md +327 -326
  3. package/README.md +327 -326
  4. package/THIRD_PARTY_NOTICES.md +30 -30
  5. package/examples/alpha.html +18 -18
  6. package/examples/animation.html +17 -17
  7. package/examples/assets/OFL.txt +93 -93
  8. package/examples/assets/card.svg +6 -6
  9. package/examples/three-cards.html +17 -17
  10. package/examples/typography.html +17 -17
  11. package/licenses/GPL-3.0.txt +232 -232
  12. package/licenses/LGPL-3.0.txt +304 -304
  13. package/licenses/acorn-LICENSE +21 -21
  14. package/licenses/colour-LICENSE.md +82 -82
  15. package/licenses/detect-libc-LICENSE +201 -201
  16. package/licenses/entities-LICENSE +11 -11
  17. package/licenses/nanoid-LICENSE +20 -20
  18. package/licenses/parse5-LICENSE +19 -19
  19. package/licenses/picocolors-LICENSE +15 -15
  20. package/licenses/playwright-NOTICE +5 -5
  21. package/licenses/playwright-ThirdPartyNotices.txt +14 -14
  22. package/licenses/playwright-core-ThirdPartyNotices.txt +13 -13
  23. package/licenses/postcss-LICENSE +20 -20
  24. package/licenses/postcss-value-parser-LICENSE +22 -22
  25. package/licenses/semver-LICENSE +15 -15
  26. package/licenses/sharp-LICENSE +191 -191
  27. package/licenses/sharp-win32-x64-NOTICES.md +45 -45
  28. package/licenses/source-map-js-LICENSE +28 -28
  29. package/licenses/three-LICENSE +21 -21
  30. package/package.json +1 -1
  31. package/skills/multiplane/SKILL.md +215 -213
  32. package/src/alpha.mjs +69 -69
  33. package/src/artifacts.mjs +61 -61
  34. package/src/browser-recovery.mjs +26 -26
  35. package/src/capture.mjs +16 -6
  36. package/src/encoder.mjs +16 -16
  37. package/src/environment.mjs +56 -56
  38. package/src/events.mjs +3 -0
  39. package/src/ffmpeg.mjs +32 -32
  40. package/src/format.mjs +27 -27
  41. package/src/frame-rate.mjs +28 -28
  42. package/src/home-migration.mjs +54 -54
  43. package/src/hosted.mjs +1 -1
  44. package/src/lint.mjs +248 -248
  45. package/src/output-alpha.mjs +36 -36
  46. package/src/output-check.mjs +110 -110
  47. package/src/platform.mjs +114 -114
  48. package/src/png-native.mjs +12 -12
  49. package/src/png.mjs +74 -74
  50. package/src/post.mjs +55 -21
  51. package/src/preview.mjs +23 -23
  52. package/src/process.mjs +9 -9
  53. package/src/render-pool.mjs +16 -6
  54. package/src/render-worker.mjs +5 -3
  55. package/src/render.mjs +5 -5
  56. package/src/resource-path.mjs +29 -29
  57. package/src/resources.mjs +27 -27
  58. package/src/runtime/camera.js +37 -37
  59. package/src/runtime/shot.css +9 -9
  60. package/src/server.mjs +7 -1
  61. package/src/setup.mjs +51 -51
  62. package/src/skills.mjs +128 -128
  63. package/src/stills.mjs +5 -4
  64. package/src/three/index.js +44 -14
  65. package/src/verify.mjs +3 -1
  66. package/src/video-pipe.mjs +6 -6
  67. package/src/workers.mjs +5 -1
  68. package/tools/bench/shots/canvas-2d.html +1 -1
  69. package/tools/bench/shots/dom-light.html +1 -1
  70. package/tools/bench/shots/scene.js +44 -44
  71. package/tools/bench/shots/webgl.html +1 -1
  72. 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
+ }
@@ -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
+ }
@@ -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;