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/package.json CHANGED
@@ -1,92 +1,92 @@
1
- {
2
- "name": "multiplane",
3
- "version": "0.3.6",
4
- "type": "module",
5
- "license": "Apache-2.0",
6
- "publishConfig": {
7
- "registry": "https://registry.npmjs.org/",
8
- "access": "public"
9
- },
10
- "engines": {
11
- "node": ">=24"
12
- },
13
- "exports": {
14
- ".": "./src/index.mjs",
15
- "./three": "./src/three/index.js",
16
- "./runtime/*": "./src/runtime/*"
17
- },
18
- "bin": {
19
- "multiplane": "bin/multiplane"
20
- },
21
- "files": [
22
- "src/",
23
- "bin/",
24
- "examples/",
25
- "skills/",
26
- "tools/install-browser.mjs",
27
- "tools/bench/shots/dom-light.html",
28
- "tools/bench/shots/canvas-2d.html",
29
- "tools/bench/shots/webgl.html",
30
- "tools/bench/shots/scene.js",
31
- "LICENSE",
32
- "THIRD_PARTY_NOTICES.md",
33
- "licenses/",
34
- "README.md",
35
- "README.en.md"
36
- ],
37
- "scripts": {
38
- "postinstall": "node tools/install-browser.mjs",
39
- "test": "node --import ./tests/test-home.mjs tools/test-suite.mjs",
40
- "export:public": "node tools/export-public.mjs",
41
- "release:check": "node tools/release-check.mjs",
42
- "bench": "node tools/bench/run.mjs",
43
- "test:full": "node --import ./tests/test-home.mjs tools/test-suite.mjs --full"
44
- },
45
- "dependencies": {
46
- "acorn": "8.15.0",
47
- "parse5": "7.3.0",
48
- "playwright": "1.63.0",
49
- "postcss": "8.5.28",
50
- "postcss-value-parser": "4.2.0",
51
- "sharp": "0.35.5"
52
- },
53
- "peerDependencies": {
54
- "three": "0.180.0"
55
- },
56
- "peerDependenciesMeta": {
57
- "three": {
58
- "optional": true
59
- }
60
- },
61
- "devDependencies": {
62
- "three": "0.180.0"
63
- },
64
- "overrides": {
65
- "postcss": {
66
- "nanoid": "3.3.19"
67
- }
68
- },
69
- "description": "Local headless HTML-to-video renderer for AI agents: shots are plain web pages driven by render(t), with determinism checks, cut-aware motion blur and a 3D layer with accumulated depth of field.",
70
- "keywords": [
71
- "video",
72
- "renderer",
73
- "html",
74
- "headless",
75
- "chromium",
76
- "playwright",
77
- "ffmpeg",
78
- "ai-agent",
79
- "deterministic",
80
- "motion-blur",
81
- "depth-of-field",
82
- "three.js"
83
- ],
84
- "repository": {
85
- "type": "git",
86
- "url": "git+https://github.com/LBEILC/Multiplane.git"
87
- },
88
- "homepage": "https://github.com/LBEILC/Multiplane#readme",
89
- "bugs": {
90
- "url": "https://github.com/LBEILC/Multiplane/issues"
91
- }
92
- }
1
+ {
2
+ "name": "multiplane",
3
+ "version": "0.3.7",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "publishConfig": {
7
+ "registry": "https://registry.npmjs.org/",
8
+ "access": "public"
9
+ },
10
+ "engines": {
11
+ "node": ">=24"
12
+ },
13
+ "exports": {
14
+ ".": "./src/index.mjs",
15
+ "./three": "./src/three/index.js",
16
+ "./runtime/*": "./src/runtime/*"
17
+ },
18
+ "bin": {
19
+ "multiplane": "bin/multiplane"
20
+ },
21
+ "files": [
22
+ "src/",
23
+ "bin/",
24
+ "examples/",
25
+ "skills/",
26
+ "tools/install-browser.mjs",
27
+ "tools/bench/shots/dom-light.html",
28
+ "tools/bench/shots/canvas-2d.html",
29
+ "tools/bench/shots/webgl.html",
30
+ "tools/bench/shots/scene.js",
31
+ "LICENSE",
32
+ "THIRD_PARTY_NOTICES.md",
33
+ "licenses/",
34
+ "README.md",
35
+ "README.en.md"
36
+ ],
37
+ "scripts": {
38
+ "postinstall": "node tools/install-browser.mjs",
39
+ "test": "node --import ./tests/test-home.mjs tools/test-suite.mjs",
40
+ "export:public": "node tools/export-public.mjs",
41
+ "release:check": "node tools/release-check.mjs",
42
+ "bench": "node tools/bench/run.mjs",
43
+ "test:full": "node --import ./tests/test-home.mjs tools/test-suite.mjs --full"
44
+ },
45
+ "dependencies": {
46
+ "acorn": "8.15.0",
47
+ "parse5": "7.3.0",
48
+ "playwright": "1.63.0",
49
+ "postcss": "8.5.28",
50
+ "postcss-value-parser": "4.2.0",
51
+ "sharp": "0.35.5"
52
+ },
53
+ "peerDependencies": {
54
+ "three": "0.180.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "three": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "three": "0.180.0"
63
+ },
64
+ "overrides": {
65
+ "postcss": {
66
+ "nanoid": "3.3.19"
67
+ }
68
+ },
69
+ "description": "Local headless HTML-to-video renderer for AI agents: shots are plain web pages driven by render(t), with determinism checks, cut-aware motion blur and a 3D layer with accumulated depth of field.",
70
+ "keywords": [
71
+ "video",
72
+ "renderer",
73
+ "html",
74
+ "headless",
75
+ "chromium",
76
+ "playwright",
77
+ "ffmpeg",
78
+ "ai-agent",
79
+ "deterministic",
80
+ "motion-blur",
81
+ "depth-of-field",
82
+ "three.js"
83
+ ],
84
+ "repository": {
85
+ "type": "git",
86
+ "url": "git+https://github.com/LBEILC/Multiplane.git"
87
+ },
88
+ "homepage": "https://github.com/LBEILC/Multiplane#readme",
89
+ "bugs": {
90
+ "url": "https://github.com/LBEILC/Multiplane/issues"
91
+ }
92
+ }
package/src/alpha.mjs CHANGED
@@ -1,69 +1,69 @@
1
- // All weighted operations use linear-light, premultiplied RGBA.
2
- const linear = Float64Array.from({ length: 256 }, (_, i) => {
3
- const x = i / 255;
4
- return x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
5
- });
6
- const clamp = x => Math.max(0, Math.min(1, x));
7
- const srgb = x => Math.round(255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055));
8
- function premultiply(image) {
9
- const data = new Float64Array(image.data.length);
10
- for (let p = 0; p < data.length; p += 4) {
11
- const a = image.data[p + 3] / 255;
12
- for (let c = 0; c < 3; c++) data[p + c] = linear[image.data[p + c]] * a;
13
- data[p + 3] = a;
14
- }
15
- return data;
16
- }
17
- function finish(data, width, height) {
18
- const bytes = Buffer.alloc(data.length);
19
- for (let p = 0; p < data.length; p += 4) {
20
- const a = data[p + 3];
21
- bytes[p + 3] = Math.round(clamp(a) * 255);
22
- // Unpremultiply with the unquantized, unclamped weight, including ringing.
23
- if (bytes[p + 3]) for (let c = 0; c < 3; c++) bytes[p + c] = srgb(clamp(data[p + c] / a));
24
- }
25
- return { width, height, data: bytes };
26
- }
27
- export function averageAlpha(images) {
28
- const data = new Float64Array(images[0].data.length);
29
- for (const image of images) {
30
- for (let p = 0; p < data.length; p += 4) {
31
- const a = image.data[p + 3] / 255;
32
- for (let c = 0; c < 3; c++) data[p + c] += linear[image.data[p + c]] * a / images.length;
33
- data[p + 3] += a / images.length;
34
- }
35
- }
36
- return finish(data, images[0].width, images[0].height);
37
- }
38
- export function clearHiddenColors(image) {
39
- const data = Buffer.from(image.data);
40
- for (let p = 0; p < data.length; p += 4) if (!data[p + 3]) data.fill(0, p, p + 3);
41
- return { ...image, data };
42
- }
43
- const sinc = x => x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
44
- function weights(input, output, kernel) {
45
- const ratio = input / output, support = Math.max(1, ratio);
46
- return Array.from({ length: output }, (_, i) => {
47
- const center = (i + 0.5) * ratio - 0.5;
48
- if (kernel === 'neighbor') return [[Math.max(0, Math.min(input - 1, Math.round(center))), 1]];
49
- const result = []; let total = 0;
50
- for (let j = Math.ceil(center - 3 * support); j <= Math.floor(center + 3 * support); j++) {
51
- const distance = (j - center) / support;
52
- const weight = Math.abs(distance) < 3 ? sinc(distance) * sinc(distance / 3) : 0;
53
- result.push([Math.max(0, Math.min(input - 1, j)), weight]); total += weight;
54
- }
55
- return result.map(([j, weight]) => [j, weight / total]);
56
- });
57
- }
58
- export function resizeAlpha(image, width, height, kernel = 'lanczos') {
59
- const input = premultiply(image), horizontal = new Float64Array(width * image.height * 4);
60
- const output = new Float64Array(width * height * 4);
61
- const wx = weights(image.width, width, kernel), wy = weights(image.height, height, kernel);
62
- for (let y = 0; y < image.height; y++) for (let x = 0; x < width; x++) {
63
- for (const [source, weight] of wx[x]) for (let c = 0; c < 4; c++) horizontal[(y * width + x) * 4 + c] += input[(y * image.width + source) * 4 + c] * weight;
64
- }
65
- for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
66
- for (const [source, weight] of wy[y]) for (let c = 0; c < 4; c++) output[(y * width + x) * 4 + c] += horizontal[(source * width + x) * 4 + c] * weight;
67
- }
68
- return finish(output, width, height);
69
- }
1
+ // All weighted operations use linear-light, premultiplied RGBA.
2
+ const linear = Float64Array.from({ length: 256 }, (_, i) => {
3
+ const x = i / 255;
4
+ return x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
5
+ });
6
+ const clamp = x => Math.max(0, Math.min(1, x));
7
+ const srgb = x => Math.round(255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055));
8
+ function premultiply(image) {
9
+ const data = new Float64Array(image.data.length);
10
+ for (let p = 0; p < data.length; p += 4) {
11
+ const a = image.data[p + 3] / 255;
12
+ for (let c = 0; c < 3; c++) data[p + c] = linear[image.data[p + c]] * a;
13
+ data[p + 3] = a;
14
+ }
15
+ return data;
16
+ }
17
+ function finish(data, width, height) {
18
+ const bytes = Buffer.alloc(data.length);
19
+ for (let p = 0; p < data.length; p += 4) {
20
+ const a = data[p + 3];
21
+ bytes[p + 3] = Math.round(clamp(a) * 255);
22
+ // Unpremultiply with the unquantized, unclamped weight, including ringing.
23
+ if (bytes[p + 3]) for (let c = 0; c < 3; c++) bytes[p + c] = srgb(clamp(data[p + c] / a));
24
+ }
25
+ return { width, height, data: bytes };
26
+ }
27
+ export function averageAlpha(images) {
28
+ const data = new Float64Array(images[0].data.length);
29
+ for (const image of images) {
30
+ for (let p = 0; p < data.length; p += 4) {
31
+ const a = image.data[p + 3] / 255;
32
+ for (let c = 0; c < 3; c++) data[p + c] += linear[image.data[p + c]] * a / images.length;
33
+ data[p + 3] += a / images.length;
34
+ }
35
+ }
36
+ return finish(data, images[0].width, images[0].height);
37
+ }
38
+ export function clearHiddenColors(image) {
39
+ const data = Buffer.from(image.data);
40
+ for (let p = 0; p < data.length; p += 4) if (!data[p + 3]) data.fill(0, p, p + 3);
41
+ return { ...image, data };
42
+ }
43
+ const sinc = x => x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
44
+ function weights(input, output, kernel) {
45
+ const ratio = input / output, support = Math.max(1, ratio);
46
+ return Array.from({ length: output }, (_, i) => {
47
+ const center = (i + 0.5) * ratio - 0.5;
48
+ if (kernel === 'neighbor') return [[Math.max(0, Math.min(input - 1, Math.round(center))), 1]];
49
+ const result = []; let total = 0;
50
+ for (let j = Math.ceil(center - 3 * support); j <= Math.floor(center + 3 * support); j++) {
51
+ const distance = (j - center) / support;
52
+ const weight = Math.abs(distance) < 3 ? sinc(distance) * sinc(distance / 3) : 0;
53
+ result.push([Math.max(0, Math.min(input - 1, j)), weight]); total += weight;
54
+ }
55
+ return result.map(([j, weight]) => [j, weight / total]);
56
+ });
57
+ }
58
+ export function resizeAlpha(image, width, height, kernel = 'lanczos') {
59
+ const input = premultiply(image), horizontal = new Float64Array(width * image.height * 4);
60
+ const output = new Float64Array(width * height * 4);
61
+ const wx = weights(image.width, width, kernel), wy = weights(image.height, height, kernel);
62
+ for (let y = 0; y < image.height; y++) for (let x = 0; x < width; x++) {
63
+ for (const [source, weight] of wx[x]) for (let c = 0; c < 4; c++) horizontal[(y * width + x) * 4 + c] += input[(y * image.width + source) * 4 + c] * weight;
64
+ }
65
+ for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
66
+ for (const [source, weight] of wy[y]) for (let c = 0; c < 4; c++) output[(y * width + x) * 4 + c] += horizontal[(source * width + x) * 4 + c] * weight;
67
+ }
68
+ return finish(output, width, height);
69
+ }
package/src/artifacts.mjs CHANGED
@@ -1,61 +1,61 @@
1
- import { constants } from 'node:fs';
2
- import { copyFile, rm, writeFile, access, rename, mkdtemp, mkdir } from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { MultiplaneError } from './errors.mjs';
5
-
6
- const outputError = (error, file) => {
7
- if (error.code === 'EEXIST') throw new MultiplaneError('output_exists', undefined, { details: { path: file }, hint: 'Choose a new output path.' });
8
- throw error;
9
- };
10
- export async function requireNewOutput(file) {
11
- try { await access(file); }
12
- catch (error) { if (error.code === 'ENOENT') return; throw error; }
13
- throw new MultiplaneError('output_exists', undefined, { details: { path: file }, hint: 'Choose a new output path.' });
14
- }
15
- export async function writeArtifact(file, bytes) {
16
- try { await writeFile(file, bytes, { flag: 'wx' }); }
17
- catch (error) { outputError(error, file); }
18
- }
19
- export async function publishVideo(temporary, file, overwrite = false) {
20
- if (overwrite) { await rename(temporary, file); return; }
21
- try { await copyFile(temporary, file, constants.COPYFILE_EXCL); }
22
- catch (error) { outputError(error, file); }
23
- await rm(temporary);
24
- }
25
-
26
- // Build a replacement directory in isolation; publish only after the whole command succeeds.
27
- export async function directoryOutput(events, action) {
28
- const target = events.options.out;
29
- await mkdir(path.dirname(target), { recursive: true });
30
- const temporary = await mkdtemp(target + '.partial-');
31
- const backup = temporary + '.previous';
32
- const artifacts = [], artifact = events.artifact;
33
- const relocate = value => {
34
- if (typeof value === 'string' && value.startsWith(temporary + path.sep)) return target + value.slice(temporary.length);
35
- if (Array.isArray(value)) return value.map(relocate);
36
- if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, relocate(v)]));
37
- return value;
38
- };
39
- events.options.out = temporary;
40
- events.artifact = (file, kind) => artifacts.push([file, kind]);
41
- let saved = false, committed = false, keepTemporary = false;
42
- try {
43
- const data = await action(events);
44
- events.check();
45
- try { await rename(target, backup); saved = true; } catch (error) { if (error.code !== 'ENOENT') throw error; }
46
- try { await rename(temporary, target); }
47
- catch (error) { const restore = saved; saved = false; if (restore) await rename(backup, target); throw error; }
48
- committed = true;
49
- for (const [file, kind] of artifacts) artifact(relocate(file), kind);
50
- return relocate(data);
51
- } catch (error) {
52
- // Keep completed diagnostic artifacts addressable when verification fails.
53
- keepTemporary = artifacts.length > 0;
54
- if (keepTemporary && !committed) for (const [file, kind] of artifacts) artifact(file, kind);
55
- throw error;
56
- } finally {
57
- events.options.out = target; events.artifact = artifact;
58
- if (!keepTemporary) await rm(temporary, { recursive: true, force: true });
59
- if (saved && committed) await rm(backup, { recursive: true, force: true });
60
- }
61
- }
1
+ import { constants } from 'node:fs';
2
+ import { copyFile, rm, writeFile, access, rename, mkdtemp, mkdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { MultiplaneError } from './errors.mjs';
5
+
6
+ const outputError = (error, file) => {
7
+ if (error.code === 'EEXIST') throw new MultiplaneError('output_exists', undefined, { details: { path: file }, hint: 'Choose a new output path.' });
8
+ throw error;
9
+ };
10
+ export async function requireNewOutput(file) {
11
+ try { await access(file); }
12
+ catch (error) { if (error.code === 'ENOENT') return; throw error; }
13
+ throw new MultiplaneError('output_exists', undefined, { details: { path: file }, hint: 'Choose a new output path.' });
14
+ }
15
+ export async function writeArtifact(file, bytes) {
16
+ try { await writeFile(file, bytes, { flag: 'wx' }); }
17
+ catch (error) { outputError(error, file); }
18
+ }
19
+ export async function publishVideo(temporary, file, overwrite = false) {
20
+ if (overwrite) { await rename(temporary, file); return; }
21
+ try { await copyFile(temporary, file, constants.COPYFILE_EXCL); }
22
+ catch (error) { outputError(error, file); }
23
+ await rm(temporary);
24
+ }
25
+
26
+ // Build a replacement directory in isolation; publish only after the whole command succeeds.
27
+ export async function directoryOutput(events, action) {
28
+ const target = events.options.out;
29
+ await mkdir(path.dirname(target), { recursive: true });
30
+ const temporary = await mkdtemp(target + '.partial-');
31
+ const backup = temporary + '.previous';
32
+ const artifacts = [], artifact = events.artifact;
33
+ const relocate = value => {
34
+ if (typeof value === 'string' && value.startsWith(temporary + path.sep)) return target + value.slice(temporary.length);
35
+ if (Array.isArray(value)) return value.map(relocate);
36
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, relocate(v)]));
37
+ return value;
38
+ };
39
+ events.options.out = temporary;
40
+ events.artifact = (file, kind) => artifacts.push([file, kind]);
41
+ let saved = false, committed = false, keepTemporary = false;
42
+ try {
43
+ const data = await action(events);
44
+ events.check();
45
+ try { await rename(target, backup); saved = true; } catch (error) { if (error.code !== 'ENOENT') throw error; }
46
+ try { await rename(temporary, target); }
47
+ catch (error) { const restore = saved; saved = false; if (restore) await rename(backup, target); throw error; }
48
+ committed = true;
49
+ for (const [file, kind] of artifacts) artifact(relocate(file), kind);
50
+ return relocate(data);
51
+ } catch (error) {
52
+ // Keep completed diagnostic artifacts addressable when verification fails.
53
+ keepTemporary = artifacts.length > 0;
54
+ if (keepTemporary && !committed) for (const [file, kind] of artifacts) artifact(file, kind);
55
+ throw error;
56
+ } finally {
57
+ events.options.out = target; events.artifact = artifact;
58
+ if (!keepTemporary) await rm(temporary, { recursive: true, force: true });
59
+ if (saved && committed) await rm(backup, { recursive: true, force: true });
60
+ }
61
+ }
@@ -1,26 +1,26 @@
1
- import { MultiplaneError } from './errors.mjs';
2
-
3
- export const MAX_BROWSER_RESTARTS = 2;
4
- export const recoveryBudget = () => new Int32Array(new SharedArrayBuffer(4));
5
-
6
- // Playwright includes bounded browser logs on transport errors. Never retain the
7
- // launch command (which also contains local profile paths) in public diagnostics.
8
- export function crashDetails(error, t) {
9
- const messages = [], seen = new Set();
10
- for (let e = error; e && !seen.has(e); e = e.cause) { seen.add(e); messages.push(e.message ?? String(e)); }
11
- const log = messages.join('\n');
12
- const exit = log.match(/process did exit: exitCode=([^,\s]+), signal=([^\s>]+)/);
13
- const stderr = log.split(/\r?\n/).filter(line => /\[err\]/.test(line) && !line.includes('<launching>')).slice(-20).join('\n').slice(-4000);
14
- return { ...(t === undefined ? {} : { t }), reason: (messages.at(-1) ?? 'Browser closed unexpectedly').split(/\r?\n/)[0].slice(0, 500),
15
- ...(exit ? { exitCode: exit[1] === 'null' ? null : Number(exit[1]), signal: exit[2] === 'null' ? null : exit[2] } : {}),
16
- ...(stderr ? { stderr } : {}) };
17
- }
18
-
19
- export function browserClosed(error, browser, page, crashed = false) {
20
- if (error?.code === 'render_threw' || error?.code === 'cancelled') return false;
21
- return crashed || page?.isClosed() || (browser && !browser.isConnected())
22
- || /Target (?:page, context or browser|closed)|(?:Page|Browser|Connection|Session) closed|Page crashed|Target crashed|browser has disconnected/i.test(error?.message ?? '')
23
- || (error?.cause && browserClosed(error.cause, browser, page, crashed));
24
- }
25
-
26
- export const crashedError = (error, t) => new MultiplaneError('browser_crashed', undefined, { cause: error, details: crashDetails(error, t) });
1
+ import { MultiplaneError } from './errors.mjs';
2
+
3
+ export const MAX_BROWSER_RESTARTS = 2;
4
+ export const recoveryBudget = () => new Int32Array(new SharedArrayBuffer(4));
5
+
6
+ // Playwright includes bounded browser logs on transport errors. Never retain the
7
+ // launch command (which also contains local profile paths) in public diagnostics.
8
+ export function crashDetails(error, t) {
9
+ const messages = [], seen = new Set();
10
+ for (let e = error; e && !seen.has(e); e = e.cause) { seen.add(e); messages.push(e.message ?? String(e)); }
11
+ const log = messages.join('\n');
12
+ const exit = log.match(/process did exit: exitCode=([^,\s]+), signal=([^\s>]+)/);
13
+ const stderr = log.split(/\r?\n/).filter(line => /\[err\]/.test(line) && !line.includes('<launching>')).slice(-20).join('\n').slice(-4000);
14
+ return { ...(t === undefined ? {} : { t }), reason: (messages.at(-1) ?? 'Browser closed unexpectedly').split(/\r?\n/)[0].slice(0, 500),
15
+ ...(exit ? { exitCode: exit[1] === 'null' ? null : Number(exit[1]), signal: exit[2] === 'null' ? null : exit[2] } : {}),
16
+ ...(stderr ? { stderr } : {}) };
17
+ }
18
+
19
+ export function browserClosed(error, browser, page, crashed = false) {
20
+ if (error?.code === 'render_threw' || error?.code === 'cancelled') return false;
21
+ return crashed || page?.isClosed() || (browser && !browser.isConnected())
22
+ || /Target (?:page, context or browser|closed)|(?:Page|Browser|Connection|Session) closed|Page crashed|Target crashed|browser has disconnected/i.test(error?.message ?? '')
23
+ || (error?.cause && browserClosed(error.cause, browser, page, crashed));
24
+ }
25
+
26
+ export const crashedError = (error, t) => new MultiplaneError('browser_crashed', undefined, { cause: error, details: crashDetails(error, t) });
package/src/capture.mjs CHANGED
@@ -119,10 +119,10 @@ async function openShotOnce(shot, scale = 1, options = {}) {
119
119
  }
