multiplane 0.0.0-stage → 0.3.0

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 (132) hide show
  1. package/LICENSE +202 -0
  2. package/README.en.md +322 -0
  3. package/README.md +321 -2
  4. package/THIRD_PARTY_NOTICES.md +30 -0
  5. package/bin/multiplane +98 -0
  6. package/examples/alpha.html +18 -0
  7. package/examples/animation.html +17 -0
  8. package/examples/assets/OFL.txt +93 -0
  9. package/examples/assets/Roboto-Variable.ttf +0 -0
  10. package/examples/assets/card.svg +6 -0
  11. package/examples/assets/readme-card-0.svg +1 -0
  12. package/examples/assets/readme-card-1.svg +1 -0
  13. package/examples/assets/readme-card-2.svg +1 -0
  14. package/examples/assets/readme-detail.svg +1 -0
  15. package/examples/readme-cards.html +23 -0
  16. package/examples/readme-cut.html +13 -0
  17. package/examples/readme-depth.html +15 -0
  18. package/examples/readme-wheel.html +14 -0
  19. package/examples/three-cards.html +17 -0
  20. package/examples/typography.html +17 -0
  21. package/licenses/GPL-3.0.txt +232 -0
  22. package/licenses/LGPL-3.0.txt +304 -0
  23. package/licenses/acorn-LICENSE +21 -0
  24. package/licenses/colour-LICENSE.md +82 -0
  25. package/licenses/detect-libc-LICENSE +201 -0
  26. package/licenses/entities-LICENSE +11 -0
  27. package/licenses/nanoid-LICENSE +20 -0
  28. package/licenses/native/README.ijg +258 -0
  29. package/licenses/native/aom-LICENSE +27 -0
  30. package/licenses/native/aom-PATENTS +108 -0
  31. package/licenses/native/archive-COPYING +65 -0
  32. package/licenses/native/cairo-COPYING +33 -0
  33. package/licenses/native/cairo-COPYING-MPL-1.1 +470 -0
  34. package/licenses/native/cgif-LICENSE +21 -0
  35. package/licenses/native/exif-COPYING +504 -0
  36. package/licenses/native/expat-COPYING +21 -0
  37. package/licenses/native/ffi-LICENSE +21 -0
  38. package/licenses/native/fontconfig-COPYING +200 -0
  39. package/licenses/native/freetype-FTL.TXT +169 -0
  40. package/licenses/native/fribidi-COPYING +501 -0
  41. package/licenses/native/glib-COPYING +175 -0
  42. package/licenses/native/harfbuzz-COPYING +42 -0
  43. package/licenses/native/heif-COPYING +871 -0
  44. package/licenses/native/highway-LICENSE +371 -0
  45. package/licenses/native/imagequant-COPYRIGHT +36 -0
  46. package/licenses/native/lcms-COPYING +22 -0
  47. package/licenses/native/mozjpeg-LICENSE.md +135 -0
  48. package/licenses/native/nsgif-COPYING +21 -0
  49. package/licenses/native/pango-COPYING +482 -0
  50. package/licenses/native/pixman-COPYING +42 -0
  51. package/licenses/native/png-LICENSE +134 -0
  52. package/licenses/native/proxy-libintl-COPYING +482 -0
  53. package/licenses/native/rsvg-COPYING.LIB +502 -0
  54. package/licenses/native/sources.json +240 -0
  55. package/licenses/native/tiff-LICENSE.md +49 -0
  56. package/licenses/native/uhdr-LICENSE +230 -0
  57. package/licenses/native/vips-COPYING +502 -0
  58. package/licenses/native/webp-COPYING +30 -0
  59. package/licenses/native/webp-PATENTS +23 -0
  60. package/licenses/native/xml2-Copyright +24 -0
  61. package/licenses/native/zlib-ng-LICENSE.md +19 -0
  62. package/licenses/parse5-LICENSE +19 -0
  63. package/licenses/picocolors-LICENSE +15 -0
  64. package/licenses/playwright-LICENSE +202 -0
  65. package/licenses/playwright-NOTICE +5 -0
  66. package/licenses/playwright-ThirdPartyNotices.txt +14 -0
  67. package/licenses/playwright-core-ThirdPartyNotices.txt +13 -0
  68. package/licenses/postcss-LICENSE +20 -0
  69. package/licenses/postcss-value-parser-LICENSE +22 -0
  70. package/licenses/semver-LICENSE +15 -0
  71. package/licenses/sharp-LICENSE +191 -0
  72. package/licenses/sharp-win32-x64-LICENSE +191 -0
  73. package/licenses/sharp-win32-x64-NOTICES.md +59 -0
  74. package/licenses/source-map-js-LICENSE +28 -0
  75. package/licenses/three-LICENSE +21 -0
  76. package/package.json +92 -6
  77. package/skills/multiplane/SKILL.md +213 -0
  78. package/src/alpha.mjs +69 -0
  79. package/src/artifacts.mjs +61 -0
  80. package/src/benchmark.mjs +140 -0
  81. package/src/browser-recovery.mjs +26 -0
  82. package/src/capture.mjs +224 -0
  83. package/src/config.mjs +79 -0
  84. package/src/doctor.mjs +56 -0
  85. package/src/draft.mjs +30 -0
  86. package/src/encoder.mjs +16 -0
  87. package/src/environment.mjs +56 -0
  88. package/src/errors.mjs +51 -0
  89. package/src/events.mjs +252 -0
  90. package/src/ffmpeg.mjs +32 -0
  91. package/src/format.mjs +27 -0
  92. package/src/frame-rate.mjs +28 -0
  93. package/src/home-migration.mjs +54 -0
  94. package/src/hosted.mjs +134 -0
  95. package/src/index.mjs +14 -0
  96. package/src/inspect.mjs +20 -0
  97. package/src/jobs.mjs +51 -0
  98. package/src/lint.mjs +248 -0
  99. package/src/output-alpha.mjs +36 -0
  100. package/src/output-check.mjs +110 -0
  101. package/src/pixel-comparison.mjs +43 -0
  102. package/src/platform.mjs +114 -0
  103. package/src/png-native.mjs +12 -0
  104. package/src/png.mjs +74 -0
  105. package/src/post.mjs +433 -0
  106. package/src/preview.mjs +23 -0
  107. package/src/process-identity.mjs +17 -0
  108. package/src/process.mjs +9 -0
  109. package/src/render-pool.mjs +93 -0
  110. package/src/render-worker.mjs +59 -0
  111. package/src/render.mjs +157 -0
  112. package/src/resource-path.mjs +29 -0
  113. package/src/resources.mjs +27 -0
  114. package/src/runtime/camera.js +37 -0
  115. package/src/runtime/shot.css +9 -0
  116. package/src/runtime/shot.js +71 -0
  117. package/src/serve.mjs +23 -0
  118. package/src/server.mjs +93 -0
  119. package/src/setup.mjs +51 -0
  120. package/src/skills.mjs +128 -0
  121. package/src/static-reuse.mjs +265 -0
  122. package/src/stills.mjs +81 -0
  123. package/src/three/index.js +348 -0
  124. package/src/user-home.mjs +105 -0
  125. package/src/verify.mjs +161 -0
  126. package/src/video-pipe.mjs +6 -0
  127. package/src/workers.mjs +62 -0
  128. package/tools/bench/shots/canvas-2d.html +1 -0
  129. package/tools/bench/shots/dom-light.html +1 -0
  130. package/tools/bench/shots/scene.js +45 -0
  131. package/tools/bench/shots/webgl.html +1 -0
  132. package/tools/install-browser.mjs +18 -0
