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
package/README.md CHANGED
@@ -1,3 +1,322 @@
1
- # Temporary Holding Version
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/LBEILC/Multiplane/main/app/design/icons/readme-dark.svg">
3
+ <img src="https://raw.githubusercontent.com/LBEILC/Multiplane/main/app/design/icons/readme-light.svg" width="72" height="72" alt="">
4
+ </picture>
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # Multiplane
7
+
8
+ [English](README.en.md) · 简体中文
9
+
10
+ 给 AI agent 用的本地无头 HTML 视频渲染器。**镜头就是普通网页**:页面实现 `render(t)`,画面只由时间 `t` 决定;Multiplane 用无头 Chromium 逐帧截图,做超采样、运动模糊、胶片颗粒等后期,用 FFmpeg 合成视频,并能检查镜头是否确定。
11
+
12
+ <img src="https://raw.githubusercontent.com/LBEILC/Multiplane/main/docs/images/hero.webp" width="720" alt="Multiplane 渲染的三维卡片:景深虚化与运动模糊">
13
+
14
+ > 当前版本 `0.3.0`。完整测试在 Windows、macOS、Linux 三个平台的 CI 上全部通过;Windows 另在真机上验证过。1.0 之前接口仍可能调整。
15
+
16
+ 名字来自迪士尼的多层摄影台(multiplane camera):一层层平面画稿放在不同深度、逐帧拍摄、带真实景深。对应这里的多层网页画面、三维卡片层和累积景深。
17
+
18
+ ## 特点
19
+
20
+ - **不用学新框架**。镜头是 HTML、CSS 和一段 JavaScript,agent 最擅长写的东西。
21
+ - **渲染前检查**。`lint` 不开浏览器、不到一秒,指出墙钟时间、随机数、CSS 动画、网络资源等会破坏确定性的写法,定位到文件、行、列;渲染命令启动前会自动先跑。
22
+ - **可验证**。`verify` 会乱序、重载页面、单进程和多进程分别渲染同一批帧,逐像素比较;不一致时给出出问题的时刻、区域和差异图。
23
+ - **运动模糊不跨硬切**。每帧在快门时间内多次采样、按线性光混合;镜头声明的硬切时刻 `cuts` 两侧的画面不会糊在一起。
24
+ - **三维层与累积景深**(可选,`multiplane/three`):图片卡片进三维空间,多次采样累积出真实的焦外虚化,与运动模糊配合。
25
+ - **机器可读**。所有命令支持 `--json`,输出带稳定错误码的事件流;每次运行有独立的作业目录,可查询进度、可取消。
26
+ - **也能出静帧**。`stills` 按指定时刻输出 PNG,和视频走同一套超采样、运动模糊、景深和透明通道。可以用来抽帧检查,也可以直接出封面、海报、配图这类高质量静帧。
27
+ - **确定性**。渲染器自身不引入墙钟时间和未设种子的随机数;同一输入在同一环境下逐像素一致,并由测试保证。
28
+
29
+ ## 效果
30
+
31
+ **运动模糊**:左边不开运动模糊;中间固定 8 个采样,快速转动的轮辐变成一条条分开的残影;右边按移动距离自动加采样,连成顺滑的拖尾。下排是局部放大。
32
+
33
+ <img src="https://raw.githubusercontent.com/LBEILC/Multiplane/main/docs/images/motion-blur.png" width="800" alt="运动模糊对比:不开模糊、固定 8 采样、自动加采样">
34
+
35
+ **硬切不糊在一起**:切点后的第一帧。左边没有声明硬切,运动模糊把切点前一个画面的颜色和圆形混了进来;右边在镜头里声明了 `cuts`,切得干净。
36
+
37
+ <img src="https://raw.githubusercontent.com/LBEILC/Multiplane/main/docs/images/hard-cut.png" width="800" alt="硬切对比:未声明与声明 cuts">
38
+
39
+ **三维景深**:同一组卡片,焦点分别在前景和背景。焦外虚化是多次采样累积出来的,不是按深度图做的后期模糊,所以前景边缘不会溢出、背景也不会渗进前景的轮廓。下排是前景卡片的局部放大。
40
+
41
+ <img src="https://raw.githubusercontent.com/LBEILC/Multiplane/main/docs/images/depth-of-field.png" width="800" alt="景深对比:焦点在前景与焦点在背景">
42
+
43
+ ## 快速开始
44
+
45
+ 需要 Node.js 24 或更新版本,以及支持 libx264 的 FFmpeg。
46
+
47
+ ```sh
48
+ npm install multiplane
49
+ npx playwright install chromium
50
+ npx multiplane doctor
51
+ npx multiplane render node_modules/multiplane/examples/animation.html
52
+ ```
53
+
54
+ 新版 npm 默认不运行依赖包的安装脚本,所以第二条命令用来下载 Playwright 使用的 Chromium。如果是克隆了本仓库,在仓库里运行 `npm install`,直接渲染 `examples/animation.html` 即可。
55
+
56
+ 把使用说明 skill 交给你的 agent:运行 `npx multiplane skill install --host all`(Claude Code 装到 `~/.claude/skills/multiplane`,Codex 装到 `~/.agents/skills/multiplane`),或者让桌面软件来安装。装好后新开一个 agent 会话。
57
+
58
+ 在克隆的仓库里,`npm install` 会自己下载这个 Chromium(已安装则复用)。离线环境可设 `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` 跳过,之后再运行 `npx playwright install chromium`。Linux 上 Chromium 还需要系统库:运行一次 `npx playwright install-deps chromium`(缺的时候 `doctor` 会提示)。
59
+
60
+ ### FFmpeg
61
+
62
+ Multiplane 不随包分发 FFmpeg。需要自己安装一个支持 libx264 的:
63
+
64
+ | 系统 | 一种获取方式 |
65
+ |---|---|
66
+ | Windows | 从 [FFmpeg 官方下载页](https://ffmpeg.org/download.html) 获取构建 |
67
+ | macOS | `brew install ffmpeg` |
68
+ | Linux | 发行版的软件包,例如 `sudo apt-get install ffmpeg` |
69
+
70
+ FFmpeg 的许可取决于你安装的构建。然后用以下任一方式告诉 Multiplane(按优先级):
71
+
72
+ 1. 命令行参数 `--ffmpeg <path>`;
73
+ 2. 环境变量 `MULTIPLANE_FFMPEG`;
74
+ 3. 用户配置文件里的 `ffmpeg`(见下文“用户配置”);
75
+ 4. 放进系统 PATH。
76
+
77
+ 想一次设好、所有项目都生效,运行 `npx multiplane config set ffmpeg <path>`,它会顺便检查这个文件能不能用。
78
+
79
+ 默认用软件编码 `x264`;有 NVIDIA 显卡时可用 `--encoder nvenc-h264` 或 `--encoder nvenc-hevc`。`doctor` 会检查 FFmpeg 能否运行、是否支持所需编码器。
80
+
81
+ ## 写一个镜头
82
+
83
+ ```html
84
+ <!doctype html>
85
+ <html lang="en"><meta charset="utf-8"><title>Example</title>
86
+ <link rel="stylesheet" href="/multiplane/runtime/shot.css">
87
+ <style>
88
+ #stage { width: 1920px; height: 1080px; background: #111; }
89
+ #box { position: absolute; left: 0; top: 490px; width: 100px; height: 100px; background: #eee; }
90
+ </style>
91
+ <div id="stage"><div id="box"></div></div>
92
+ <script src="/multiplane/runtime/shot.js"></script>
93
+ <script>
94
+ const box = document.querySelector('#box');
95
+ Shot.define({
96
+ duration: 4,
97
+ fps: 30,
98
+ render(t) {
99
+ box.style.transform = `translateX(${Math.min(t / 3, 1) * 1820}px)`;
100
+ },
101
+ });
102
+ </script></html>
103
+ ```
104
+
105
+ 约定只有三条:
106
+
107
+ - 画面都画在 `#stage` 里,用固定的 CSS 宽高声明画幅;
108
+ - `render(t)` 是同步函数,同一个 `t` 永远画出同样的画面;
109
+ - 字体、图片等素材用本地文件,在 `setup()` / `ready` 里加载完。
110
+
111
+ 完整约定、哪些写法会破坏确定性、三维层用法,见 agent 使用说明 [`skills/multiplane/SKILL.md`](skills/multiplane/SKILL.md)。它同时是给 agent 加载的 skill。
112
+
113
+ ## 命令
114
+
115
+ | 命令 | 用途 |
116
+ |---|---|
117
+ | `multiplane lint <shot>` | 静态代码检查,不开浏览器 |
118
+ | `multiplane render <shot>` | 渲染成 MP4 |
119
+ | `multiplane stills <shot>` | 按时刻输出静帧(PNG)并生成联系表;可用于检查,也可直接出高质量静帧 |
120
+ | `multiplane verify <shot>` | 确定性检查 |
121
+ | `multiplane inspect <shot>` | 不渲染,输出镜头的时长、帧数、舞台尺寸、硬切和 `meta` |
122
+ | `multiplane serve` | 在 127.0.0.1 上提供镜头,写镜头时用浏览器打开 |
123
+ | `multiplane benchmark` | 在本机实测不同进程数的渲染速度,之后渲染自动用最快的进程数 |
124
+ | `multiplane doctor` | 检查 Node、浏览器、GPU 光栅、FFmpeg、可选的 three.js |
125
+ | `multiplane status <job>` | 查看作业状态 |
126
+ | `multiplane cancel <job>` | 停止作业 |
127
+ | `multiplane wait <job>` | 等一个作业结束并输出它的事件,例如 agent 会话重启之后 |
128
+ | `multiplane config get\|set\|unset` | 读取或修改用户配置 |
129
+ | `multiplane skill status\|install\|remove` | 给 Claude Code 和 Codex 安装、更新、移除 agent skill |
130
+
131
+ 镜头路径相对资源根 `--root`(默认当前目录)。每次运行都有自己的作业目录 `out/jobs/<jobId>/`,不给 `--out` 时产物就写在里面(视频、`stills/`、`verify/`),多次运行互不覆盖;最终路径见结果里的 `artifacts`。`--out` 指定输出位置,目标已存在时会拒绝覆盖,加 `--overwrite` 才替换。`render`、`stills`、`verify` 启动前自动跑 `lint`,有 error 就停下,`--no-lint` 跳过。`npx multiplane <command> --help` 列出该命令的全部参数。
132
+
133
+ `render` 的常用参数:
134
+
135
+ | 参数 | 含义 |
136
+ |---|---|
137
+ | `--from` / `--to` | 只渲染一段(秒,不含结束时刻) |
138
+ | `--fps` | 覆盖镜头帧率 |
139
+ | `--scale` | 输出倍率,如 `2` 把 1920×1080 的镜头输出为 3840×2160 |
140
+ | `--draft` | 快速预览的预设:以不超过 0.5 的倍率截图再放大,不做运动模糊、不加颗粒。画质有意降低;显式给出的参数会覆盖它。`stills` 也支持 |
141
+ | `--render-scale`、`--upscale` | 以较低倍率截图再放大(`lanczos` / `neighbor`),不得高于 `--scale` |
142
+ | `--motion-blur`、`--shutter`、`--adaptive-blur` | 每帧时间采样数、快门占比、自适应采样 |
143
+ | `--quality` | 画质预设:`draft`(同 `--draft`)、`standard`(默认)、`high`(一般用不着,见下文)。镜头开了运动模糊时,按画面里局部移动的距离自动加采样,`standard` 最多 64、`high` 最多 256,见下文 |
144
+ | `--max-motion-blur` | 自定义自动加采样的上限,覆盖预设;设成和 `--motion-blur` 一样就是固定采样数 |
145
+ | `--grain` | 胶片颗粒强度 |
146
+ | `--no-post` | 关闭全部后期 |
147
+ | `--workers` | 并行浏览器进程数;默认用 `multiplane benchmark` 在本机实测的值,没测过时按 CPU、内存和分辨率估算,见下文“并发数” |
148
+ | `--raster` | `gpu`(默认)或 `software` |
149
+ | `--format`、`--alpha` | 输出格式 `mp4`(默认)、`webm`(VP9)或 `mov`(ProRes 4444);`--alpha` 保留透明(只限 WebM 和 MOV,`stills`、`verify` 也支持)。见下文“透明视频” |
150
+ | `--encoder`、`--max-bitrate` | 编码器、码率上限(Mbps) |
151
+ | `--audio`、`--audio-offset` | 混入一条音轨及其偏移(秒) |
152
+
153
+ 镜头里 `post` 声明的后期参数是默认值,命令行参数覆盖它。
154
+
155
+ ## 机器可读输出
156
+
157
+ 加 `--json` 后,stdout 每行一个 JSON 事件(NDJSON),最后一行一定是最终结果:
158
+
159
+ ```json
160
+ {"schema":"multiplane/1","type":"result","command":"render","ts":"2026-10-11T09:30:09.120Z","ok":true,"code":null,"message":null,"artifacts":[{"path":"/abs/out/jobs/20261011-093000-animation-3f2a/animation.mp4","kind":"video","bytes":81234}],"elapsedMs":3120,"jobId":"20261011-093000-animation-3f2a","data":{"frames":24}}
161
+ ```
162
+
163
+ - 失败时 `ok` 为 `false`,`code` 是稳定的错误码,`hint` 给修复建议;人类可读的信息走 stderr。
164
+ - 退出码:`0` 成功,`1` 镜头或检查不通过,`2` 参数错误,`3` 环境问题,`4` 渲染器内部错误,`130` 被取消。
165
+ - 每次运行在 `out/jobs/<jobId>/` 下写 `events.jsonl`(同一事件流)和 `status.json`(最新快照),其他程序可以据此展示进度;渲染时还会维护一张预览缩略图 `preview.png`。
166
+
167
+ 事件类型、字段和全部错误码见 [`docs/protocol.zh-CN.md`](https://github.com/LBEILC/Multiplane/blob/main/docs/protocol.zh-CN.md)。
168
+
169
+ ## 用户配置与作业索引
170
+
171
+ 渲染器在用户级目录里读配置、登记作业:Windows 为 `%USERPROFILE%\.multiplane`(0.2.0 及以前是 `%APPDATA%\multiplane`,首次使用时会把那里的文件复制过来),macOS 为 `~/Library/Application Support/multiplane`,Linux 为 `~/.config/multiplane`;环境变量 `MULTIPLANE_HOME` 可改。
172
+
173
+ - `config.json`:用户默认值,字段 `ffmpeg`、`raster`、`encoder`、`workers`、`jobsDir`,都可省略。优先级为命令行参数 > 环境变量 > `config.json` > 内置默认值。渲染时从不写它,由 `multiplane config` 命令或桌面软件写;`doctor` 和 `config get` 会报告它的位置和哪些字段生效。
174
+ - `jobs.jsonl`:每个作业开始、结束各追加一行,供其他程序发现各项目里的作业;作业状态以作业目录里的 `status.json` 为准。`MULTIPLANE_NO_INDEX=1` 关闭登记。
175
+
176
+ 这个目录读写失败不影响渲染,只会给出一条警告。格式见 [`docs/protocol.zh-CN.md`](https://github.com/LBEILC/Multiplane/blob/main/docs/protocol.zh-CN.md) 第 8 节。
177
+
178
+ ## JS API
179
+
180
+ ```js
181
+ import { inspectShot, lintShot, renderShot, stillsShot, verifyShot } from 'multiplane';
182
+
183
+ const result = await renderShot('examples/animation.html', {
184
+ workers: 2,
185
+ out: 'out/demo.mp4',
186
+ onEvent: event => { if (event.type === 'progress') console.error(event.done, event.total); },
187
+ });
188
+ ```
189
+
190
+ 返回值与 `result` 事件结构相同(不含 `schema`、`type`、`ts`);失败时抛出带 `code` 的 `MultiplaneError`。
191
+
192
+ ## 透明视频
193
+
194
+ 渲染背景透明的镜头,用来叠在别的画面上:
195
+
196
+ ```sh
197
+ npx multiplane render examples/alpha.html --format webm --alpha
198
+ npx multiplane render examples/alpha.html --format mov --alpha
199
+ npx multiplane stills examples/alpha.html --alpha --at 0,1
200
+ ```
201
+
202
+ 页面背景会变成透明;镜头自己画的东西,包括 `#stage` 的背景色,照样不透明。想要整层透明,就别给 `#stage` 设背景。镜头可以用 `post: { alpha: true }` 把透明设为默认。
203
+
204
+ - `webm` 用带 alpha 的 VP9。不少播放器会忽略 VP9 的 alpha;用 FFmpeg 检查时要加 `-c:v libvpx-vp9` 解码,默认的 VP9 解码器会丢掉 alpha。
205
+ - `mov` 用 ProRes 4444,大多数剪辑软件能读出 alpha。
206
+ - 运动模糊和缩放都按预乘颜色计算,移动的边缘不会出现暗边或色边。发布前渲染器会解码首帧和中间帧,确认 alpha 编码后仍在(每个像素误差不超过 2/255)。
207
+ - FFmpeg 构建需要带 `libvpx-vp9`(WebM)和 `prores_ks`(MOV),`doctor` 会报告这两项。
208
+
209
+ ## 画质与运动模糊
210
+
211
+ 运动模糊是每帧在快门时间内截多张图再平均。采样太少时,动得快的东西会变成一条条分开的残影;全片都用很多采样又太慢。Multiplane 按画面里局部移动的距离给每一帧定采样数:
212
+
213
+ - 镜头决定**要不要**运动模糊:`post.motionBlur` 大于 1 就开启,它的值是起步采样数;快门 `shutter` 和颗粒也由镜头定。
214
+ - `--quality` 决定**画质**:先按起步数截,把画面分成小块逐块比较相邻两个采样,哪一块移动超过约 2 像素(`standard`)或 1 像素(`high`)就把采样数加倍,最多到 64 或 256。静止的帧只用 1 个采样,慢的帧保持起步数,只有动得快的帧才多花时间。
215
+ - 迭代和最终成片都用默认的 `standard` 就够了。`high` 在真实镜头上要多花几倍时间,提升要放大才看得出,只在静帧要放大看细节、或画面里有极快极细的运动时才用;只看构图用 `--draft`。
216
+ - 想完全自己控制:`--max-motion-blur` 改上限,设成和 `--motion-blur` 一样就是固定采样数。镜头定义了 `blurAt(t)` 时按它的返回值来,不会自动加。
217
+ - 结果里的 `blurSamples` 列出每帧实际用了多少采样。
218
+
219
+ ## 并发数
220
+
221
+ `render` 和 `verify` 会同时开几个浏览器进程分担帧(`--workers`)。进程多了不一定更快:开 GPU 光栅时所有进程共用一块显卡,开得太多反而变慢。
222
+
223
+ - **在本机实测一次**:`multiplane benchmark`(约 2–3 分钟)用普通网页、Canvas、三维三类镜头,分别测 1、2、4、6、8、12、16、24 个进程的速度,每类记下最快速度 95% 以内的最小进程数。之后渲染时没给 `--workers`、配置里也没设,就按镜头类型自动用这个值。
224
+ - 测的时候内存不够,就只测到能开的进程数为止,用已经测到的点给出推荐值,`doctor` 会注明“受内存限制”。
225
+ - 没测过时用估算值:按 CPU 核数、空闲内存和输出分辨率算,最多 12 个。
226
+ - 换了电脑、显卡、浏览器或 Multiplane 版本后,`doctor` 会提示测试已过期,重跑一次即可。结果存在用户级目录的 `benchmark.json`。
227
+ - 结果里的 `resources.workersSource` 说明这次的进程数从哪来:`option`(命令行)、`config`(用户配置)、`benchmark`(本机实测)或 `estimate`(估算)。
228
+ - 进程数只影响速度,不影响画面:同一个镜头用几个进程渲染,像素都一样。
229
+
230
+ ## 静帧
231
+
232
+ `stills` 不只是抽帧检查用的。它输出的每张 PNG 和视频里同一帧走的是同一套后期,所以也可以拿来出最终交付的静帧:封面、海报、文章配图、社交平台的图片。
233
+
234
+ ```sh
235
+ # 在 0、1.5、3 秒各出一张
236
+ npx multiplane stills shot.html --at 0,1.5,3 --out out/stills
237
+
238
+ # 封面:2 倍超采样(1920×1080 的镜头输出 3840×2160),带运动模糊
239
+ npx multiplane stills shot.html --at 2.4 --scale 2 --motion-blur 16 --out out/cover
240
+
241
+ # 透明背景的 PNG,用来叠在别的画面上
242
+ npx multiplane stills shot.html --at 1 --alpha --out out/overlay
243
+
244
+ # 每 0.5 秒出一张,用来快速浏览整个镜头
245
+ npx multiplane stills shot.html --every 0.5
246
+ ```
247
+
248
+ - 文件名是 `t-<秒>.png`(例如 `t-2.40.png`),同一目录里还有一张所有静帧拼在一起的联系表 `_sheet.png`。
249
+ - `--quality`、`--scale`、`--motion-blur`、`--max-motion-blur`、`--shutter`、`--grain`、`--alpha` 都和 `render` 一样生效;三维层的景深也一样。镜头在 `post` 里声明的默认值同样适用。
250
+ - 想要某一刻画面里有动态模糊,时刻要选在运动当中;静止的时刻采样数会自动降到 1,画面和不开模糊一样清晰。
251
+ - 静帧要放大看细节时可以加 `--quality high`;`--draft` 只适合检查构图,不要用来出交付的图。
252
+
253
+ ## 三维层
254
+
255
+ ```sh
256
+ npm install three@0.180.0
257
+ ```
258
+
259
+ `multiplane/three` 在舞台里加一层透明 WebGL 画布,提供图片卡片、表面标注和累积景深,可以和普通 HTML 元素叠放。不用三维的镜头不会加载它。示例见 [`examples/three-cards.html`](examples/three-cards.html),用法见 skill 的“三维层”一节。
260
+
261
+ ## 桌面软件
262
+
263
+ `app/` 下是配套的桌面软件 Multiplane(Electron),给人看、不给 agent 用:
264
+
265
+ <picture>
266
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/LBEILC/Multiplane/main/docs/images/app-jobs-zh-dark.png">
267
+ <img src="https://raw.githubusercontent.com/LBEILC/Multiplane/main/docs/images/app-jobs-zh-light.png" width="800" alt="Multiplane 桌面软件的作业页:进行中的作业、排队中的作业和实时预览帧">
268
+ </picture>
269
+
270
+
271
+ - **作业**:列出各项目里的渲染、抽帧、检查作业,实时显示进度和预览帧,查看成片、联系表和检查不通过时的差异图,可以停止进行中的作业。
272
+ - **环境**:检查浏览器、GPU 光栅、FFmpeg 等是否就绪;给 Claude Code(`~/.claude/skills/`)和 Codex(`~/.agents/skills/`)安装、更新 Multiplane 的 skill,自己改过的不会被直接覆盖。
273
+ - **设置**:写入上文的用户配置 `config.json`。
274
+ - **托管渲染**(默认关闭):在设置里开启后,agent 发起的渲染改由软件执行,不再挂在 agent 的会话上,会话结束也会照常做完。一次只执行一个托管渲染,其余排队。agent 的命令和输出不变;会话重启后用 `multiplane wait <job>` 接上之前的作业。开启托管后,关闭窗口时软件留在系统托盘。见协议第 11 节。
275
+ - **更新**:启动时和之后每天检查一次 GitHub 上的新版本(可以关闭)。Windows 上在软件里下载并安装;macOS 和 Linux 上打开下载页。
276
+ - 界面有英文和简体中文,默认跟随系统语言,也可以在设置里切换。
277
+
278
+ 它只通过 [`docs/protocol.zh-CN.md`](https://github.com/LBEILC/Multiplane/blob/main/docs/protocol.zh-CN.md) 的文件和命令行与渲染器打交道,不是渲染的必需品:不装它,agent 照样可以用命令行渲染。
279
+
280
+ 从 [Releases 页面](https://github.com/LBEILC/Multiplane/releases) 下载安装包。安装包里带一份同版本的渲染器,不需要另装 Node.js。这份渲染器也可以当命令用:Windows 为安装目录下的 `multiplane.cmd`,macOS 为 `Multiplane.app/Contents/Resources/bin/multiplane`,Linux 为 `Multiplane-<版本>-x86_64.AppImage --multiplane-cli`。PATH 里找不到 `multiplane` 时,环境页会显示完整路径。FFmpeg 不随包提供(见上文),Chromium 在环境页首次使用时下载。
281
+
282
+ | 系统 | 安装包 | 首次打开(安装包没有代码签名) |
283
+ |---|---|---|
284
+ | Windows | NSIS 安装程序(`.exe`) | SmartScreen 会提示:点“更多信息”,再点“仍要运行”。 |
285
+ | macOS | `.dmg`,Apple 芯片和 Intel 各一个 | 首次打开会被拦下:右键点应用选“打开”,或在“系统设置 → 隐私与安全性”里放行。 |
286
+ | Linux | AppImage | 先加可执行权限(`chmod +x`)再运行;需要 FUSE。没有 FUSE 时,先运行一次 `--appimage-extract` 解压,以后运行解压出来的 `squashfs-root/AppRun`(不要用 `--appimage-extract-and-run` 同时开两个:后退出的那个会删掉共用的临时目录,让另一个出错)。在不支持非特权用户命名空间的系统上,AppImage 的启动入口会关闭 Chromium 沙箱来启动软件。 |
287
+
288
+ 在当前系统上构建安装包,或在开发时启动:
289
+
290
+ ```sh
291
+ npm --prefix app install
292
+ npm --prefix app run dist
293
+ npm --prefix app start
294
+ ```
295
+
296
+ `dist` 把安装包写到 `app/release/`。`npm --prefix app start` 用的是单独的开发目录(渲染器配置、作业索引、软件数据都不碰真实的),加 `-- --use-user-home` 才使用真实目录。
297
+
298
+ ## 示例
299
+
300
+ | 文件 | 内容 |
301
+ |---|---|
302
+ | [`examples/animation.html`](examples/animation.html) | 二维几何动画,中间一次硬切,演示运动模糊不跨切 |
303
+ | [`examples/typography.html`](examples/typography.html) | 本地字体排版(Roboto,OFL) |
304
+ | [`examples/three-cards.html`](examples/three-cards.html) | 三维卡片与累积景深 |
305
+
306
+ ## 确定性与限制
307
+
308
+ - 像素一致的保证限于**同一台机器、同一浏览器版本、同一字体和素材**。不同 GPU、驱动或浏览器版本之间可能有细小差异。
309
+ - `lint` 是静态检查,抓不到间接调用(如 `const now = Date.now; now()`)和状态累积,最终以 `verify` 为准。
310
+ - `verify` 是抽样检查:默认取时长 0%、25%、50%、75% 附近的帧和开头一秒的连续帧,通过不代表穷尽了所有时刻。
311
+ - 自适应运动模糊按画面变化估算采样数,是近似。
312
+ - 用 GPU 光栅(默认)时,同一帧在重载或换进程后可能有零星像素差一两级(显卡造成,肉眼看不出)。`verify` 会把它报告为 `gpu_raster_jitter` 警告,不算失败;软件光栅(`--raster software`)下仍要求逐像素一致。
313
+ - 画面没变的帧直接复用、不重新截图,并定期逐像素核验;用 `--no-static-reuse` 关闭后成品完全相同。含视频、iframe 或正在运行的 CSS 动画的镜头总是逐帧截图。见协议第 12 节。
314
+ - 渲染器和桌面软件在 Windows、macOS(Apple 芯片)、Linux(x64,Xvfb 下的 X11)三个平台的 CI 上跑同一套测试,全部通过。Windows 10/11 另在真机上验证过;macOS 和 Linux 还没在真机上试过,Wayland 没有测试。Linux 的 CI 没有 GPU,覆盖的是软件光栅;Linux 上的 GPU 光栅尚未测试。
315
+
316
+ ## 同类项目
317
+
318
+ HTML 加时间函数、无头浏览器、FFmpeg 这条路不是 Multiplane 首创。同类项目有 [HyperFrames](https://github.com/heygen-com/hyperframes)、[Remotion](https://www.remotion.dev/)、[Revideo](https://github.com/redotvideo/revideo)、[Motion Canvas](https://motioncanvas.io/) 等,各有侧重。Multiplane 的侧重是:镜头写成不依赖框架的普通网页,可验证的确定性,不跨硬切的运动模糊,以及三维层的累积景深。
319
+
320
+ ## 许可
321
+
322
+ [Apache-2.0](LICENSE)。第三方组件和示例字体的许可见 [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。FFmpeg 不随本包分发,其许可取决于你使用的构建。
@@ -0,0 +1,30 @@
1
+ # 第三方声明
2
+
3
+ Multiplane 使用以下依赖;它们保留各自的版权和许可,不因本项目的 Apache-2.0 许可而改变。
4
+
5
+ | 组件 | 本次锁定版本 | 许可与原文 | 用途 |
6
+ |---|---|---|---|
7
+ | [Playwright](https://github.com/microsoft/playwright) / playwright-core | 1.63.0 | Apache-2.0:[LICENSE](licenses/playwright-LICENSE)、[NOTICE](licenses/playwright-NOTICE)、[Playwright 第三方声明](licenses/playwright-ThirdPartyNotices.txt)、[core 第三方声明](licenses/playwright-core-ThirdPartyNotices.txt) | 安装和控制 Chromium |
8
+ | [sharp](https://github.com/lovell/sharp) | 0.35.5 | Apache-2.0:[LICENSE](licenses/sharp-LICENSE) | PNG 解码和缩放 |
9
+ | sharp Windows x64 原生依赖 | 0.35.5 | Apache-2.0 AND LGPL-3.0-or-later:[包许可](licenses/sharp-win32-x64-LICENSE)、[上游第三方组件声明](licenses/sharp-win32-x64-NOTICES.md)、[LGPLv3](licenses/LGPL-3.0.txt)、[GPLv3](licenses/GPL-3.0.txt) | 原生图像处理,包含 libvips 等组件 |
10
+ | [three.js](https://github.com/mrdoob/three.js) | 0.180.0 | MIT:[LICENSE](licenses/three-LICENSE) | 可选三维层 |
11
+ | @img/colour | 随 lockfile 固定 | MIT:[LICENSE](licenses/colour-LICENSE.md) | sharp 间接依赖 |
12
+ | detect-libc | 随 lockfile 固定 | Apache-2.0:[LICENSE](licenses/detect-libc-LICENSE) | sharp 平台检测 |
13
+ | semver | 随 lockfile 固定 | ISC:[LICENSE](licenses/semver-LICENSE) | 版本解析 |
14
+ | Roboto 可变字体 | Google Fonts 原始 TTF | SIL OFL 1.1:[OFL 原文](examples/assets/OFL.txt) | 文字示例 |
15
+ | [Acorn](https://github.com/acornjs/acorn) | 8.15.0 | MIT:[LICENSE](licenses/acorn-LICENSE) | JavaScript 静态语法与位置解析 |
16
+ | [parse5](https://github.com/inikulin/parse5) | 7.3.0 | MIT:[LICENSE](licenses/parse5-LICENSE) | HTML 元素、属性和内联源码位置解析 |
17
+ | [PostCSS](https://github.com/postcss/postcss) | 8.5.28 | MIT:[LICENSE](licenses/postcss-LICENSE) | CSS 规则与声明解析;禁用 source map 读取 |
18
+ | [postcss-value-parser](https://github.com/TrySound/postcss-value-parser) | 4.2.0 | MIT:[LICENSE](licenses/postcss-value-parser-LICENSE) | CSS 资源 URL 与字符串解析 |
19
+ | entities | 6.0.1 | BSD-2-Clause:[LICENSE](licenses/entities-LICENSE) | parse5 间接依赖,HTML 实体解码 |
20
+ | nanoid | 3.3.19 | MIT:[LICENSE](licenses/nanoid-LICENSE) | PostCSS 间接依赖 |
21
+ | picocolors | 1.1.1 | ISC:[LICENSE](licenses/picocolors-LICENSE) | PostCSS 间接依赖,诊断颜色工具 |
22
+ | source-map-js | 1.2.2 | BSD-3-Clause:[LICENSE](licenses/source-map-js-LICENSE) | PostCSS 间接依赖,source map 工具 |
23
+
24
+ Roboto 文件从 [Google Fonts 的 Roboto 目录](https://github.com/google/fonts/tree/main/ofl/roboto) 获取,文件名为 `Roboto[wdth,wght].ttf`,本仓库仅将文件名改为 `Roboto-Variable.ttf`,未修改字体内容。Copyright 2011 The Roboto Project Authors。许可原文随字体保存。
25
+
26
+ `examples/assets/card.svg` 为本项目自行绘制的几何图形,采用本项目 Apache-2.0 许可。
27
+
28
+ Chromium 由 Playwright 安装或复用本机已安装的对应版本,不放入 Multiplane 包。其自身声明随浏览器发行物提供。不同平台的 sharp 原生依赖由 npm 按平台安装,不重打包进 Multiplane;应保留对应包内的许可和声明。上表原生依赖副本对应当前验收的 Windows x64 平台;其第三方库清单和使用许可详见上游声明,源码与构建信息见 [sharp-libvips](https://github.com/lovell/sharp-libvips)。Playwright 的内嵌依赖另在其安装包内 `*.js.LICENSE` 文件中声明。
29
+
30
+ 本包不包含 FFmpeg 二进制,也不依赖 `ffmpeg-static`。使用方通过命令行、环境变量或 PATH 提供 FFmpeg;其具体构建的许可与功能以该发行物为准。
package/bin/multiplane ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import { renderShot, stillsShot, verifyShot, inspectShot, lintShot, doctor, statusJob, cancelJob } from '../src/index.mjs';
3
+ import { benchmark } from '../src/benchmark.mjs';
4
+ import { serve } from '../src/serve.mjs';
5
+ import { setup } from '../src/setup.mjs';
6
+ import { postArgs, scaleArgs } from '../src/post.mjs';
7
+ import { createEvents, version } from '../src/events.mjs';
8
+ import { asError, invalidOption } from '../src/errors.mjs';
9
+ import { waitJob } from '../src/jobs.mjs';
10
+ import { claimExecutor, resultExitCode } from '../src/hosted.mjs';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const args = process.argv.slice(2), json = args.includes('--json');
14
+ const common = [...postArgs, ...scaleArgs, 'static-reuse', 'no-static-reuse', 'alpha', 'fps', 'raster', 'root', 'job-dir', 'no-lint', 'overwrite', 'no-hosted', 'job-dir-exact', 'hosted-executor'];
15
+ const allowed = {
16
+ inspect: ['root', 'fps', 'no-lint'], serve: ['root', 'port'],
17
+ render: [...common, 'format', 'draft', 'out', 'from', 'to', 'workers', 'encoder', 'max-bitrate', 'audio', 'audio-offset', 'ffmpeg'],
18
+ stills: [...common, 'draft', 'out', 'at', 'every'], verify: [...common, 'draft', 'out', 'at', 'workers'],
19
+ benchmark: ['raster', 'max-workers'], config: [], skill: ['host', 'force', 'expect'],
20
+ lint: ['root'], doctor: ['ffmpeg', 'raster'], status: ['job-dir'], cancel: ['job-dir', 'wait'],
21
+ wait: ['job-dir', 'timeout'],
22
+ };
23
+ // Internal flags used by the desktop app to start hosted executors; not listed in help.
24
+ const internal = ['job-dir-exact', 'hosted-executor'];
25
+ const listed = flags => flags.filter(f => !internal.includes(f));
26
+ let reported = false;
27
+ const onEvent = event => {
28
+ if (event.type === 'result') reported = true;
29
+ if (json) process.stdout.write(JSON.stringify(event) + '\n');
30
+ else if (event.type === 'result') {
31
+ if (event.ok) console.log(JSON.stringify(event.data, null, 2));
32
+ else console.error(event.code + ': ' + event.message + (event.hint ? '\n' + event.hint : ''));
33
+ } else if (event.type === 'listening') console.log(event.url);
34
+ else if (['progress', 'warning', 'finding'].includes(event.type)) console.error(JSON.stringify(event));
35
+ };
36
+ let command = args[0] ?? 'doctor';
37
+ try {
38
+ const executor = ['render', 'stills', 'verify'].includes(command) && args.includes('--hosted-executor');
39
+ const exactIndex = args.indexOf('--job-dir-exact');
40
+ const lostClaim = executor && (exactIndex < 0 || !args[exactIndex + 1]
41
+ || !await claimExecutor(args[exactIndex + 1], command));
42
+ const onBytes = bytes => new Promise((resolve, reject) => process.stdout.write(bytes, error => error ? reject(error) : resolve()));
43
+ if (lostClaim) { process.exitCode = 0; }
44
+ else if (args.length === 1 && args[0] === '--version') console.log(version);
45
+ else if (args.length === 0 || args[0] === '--help') {
46
+ console.log('Usage: multiplane <render|stills|verify|inspect|lint> <shot.html> [--json] [--option value]\n'
47
+ + ' multiplane serve [--root directory] [--port number] [--json]\n'
48
+ + ' multiplane <status|cancel> <jobId|jobDir> [--json] [--wait seconds]\n'
49
+ + ' multiplane wait <jobId|jobDir> [--json] [--timeout seconds]\n'
50
+ + ' multiplane benchmark [--json] [--raster gpu|software] [--max-workers n]\n'
51
+ + ' multiplane doctor [--json] [--ffmpeg path]\n'
52
+ + ' multiplane config <get [key]|set key value|unset key> [--json]\n'
53
+ + ' multiplane skill <status|install|remove> [--host claude|codex|all] [--force] [--expect signature] [--json]\n'
54
+ + Object.entries(allowed).map(([name, flags]) => name + ': ' + listed(flags).map(f => '--' + f).join(' ')).join('\n'));
55
+ } else if (['config', 'skill'].includes(command)) {
56
+ if (args.includes('--help')) console.log(command === 'config'
57
+ ? 'Usage: multiplane config <get [key]|set key value|unset key> [--json]\nKeys: ffmpeg jobsDir raster encoder workers hosted'
58
+ : 'Usage: multiplane skill <status|install|remove> [--host claude|codex|all] [--force] [--expect signature] [--json]\nInstall and remove require --host.');
59
+ else await setup(command, args.slice(1), onEvent);
60
+ } else {
61
+ if (!allowed[command]) throw invalidOption('command', 'Unknown command: ' + command);
62
+ if (args.includes('--help')) {
63
+ console.log('Usage: multiplane ' + command + (['doctor', 'serve', 'benchmark'].includes(command) ? '' : ['status', 'cancel', 'wait'].includes(command) ? ' <jobId|jobDir>' : ' <shot.html>') + ' [options]\nOptions: --json --help ' + listed(allowed[command]).map(f => '--' + f + (['static-reuse', 'no-static-reuse', 'overwrite', 'no-lint', 'no-post', 'alpha', 'draft', 'no-hosted', 'hosted-executor'].includes(f) ? '' : ' <value>')).join(' '));
64
+ } else {
65
+ const needsTarget = !['doctor', 'serve', 'benchmark'].includes(command);
66
+ const target = needsTarget ? args[1] : undefined;
67
+ if (needsTarget && (!target || target.startsWith('--'))) throw invalidOption('target', 'Provide a shot or job path.');
68
+ const flags = args.slice(needsTarget ? 2 : 1), options = {};
69
+ for (let i = 0; i < flags.length;) {
70
+ const flag = flags[i], key = flag.replace(/^--/, '');
71
+ if (!flag.startsWith('--') || !(allowed[command].includes(key) || key === 'json') || key in options) throw invalidOption(flag, 'Invalid or duplicate option: ' + flag);
72
+ if (['static-reuse', 'no-static-reuse', 'json', 'alpha', 'no-post', 'overwrite', 'no-lint', 'draft', 'no-hosted', 'hosted-executor'].includes(key)) { options[key] = true; i++; }
73
+ else {
74
+ if (flags[i + 1] === undefined || flags[i + 1].startsWith('--')) throw invalidOption(flag, 'Missing option value: ' + flag);
75
+ options[key] = flags[i + 1]; i += 2;
76
+ }
77
+ }
78
+ delete options.json;
79
+ if (options['job-dir-exact'] && !executor) throw invalidOption('--job-dir-exact', '--job-dir-exact requires --hosted-executor.');
80
+ options.onEvent = executor ? undefined : onEvent;
81
+ if (['render', 'stills', 'verify'].includes(command) && !executor) options._hostedCLI = {
82
+ args: args.slice(1).filter(arg => arg !== '--json'), bin: fileURLToPath(import.meta.url), onBytes,
83
+ };
84
+ if (command === 'wait') {
85
+ const result = await waitJob(target, { ...options, onEvent: json ? undefined : onEvent, onBytes: json ? onBytes : undefined });
86
+ reported = true; process.exitCode = resultExitCode(result);
87
+ } else {
88
+ const action = { render: renderShot, stills: stillsShot, verify: verifyShot, inspect: inspectShot, serve, lint: lintShot, doctor, benchmark, status: statusJob, cancel: cancelJob }[command];
89
+ const result = needsTarget ? await action(target, options) : await action(options);
90
+ if (result?.ok !== undefined) { reported = true; process.exitCode = resultExitCode(result); }
91
+ }
92
+ }
93
+ }
94
+ } catch (failure) {
95
+ const error = asError(failure);
96
+ if (!reported) createEvents(command, null, { onEvent }).finish({}, error);
97
+ process.exitCode = error.exitCode;
98
+ }
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html lang="en"><meta charset="utf-8"><title>Alpha example</title>
3
+ <link rel="stylesheet" href="/multiplane/runtime/shot.css">
4
+ <style>
5
+ @font-face{font-family:Example;src:url('./assets/Roboto-Variable.ttf')}
6
+ #stage{width:320px;height:180px;background:transparent}
7
+ #block{position:absolute;top:65px;width:60px;height:60px;background:rgba(40,120,220,.5)}
8
+ #label{position:absolute;left:24px;top:20px;color:rgba(230,70,30,.8);font:28px Example,sans-serif}
9
+ </style>
10
+ <div id="stage"><div id="block"></div><div id="label">Alpha</div></div>
11
+ <script src="/multiplane/runtime/shot.js"></script>
12
+ <script>
13
+ // TODO(design): Review the neutral alpha example before publication.
14
+ const block = document.querySelector('#block');
15
+ Shot.define({duration:2,fps:12,fonts:[['28px Example','Alpha']],post:{alpha:true,motionBlur:5,shutter:0.8},
16
+ render(t){block.style.transform=`translateX(${24+90*t}px)`;}
17
+ });
18
+ </script></html>
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en"><meta charset="utf-8"><title>Geometry</title>
3
+ <link rel="stylesheet" href="/multiplane/runtime/shot.css">
4
+ <style>#stage{width:640px;height:360px}#shape{position:absolute;top:140px;width:80px;height:80px;background:#888}</style>
5
+ <div id="stage"><div id="shape"></div></div>
6
+ <script src="/multiplane/runtime/shot.js"></script>
7
+ <script>
8
+ // TODO(design): Review the neutral examples before publication.
9
+ const stage = document.querySelector('#stage'), shape = document.querySelector('#shape');
10
+ Shot.define({ duration: 2, fps: 12, cuts: [1], post: { motionBlur: 5, shutter: 0.8, grain: 0 },
11
+ render(t) {
12
+ stage.style.background = t < 1 ? '#eee' : '#222';
13
+ shape.style.borderRadius = t < 1 ? '0' : '50%';
14
+ shape.style.transform = `translateX(${40 + 480 * (t < 1 ? t : t - 1)}px)`;
15
+ }
16
+ });
17
+ </script></html>
@@ -0,0 +1,93 @@
1
+ Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://openfontlicense.org
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
2
+ <rect width="256" height="256" fill="#f4f4f4"/>
3
+ <rect x="32" y="32" width="80" height="80" fill="#444"/>
4
+ <circle cx="184" cy="72" r="40" fill="#999"/>
5
+ <path d="M32 152h192M32 184h192M32 216h128" stroke="#666" stroke-width="8"/>
6
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="400" height="500" viewBox="0 0 400 500"><rect width="400" height="500" fill="#f5f2e9"/><circle cx="200" cy="230" r="132" fill="#f2d067"/><circle cx="200" cy="230" r="90" fill="#2d5272"/><circle cx="200" cy="230" r="48" fill="#f5f2e9"/><path d="M28 28H372V472H28Z" fill="none" stroke="#26333a" stroke-width="3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="400" height="500" viewBox="0 0 400 500"><rect width="400" height="500" fill="#f5f2e9"/><path d="M55 360L200 95L345 360Z" fill="#cf694e"/><path d="M125 320L200 182L275 320Z" fill="#f4ead9"/><path d="M28 28H372V472H28Z" fill="none" stroke="#26333a" stroke-width="3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="400" height="500" viewBox="0 0 400 500"><rect width="400" height="500" fill="#f5f2e9"/><path d="M48 115H350V172H48ZM48 220H350V277H48ZM48 325H350V382H48Z" fill="#518573"/><circle cx="200" cy="250" r="82" fill="#e3bc65"/><path d="M28 28H372V472H28Z" fill="none" stroke="#26333a" stroke-width="3"/></svg>