120
120
  if (alpha && cdp) await cdp.send('Emulation.setDefaultBackgroundColorOverride', { color: { r: 0, g: 0, b: 0, a: 0 } });
121
121
  const timings = { renderCallMs: 0, screenshotMs: 0 };
122
- let closing, sampleContextUsed = false, motionProbeUsed = false, dofSamples = 0;
123
- const draw = async (t, { motionBlur = 1, motionBlurBase = motionBlur, motionProbeTime, dofMotionSamples } = {}) => {
122
+ let closing, sampleContextUsed = false, motionProbeUsed = false, dofSamples = 0;
123
+ const draw = async (t, { motionBlur = 1, motionBlurBase = motionBlur, motionProbeTime, dofMotionSamples, dofMotionIndex } = {}) => {
124
124
  const renderStart = performance.now();
125
- const changed = await page.evaluate(({ t, motionBlur, motionBlurBase, motionProbeTime, dofMotionSamples, alpha }) => {
125
+ const changed = await page.evaluate(({ t, motionBlur, motionBlurBase, motionProbeTime, dofMotionSamples, dofMotionIndex, alpha }) => {
126
126
  globalThis.__multiplaneStatic?.suspend();
127
127
  if (alpha) document.documentElement.style.setProperty('--multiplane-page-background', 'transparent');
128
128
  const stage = document.querySelector('#stage');
@@ -139,7 +139,7 @@ async function openShotOnce(shot, scale = 1, options = {}) {
139
139
  SHOT.sampleContext = { time: t, motionBlurBase, get motionBlur() { contextUsed = true; return motionBlur; } };
140
140
  let failure;
141
141
  // Private renderer/layer handshake; never part of the shot contract.
142
- const probe = globalThis.__multiplaneMotionProbe = { time: motionProbeTime, used: false, motionSamples: dofMotionSamples, samples: 0 };
142
+ const probe = globalThis.__multiplaneMotionProbe = { time: motionProbeTime, used: false, motionSamples: dofMotionSamples, motionIndex: dofMotionIndex, samples: 0 };
143
143
  try { SHOT.render(t); } catch (error) { failure = String(error?.message ?? error); } finally {
144
144
  delete globalThis.__multiplaneMotionProbe;
145
145
  if (previous === undefined) delete SHOT.sampleContext;
@@ -147,11 +147,11 @@ async function openShotOnce(shot, scale = 1, options = {}) {
147
147
  }
148
148
  stage.getBoundingClientRect();
149
149
  const changed = globalThis.__multiplaneStatic?.read() ?? true;
150
- return { failure, contextUsed, probeUsed: probe.used, dofSamples: probe.samples, changed, reason: globalThis.__multiplaneStatic?.reason };
151
- }, { t, motionBlur, motionBlurBase, motionProbeTime, dofMotionSamples, alpha }).then(({ failure, changed, contextUsed, probeUsed, dofSamples: count, reason }) => {
150
+ return { failure, contextUsed, probeUsed: probe.used, dofSamples: probe.samples, changed, reason: globalThis.__multiplaneStatic?.reason };
151
+ }, { t, motionBlur, motionBlurBase, motionProbeTime, dofMotionSamples, dofMotionIndex, alpha }).then(({ failure, changed, contextUsed, probeUsed, dofSamples: count, reason }) => {
152
152
  sampleContextUsed = contextUsed;
153
- motionProbeUsed = probeUsed;
154
- dofSamples = count;
153
+ motionProbeUsed = probeUsed;
154
+ dofSamples = count;
155
155
  const index = observationReasons.indexOf(reason);
156
156
  if (index >= 0) Atomics.add(reuseState(options), index + 4, 1);
157
157
  if (failure !== undefined) throw new MultiplaneError('render_threw', undefined, { details: { t, reason: failure } });
@@ -177,8 +177,8 @@ async function openShotOnce(shot, scale = 1, options = {}) {
177
177
  return {
178
178
  page, browser, server, ...metadata, alpha, beginFrame, timings,
179
179
  get sampleContextUsed() { return sampleContextUsed; },
180
- get motionProbeUsed() { return motionProbeUsed; },
181
- get dofSamples() { return dofSamples; },
180
+ get motionProbeUsed() { return motionProbeUsed; },
181
+ get dofSamples() { return dofSamples; },
182
182
  ...(reuseEnabled(options) ? { observe: draw } : {}),
183
183
  isCrash: error => browserClosed(error, browser, page, crashed),
184
184
  ...(metadata.hasBlurAt ? { blurAt: async t => {
@@ -220,8 +220,8 @@ export async function openShot(shot, scale = 1, options = {}) {
220
220
  get page() { return current.page; },
221
221
  get browser() { return current.browser; },
222
222
  get sampleContextUsed() { return current.sampleContextUsed; },
223
- get motionProbeUsed() { return current.motionProbeUsed; },
224
- get dofSamples() { return current.dofSamples; },
223
+ get motionProbeUsed() { return current.motionProbeUsed; },
224
+ get dofSamples() { return current.dofSamples; },
225
225
  ...(current.observe ? { observe: (...args) => current.observe(...args) } : {}),
226
226
  beginFrame: (...args) => current.beginFrame(...args),
227
227
  frame: (...args) => current.frame(...args),
package/src/encoder.mjs CHANGED
@@ -1,16 +1,16 @@
1
- import { invalidOption } from './errors.mjs';
2
- export const encoders = ['x264', 'nvenc-h264', 'nvenc-hevc'];
3
-
4
- export function encoderArgs(name = 'x264', quality, maxMbps) {
5
- if (!encoders.includes(name)) throw invalidOption('--encoder', '--encoder must be x264, nvenc-h264 or nvenc-hevc.');
6
- if (maxMbps !== undefined && !(Number.isFinite(maxMbps) && maxMbps > 0)) throw invalidOption('--max-bitrate', '--max-bitrate must be positive (Mbps).');
7
- const cap = maxMbps ? ['-maxrate', `${maxMbps}M`, '-bufsize', `${maxMbps * 2}M`] : [];
8
- if (name === 'x264') return ['-c:v', 'libx264', '-profile:v', 'high', '-crf', '16', ...cap];
9
- quality ??= name === 'nvenc-hevc' ? 20 : 21;
10
- if (!Number.isInteger(quality) || quality < 1 || quality > 51) throw invalidOption('NVENC', 'NVENC CQ must be an integer from 1 to 51.');
11
- return ['-c:v', name === 'nvenc-h264' ? 'h264_nvenc' : 'hevc_nvenc',
12
- '-preset', 'p7', '-tune', 'hq', '-rc', 'vbr', '-cq', String(quality), '-b:v', '0',
13
- ...(maxMbps ? cap : ['-qmin', '0', '-qmax', String(Math.min(51, quality + 2))]),
14
- '-multipass', 'fullres', '-spatial-aq', '1', '-temporal-aq', '1', '-rc-lookahead', '32', '-bf', '3',
15
- ...(name === 'nvenc-h264' ? ['-profile:v', 'high'] : ['-tag:v', 'hvc1'])];
16
- }
1
+ import { invalidOption } from './errors.mjs';
2
+ export const encoders = ['x264', 'nvenc-h264', 'nvenc-hevc'];
3
+
4
+ export function encoderArgs(name = 'x264', quality, maxMbps) {
5
+ if (!encoders.includes(name)) throw invalidOption('--encoder', '--encoder must be x264, nvenc-h264 or nvenc-hevc.');
6
+ if (maxMbps !== undefined && !(Number.isFinite(maxMbps) && maxMbps > 0)) throw invalidOption('--max-bitrate', '--max-bitrate must be positive (Mbps).');
7
+ const cap = maxMbps ? ['-maxrate', `${maxMbps}M`, '-bufsize', `${maxMbps * 2}M`] : [];
8
+ if (name === 'x264') return ['-c:v', 'libx264', '-profile:v', 'high', '-crf', '16', ...cap];
9
+ quality ??= name === 'nvenc-hevc' ? 20 : 21;
10
+ if (!Number.isInteger(quality) || quality < 1 || quality > 51) throw invalidOption('NVENC', 'NVENC CQ must be an integer from 1 to 51.');
11
+ return ['-c:v', name === 'nvenc-h264' ? 'h264_nvenc' : 'hevc_nvenc',
12
+ '-preset', 'p7', '-tune', 'hq', '-rc', 'vbr', '-cq', String(quality), '-b:v', '0',
13
+ ...(maxMbps ? cap : ['-qmin', '0', '-qmax', String(Math.min(51, quality + 2))]),
14
+ '-multipass', 'fullres', '-spatial-aq', '1', '-temporal-aq', '1', '-rc-lookahead', '32', '-bf', '3',
15
+ ...(name === 'nvenc-h264' ? ['-profile:v', 'high'] : ['-tag:v', 'hvc1'])];
16
+ }