@@ -0,0 +1,43 @@
1
+ // Calibration and boundaries: tools/reports/059-gpu-jitter-tolerance.md.
2
+ // Set between the largest measured GPU jitter (0.109) and the weakest real change (0.875), with similar margin both ways; see tools/reports/059.
3
+ export const GPU_JITTER_BLOCK_THRESHOLD = 0.3;
4
+ export const GPU_JITTER_CHANNEL_LIMIT = 32;
5
+
6
+ export function classifyPixelDifference(difference, raster = 'gpu') {
7
+ if (difference.changedPixels === 0) return 'identical';
8
+ return raster === 'gpu' && difference.maxBlockMeanDiff <= GPU_JITTER_BLOCK_THRESHOLD &&
9
+ difference.maxChannelDiff <= GPU_JITTER_CHANNEL_LIMIT ? 'jitter' : 'different';
10
+ }
11
+
12
+ // Absolute differences are averaged per channel, never across channels.
13
+ export function pixelDifference(reference, actual, { distribution = false } = {}) {
14
+ if (reference.width !== actual.width || reference.height !== actual.height ||
15
+ reference.data.length !== reference.width * reference.height * 4 || actual.data.length !== reference.data.length) {
16
+ throw new RangeError('RGBA image dimensions must match.');
17
+ }
18
+ const { width, height } = reference;
19
+ let changedPixels = 0, maxChannelDiff = 0, maxBlockMeanDiff = 0;
20
+ const blockMeans = distribution ? [] : undefined;
21
+ if (!distribution && reference.data.equals(actual.data)) return { changedPixels, maxChannelDiff, maxBlockMeanDiff };
22
+ for (let y = 0; y < height; y += 8) for (let x = 0; x < width; x += 8) {
23
+ const sums = [0, 0, 0, 0], w = Math.min(8, width - x), h = Math.min(8, height - y);
24
+ for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) {
25
+ const p = ((y + j) * width + x + i) * 4;
26
+ let changed = false;
27
+ for (let c = 0; c < 4; c++) {
28
+ const delta = Math.abs(reference.data[p + c] - actual.data[p + c]);
29
+ sums[c] += delta;
30
+ maxChannelDiff = Math.max(maxChannelDiff, delta);
31
+ changed ||= delta > 0;
32
+ }
33
+ if (changed) changedPixels++;
34
+ }
35
+ for (let c = 0; c < 4; c++) {
36
+ const mean = sums[c] / (w * h);
37
+ maxBlockMeanDiff = Math.max(maxBlockMeanDiff, mean);
38
+ // Opaque alpha has no encoding error and is excluded from RGB statistics.
39
+ if (distribution && c < 3) blockMeans.push(mean);
40
+ }
41
+ }
42
+ return { changedPixels, maxChannelDiff, maxBlockMeanDiff, ...(distribution ? { blockMeans } : {}) };
43
+ }
@@ -0,0 +1,114 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { execFile, spawn } from 'node:child_process';
4
+ import { createInterface } from 'node:readline';
5
+ import { promisify } from 'node:util';
6
+
7
+ const execute = promisify(execFile);
8
+ const windowsMemoryQuery = '$rows = @(Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,WorkingSetSize | Select-Object ProcessId,ParentProcessId,WorkingSetSize); @{ observer = $PID; rows = $rows } | ConvertTo-Json -Compress';
9
+ function windowsMemory(stdout, pid) {
10
+ const data = JSON.parse(stdout);
11
+ const rows = data.rows.map(row => ({ pid: Number(row.ProcessId), parent: Number(row.ParentProcessId), bytes: Number(row.WorkingSetSize) }));
12
+ return sumProcessTree(rows, pid, [data.observer]);
13
+ }
14
+
15
+ // Reuse the Windows observer to avoid launching PowerShell for every sample.
16
+ // It reads requests on stdin and writes one JSON line per response.
17
+ export function createProcessTreeReader({ platform = process.platform, pid = process.pid } = {}) {
18
+ if (platform !== 'win32') return { sample: () => processTreeMemory({ platform, pid }), close() {} };
19
+ let child, pending, closed = false, failed = false;
20
+ const settle = value => { const current = pending; pending = undefined; if (current) { clearTimeout(current.timer); current.resolve(value); } };
21
+ const fail = () => { failed = true; settle(null); child?.kill(); };
22
+ return {
23
+ sample() {
24
+ if (closed || failed) return Promise.resolve(null);
25
+ if (pending) return pending.promise;
26
+ if (!child) {
27
+ const script = `$ErrorActionPreference = 'Stop'; while ($null -ne [Console]::ReadLine()) { try { ${windowsMemoryQuery} } catch { [Console]::WriteLine('null') } }`;
28
+ child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { windowsHide: true, stdio: ['pipe', 'pipe', 'ignore'] });
29
+ child.on('error', fail); child.on('close', () => { failed = true; settle(null); }); child.stdin.on('error', fail);
30
+ createInterface({ input: child.stdout }).on('line', line => {
31
+ let bytes = null; try { bytes = windowsMemory(line, pid); } catch {}
32
+ settle(bytes);
33
+ });
34
+ }
35
+ let resolve;
36
+ const promise = new Promise(done => { resolve = done; });
37
+ pending = { promise, resolve, timer: setTimeout(fail, 5000) };
38
+ child.stdin.write('\n');
39
+ return promise;
40
+ },
41
+ async close() {
42
+ if (closed) return;
43
+ closed = true; settle(null);
44
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
45
+ await new Promise(resolve => {
46
+ const timer = setTimeout(() => child.kill('SIGKILL'), 1000);
47
+ child.once('close', () => { clearTimeout(timer); resolve(); });
48
+ child.stdin.end();
49
+ });
50
+ },
51
+ };
52
+ }
53
+ export function sumProcessTree(rows, rootPid, excluded = []) {
54
+ const selected = new Set([rootPid]), blocked = new Set(excluded);
55
+ for (let changed = true; changed;) {
56
+ changed = false;
57
+ for (const row of rows) if (!blocked.has(row.pid) && selected.has(row.parent) && !selected.has(row.pid)) { selected.add(row.pid); changed = true; }
58
+ }
59
+ if (!rows.some(row => row.pid === rootPid)) return null;
60
+ let sum = 0;
61
+ for (const row of rows) if (selected.has(row.pid)) {
62
+ if (!Number.isSafeInteger(row.bytes) || row.bytes < 0) return null;
63
+ sum += row.bytes;
64
+ }
65
+ return Number.isSafeInteger(sum) && sum > 0 ? sum : null;
66
+ }
67
+
68
+ export async function processTreeMemory({ platform = process.platform, pid = process.pid, run = execute } = {}) {
69
+ try {
70
+ let rows;
71
+ if (platform === 'win32') {
72
+ // Exclude the observer itself and its descendants from the target tree.
73
+ const script = '$ErrorActionPreference = "Stop"; ' + windowsMemoryQuery;
74
+ const { stdout } = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { windowsHide: true, timeout: 5000, maxBuffer: 8 * 1024 * 1024 });
75
+ return windowsMemory(stdout, pid);
76
+ }
77
+ if (!['linux', 'darwin'].includes(platform)) return null;
78
+ const { stdout } = await run('ps', ['-axo', 'pid=,ppid=,rss=,comm='], { timeout: 5000, maxBuffer: 8 * 1024 * 1024 });
79
+ rows = stdout.trim().split(/\r?\n/).map(line => {
80
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.+)$/);
81
+ if (!match) throw new Error('Invalid process table.');
82
+ return { pid: Number(match[1]), parent: Number(match[2]), bytes: Number(match[3]) * 1024, name: match[4] };
83
+ });
84
+ return sumProcessTree(rows, pid, rows.filter(row => row.parent === pid && /(^|\/)ps$/.test(row.name)).map(row => row.pid));
85
+ } catch { return null; }
86
+ }
87
+
88
+ // Keep platform-specific Chromium switches isolated from capture logic.
89
+ export function browserOptions(raster = 'gpu', options = {}, platform = process.platform) {
90
+ const gpu = raster === 'gpu';
91
+ return { headless: true, ...(gpu ? { channel: 'chromium' } : {}), args: [
92
+ '--disable-partial-raster',
93
+ ...(options.rasterThreads ? [`--num-raster-threads=${options.rasterThreads}`] : []),
94
+ ...(gpu ? ['--enable-gpu-rasterization', ...(platform === 'win32' ? ['--use-angle=d3d11'] : platform === 'darwin' ? ['--use-angle=metal'] : ['--use-angle=gl']),
95
+ '--force-high-performance-gpu', '--ignore-gpu-blocklist'] : []),
96
+ ...(!gpu && platform !== 'win32' ? ['--use-angle=swiftshader', '--enable-unsafe-swiftshader'] : []),
97
+ ] };
98
+ }
99
+
100
+ export const ffmpegName = (platform = process.platform) => platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
101
+ export const ffmpegHint = (platform = process.platform) => `${platform === 'darwin' ? 'Run brew install ffmpeg' : platform === 'linux' ? 'Install FFmpeg with libx264 using your distribution package manager (for example: sudo apt-get install ffmpeg)' : 'Get FFmpeg with libx264 from https://ffmpeg.org/download.html'} and set --ffmpeg, MULTIPLANE_FFMPEG or PATH.`;
102
+ export const browserHint = (missing, platform = process.platform) => missing ? 'Run npx playwright install chromium.'
103
+ : platform === 'linux' ? 'Run npx playwright install-deps chromium, then npx playwright install chromium.' : 'Run npx playwright install chromium.';
104
+
105
+ export const stopSignals = (platform = process.platform) => platform === 'win32'
106
+ ? ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK'] : ['SIGINT', 'SIGTERM', 'SIGHUP'];
107
+ export function userHome(env = process.env, platform = process.platform, home = os.homedir()) {
108
+ const paths = platform === 'win32' ? path.win32 : path.posix;
109
+ if (env.MULTIPLANE_HOME) return paths.resolve(env.MULTIPLANE_HOME);
110
+ if (platform === 'win32') return paths.join(env.USERPROFILE || home, '.multiplane');
111
+ const base = platform === 'darwin' ? paths.join(home, 'Library', 'Application Support')
112
+ : (env.XDG_CONFIG_HOME || paths.join(home, '.config'));
113
+ return paths.join(base, 'multiplane');
114
+ }
@@ -0,0 +1,12 @@
1
+ import { MultiplaneError } from './errors.mjs';
2
+ import sharp from 'sharp';
3
+
4
+ sharp.concurrency(1);
5
+ sharp.cache(false);
6
+
7
+ export async function decodeNativePNG(png) {
8
+ if (png.length < 33 || !png.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex')) || png[24] !== 8 || ![2, 6].includes(png[25]) || png[26] || png[27] || png[28]) throw new MultiplaneError('internal_error', 'Only non-interlaced 8-bit RGB/RGBA PNG is supported.');
9
+ const { data, info } = await sharp(png, { ignoreIcc: true }).ensureAlpha().raw({ depth: 'uchar' }).toBuffer({ resolveWithObject: true });
10
+ if (info.channels !== 4 || data.length !== info.width * info.height * 4) throw new MultiplaneError('internal_error', 'Native PNG decoder returned an invalid format.');
11
+ return { width: info.width, height: info.height, data };
12
+ }
package/src/png.mjs ADDED
@@ -0,0 +1,74 @@
1
+ import { MultiplaneError } from './errors.mjs';
2
+ import { deflateSync, inflateSync, crc32 } from 'node:zlib';
3
+
4
+ export function decodePNG(png) {
5
+ if (!png.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex'))) throw new MultiplaneError('internal_error', 'Invalid PNG.');
6
+ let width, height, channels;
7
+ const chunks = [];
8
+ for (let offset = 8; offset < png.length;) {
9
+ const size = png.readUInt32BE(offset), type = png.toString('ascii', offset + 4, offset + 8);
10
+ const data = png.subarray(offset + 8, offset + 8 + size);
11
+ if (type === 'IHDR') {
12
+ width = data.readUInt32BE(0); height = data.readUInt32BE(4);
13
+ if (data[8] !== 8 || ![2, 6].includes(data[9]) || data[10] || data[11] || data[12]) throw new MultiplaneError('internal_error', 'Only non-interlaced 8-bit RGB/RGBA PNG is supported.');
14
+ channels = data[9] === 2 ? 3 : 4;
15
+ }
16
+ if (type === 'IDAT') chunks.push(data);
17
+ offset += size + 12;
18
+ }
19
+ const raw = inflateSync(Buffer.concat(chunks)), stride = width * channels;
20
+ if (raw.length !== (stride + 1) * height) throw new MultiplaneError('internal_error', 'PNG data length mismatch.');
21
+ const line = stride + 1;
22
+ for (let y = 0; y < height; y++) {
23
+ const filter = raw[y * line];
24
+ if (filter > 4) throw new MultiplaneError('internal_error', 'Invalid PNG row filter.');
25
+ const row = y * line + 1;
26
+ if (filter === 0) continue;
27
+ if (filter === 1 || (filter === 4 && y === 0)) {
28
+ for (let x = channels; x < stride; x++) raw[row + x] += raw[row + x - channels];
29
+ continue;
30
+ }
31
+ if (filter === 2) {
32
+ if (y) for (let x = 0; x < stride; x++) raw[row + x] += raw[row + x - line];
33
+ continue;
34
+ }
35
+ if (filter === 3) {
36
+ for (let x = 0; x < stride; x++) raw[row + x] += ((x >= channels ? raw[row + x - channels] : 0) + (y ? raw[row + x - line] : 0)) >>> 1;
37
+ continue;
38
+ }
39
+ for (let x = 0; x < stride; x++) {
40
+ const p = row + x;
41
+ const a = x >= channels ? raw[p - channels] : 0;
42
+ const b = raw[p - line];
43
+ const c = x >= channels ? raw[p - line - channels] : 0;
44
+ const predictor = a + b - c;
45
+ const pa = Math.abs(predictor - a), pb = Math.abs(predictor - b), pc = Math.abs(predictor - c);
46
+ const correction = pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
47
+ raw[p] += correction;
48
+ }
49
+ }
50
+ const data = Buffer.alloc(width * height * 4);
51
+ for (let y = 0; y < height; y++) {
52
+ const row = y * line + 1;
53
+ if (channels === 4) { raw.copy(data, y * stride, row, row + stride); continue; }
54
+ for (let x = 0, j = y * width * 4; x < stride; x += 3, j += 4) {
55
+ data[j] = raw[row + x]; data[j + 1] = raw[row + x + 1]; data[j + 2] = raw[row + x + 2]; data[j + 3] = 255;
56
+ }
57
+ }
58
+ return { width, height, data };
59
+ }
60
+
61
+ function chunk(type, data) {
62
+ const result = Buffer.alloc(data.length + 12);
63
+ result.writeUInt32BE(data.length); result.write(type, 4, 'ascii'); data.copy(result, 8);
64
+ result.writeUInt32BE(crc32(result.subarray(4, -4)), result.length - 4);
65
+ return result;
66
+ }
67
+
68
+ export function encodePNG({ width, height, data }) {
69
+ const header = Buffer.alloc(13);
70
+ header.writeUInt32BE(width); header.writeUInt32BE(height, 4); header[8] = 8; header[9] = 6;
71
+ const stride = width * 4, raw = Buffer.alloc((stride + 1) * height);
72
+ for (let y = 0; y < height; y++) data.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
73
+ return Buffer.concat([Buffer.from('89504e470d0a1a0a', 'hex'), chunk('IHDR', header), chunk('IDAT', deflateSync(raw, { level: 1 })), chunk('IEND', Buffer.alloc(0))]);
74
+ }