宣传素材与生成管线
Ark UI Skill 内置一组由真实样例界面编排而成的社交宣传图(横版 1600×900 / 竖版 1080×1350 共 8 张 PNG),以及一条用 Node 驱动无头 Edge 浏览器把它们重新渲染出来的生成管线。本页覆盖这条管线的完整链路:可编辑源文件、参数化页面、无头截图脚本与产物清单。
Purpose and Scope
本页是宣传素材子系统的完整参考,覆盖:
assets/promo/下的三个可编辑源文件(promo.html/promo.js/promo.css)如何组成一个"按查询参数切换画面"的单页宣传画布;scripts/capture-promos.mjs如何用无头 Microsoft Edge 逐张导出 8 张 PNG;- 生成产物的命名、尺寸、对应场景,以及环境变量
ARK_UI_EDGE_BIN等运行配置。
以下内容有意留给兄弟页面:
- 样例界面本身的实现(五个风格族样例、四档深度控制),见 5-assets-and-examples 下的 showcase 相关页面;
- 样例截图是如何被采集出来的(桌面/移动视口、横向溢出校验),由
scripts/capture-showcases.mjs负责,见 showcase 截图采集相关页面; - README/SKILL 层面的整体资产目录说明。
Overview
宣传素材解决的问题是:用同一套真实证据(showcase 截图)讲清楚 Ark UI Skill 的三个核心卖点——五种风格族、四档应用深度、经过验证的响应式重构。因此它不是一个静态图片目录,而是一条可重复执行的管线:
- 素材来源:五个复杂档(complex)样例的桌面截图与移动截图,位于
assets/showcases/screenshots/; - 编排层:
assets/promo/promo.html定义四个宣传场景卡片(cover / families / depth / responsive),把上述截图作为"产品证据"嵌入版面; - 参数路由层:
assets/promo/promo.js读取 URL 查询参数scene与format,白名单校验后写入body.dataset; - 样式层:
assets/promo/promo.css通过body[data-scene="..."]选择器只显示一张卡片,并承载全部品牌视觉 token; - 生成脚本:
scripts/capture-promos.mjs串行 spawn 无头 Edge,注入 8 组"文件名 + 场景 + 比例 + 视口"任务,用--screenshot直接落盘。
设计意图(WHY):
- 单一可编辑源:文案与版式改动只需编辑 HTML/CSS,再跑一次脚本即可全量重生成,不需要打开图形软件手工改图;
- 证据真实性:宣传图里的界面全部来自真实渲染的样例截图,而非示意图——这与仓库"不复制受保护素材"的合规边界一致(SKILL.md 明确禁止复制官方 logo、角色立绘、宣传图);
- 无 JS 依赖的降级:
promo.html的<body>带有默认data-scene="cover" data-format="landscape",即使脚本尚未执行,封面卡片也已按 CSS 默认显示。
Architecture
架构说明:
capture-promos.mjs是唯一的编排者。它不解析页面内容,也不做图像后处理——所有视觉决策都在 HTML/CSS 里,脚本只负责"给定 URL + 视口 → 落一张 PNG"。这使管线没有除 Edge 之外的任何原生依赖(无 puppeteer、无 sharp)。promo.html是数据源 + 模板的合体。四个<section class="promo-card scene-*">同时存在于 DOM 中,靠 CSSdisplay切换,而不是靠 JS 重建 DOM。因此同一份 HTML 能服务 4 个场景 × 2 种比例 = 8 张图。promo.js只做路由,不做渲染。14 行脚本完成参数白名单校验、dataset 写入与字体就绪标记,渲染逻辑全部下沉到 CSS,避免截图时序与 JS 执行时序耦合。- 产物是构建结果。
assets/promo/output/中的 PNG 与源文件并列存放,README 直接引用其中的封面图作为项目门面。
Core Flow:一次生成任务的端到端执行
关键步骤逐条解读(均对应 scripts/capture-promos.mjs 实际代码):
- 目录自举(第 41 行):
mkdir(outputDirectory, { recursive: true })保证assets/promo/output/存在,因此脚本从任意 CWD 运行都不会因目录缺失而失败;outputDirectory是从import.meta.url反推出来的(第 8–11 行),与 CWD 完全解耦。 - 任务表驱动(第 15–24 行):
jobs数组把"产物文件名 ↔ 场景 ↔ 比例 ↔ 视口尺寸"四元组固化。新增一张宣传图只需在此追加一行,不需要改任何渲染代码。 - URL 参数注入(第 43–45 行):对
file://形式的promo.html追加scene与format查询参数。用pathToFileURL而非手工拼字符串,正确处理了路径中的空格等字符(例如默认 Edge 安装路径)。 - 串行执行(
for...of+await runEdge):8 次截图严格串行。这是有意为之——避免多个 Edge 实例同时争抢--virtual-time-budget计时与字体缓存,也让日志按文件名顺序输出,便于肉眼核对。 - 退出码即校验(第 33–37 行):
runEdge监听stderr与exit,非 0 退出码直接reject,最终由main().catch打印堆栈并置process.exitCode = 1。管线没有像素级 diff 校验,失败语义是"进程级失败"(例如 Edge 路径错误、找不到页面)。
数据模型与产物清单
管线没有数据库或持久化实体,"数据模型"体现为两个层面的静态结构:
产物表(assets/promo/output/)
| 文件名 | scene | format | 视口尺寸 | 内容主题 |
|---|---|---|---|---|
01-cover-landscape.png | cover | landscape | 1600×900 | 主封面:五风格族 × 四深度 × 响应式卖点,三张样例屏透视叠放 |
02-families-landscape.png | families | landscape | 1600×900 | 五个风格族磁贴,逐一列出配色关键词 |
03-depth-landscape.png | depth | landscape | 1600×900 | 四档深度卡 + 复杂档 Endfield 参考 |
04-responsive-landscape.png | responsive | landscape | 1600×900 | 桌面 1440×900 与三台移动设备对照 |
05-cover-portrait.png | cover | portrait | 1080×1350 | 同 01,竖版构图 |
06-families-portrait.png | families | portrait | 1080×1350 | 同 02,竖版构图 |
07-depth-portrait.png | depth | portrait | 1080×1350 | 同 03,竖版构图 |
08-responsive-portrait.png | responsive | portrait | 1080×1350 | 同 04,竖版构图 |
场景与参数的合法取值(promo.js 中的白名单)
| 参数 | 合法值 | 非法/缺省时的回退 |
|---|---|---|
scene | cover / families / depth / responsive | 回退为 cover |
format | landscape / portrait | 回退为 landscape |
这张白名单表就是 promo.js 的全部业务逻辑——它同时是 URL 校验器和 CSS 场景选择器之间的契约。
Usage Examples
场景路由脚本(promo.js 全文)
参数白名单 + dataset 写入 + 字体就绪标记,是整条管线的"路由核心":
1const params = new URLSearchParams(location.search);
2const allowedScenes = ['cover', 'families', 'depth', 'responsive'];
3const allowedFormats = ['landscape', 'portrait'];
4
5const scene = allowedScenes.includes(params.get('scene')) ? params.get('scene') : 'cover';
6const format = allowedFormats.includes(params.get('format')) ? params.get('format') : 'landscape';
7
8document.body.dataset.scene = scene;
9document.body.dataset.format = format;
10document.title = `Ark UI Promo / ${scene} / ${format}`;
11document.fonts.ready.then(() => {
12 document.body.dataset.ready = 'true';
13});Source: promo.js
要点:
- 防御式解析:
includes(...) ? ... : 'cover'保证任何非法查询串都得到可渲染的画面,截图永远不会得到空白页; document.title被同步改写,便于在浏览器调试时快速确认当前 scene/format 组合;document.fonts.ready之后才置dataset.ready = 'true'——配合截图脚本的--virtual-time-budget=1200,确保字体加载完成后再定格。
场景切换的 CSS 机制
1.promo-card { position: relative; display: none; isolation: isolate; overflow: hidden; }
2body[data-scene="cover"] .scene-cover,
3body[data-scene="families"] .scene-families,
4body[data-scene="depth"] .scene-depth,
5body[data-scene="responsive"] .scene-responsive { display: block; }Source: promo.css
这是"一个 HTML 出八张图"的关键:所有场景卡片常驻 DOM,display: none 默认隐藏,只有匹配 body[data-scene] 的那张变成 display: block。isolation: isolate 建立层叠上下文,保证每个场景的 ::before 装饰层(如封面右侧的黄色同心圆)不会互相串染。
任务表与无头截图调用
1const jobs = [
2 ['01-cover-landscape.png', 'cover', 'landscape', 1600, 900],
3 ['02-families-landscape.png', 'families', 'landscape', 1600, 900],
4 ['03-depth-landscape.png', 'depth', 'landscape', 1600, 900],
5 ['04-responsive-landscape.png', 'responsive', 'landscape', 1600, 900],
6 ['05-cover-portrait.png', 'cover', 'portrait', 1080, 1350],
7 ['06-families-portrait.png', 'families', 'portrait', 1080, 1350],
8 ['07-depth-portrait.png', 'depth', 'portrait', 1080, 1350],
9 ['08-responsive-portrait.png', 'responsive', 'portrait', 1080, 1350],
10];Source: capture-promos.mjs
以及真正把页面变成 PNG 的那段:
1await runEdge([
2 '--headless=new',
3 '--disable-gpu',
4 '--hide-scrollbars',
5 '--run-all-compositor-stages-before-draw',
6 '--virtual-time-budget=1200',
7 `--window-size=${width},${height}`,
8 `--screenshot=${outputPath}`,
9 url.href,
10]);Source: capture-promos.mjs
每个开关都有明确目的:
| 参数 | 作用 | 为何需要 |
|---|---|---|
--headless=new | 新版无头模式 | 与有头渲染引擎一致,避免旧无头模式的排版差异 |
--disable-gpu | 关闭 GPU 加速 | 无头环境下的稳定性考虑,避免驱动差异 |
--hide-scrollbars | 隐藏滚动条 | html, body { overflow: hidden } 已限制溢出,但保险起见仍关闭 |
--run-all-compositor-stages-before-draw | 先完成合成阶段再绘制 | 确保层叠/透明度/滤镜全部就绪 |
--virtual-time-budget=1200 | 虚拟时间预算 1200ms | 给 document.fonts.ready 与 <img> 加载留时间,替代 sleep |
--window-size=W,H | 视口尺寸 | 横版 1600×900、竖版 1080×1350 的来源 |
--screenshot=path | 直接写文件 | 免去 CDP 截图协议,无需任何 Node 端图像库 |
产物被 README 直接引用
1
2
3
4
5完整素材位于 `assets/promo/output/`,可编辑源文件位于 `assets/promo/`。重新生成全部八张图片:
6
7node "$CODEX_HOME/skills/ark-ui/scripts/capture-promos.mjs"Source: README.md
注意这里的调用路径是 $CODEX_HOME/skills/ark-ui/scripts/capture-promos.mjs——脚本按"skill 安装位置"编写,但在本仓库内直接以 node scripts/capture-promos.mjs 运行同样有效,因为所有路径都从 import.meta.url 推导。
Configuration Options
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ARK_UI_EDGE_BIN | 环境变量 | /Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge | 指定 Microsoft Edge 可执行文件路径;非 macOS 或非默认安装位置必须显式覆盖 |
scene(URL 查询参数) | string | cover(回退值) | 决定显示哪张 promo-card,取值见场景白名单 |
format(URL 查询参数) | string | landscape(回退值) | 画幅比例标记,写入 body.dataset.format |
--virtual-time-budget | 内置常量 | 1200(毫秒) | 截图前等待字体与图片的虚拟时间上限,写死在脚本中 |
| 视口尺寸 | 内置常量 | 横版 1600×900,竖版 1080×1350 | 与 jobs 表一一对应,改尺寸需同步改任务表 |
设计意图:唯一的运行时可变项是 ARK_UI_EDGE_BIN,其余全部固化为常量——这是一条"确定性管线",目标是每次运行产出字节级一致(或视觉一致)的 8 张图,而不是可配置的通用截图工具。通用样例截图采集由 capture-showcases.mjs 承担(不在本页范围)。
API Reference
本子系统没有导出的公共 API;下面列出两个"事实上被外部调用"的入口。
node scripts/capture-promos.mjs
行为:串行调用无头 Edge,按 jobs 表生成 8 张 PNG 到 assets/promo/output/。
参数:无命令行参数。所有输入来自脚本内常量与环境变量 ARK_UI_EDGE_BIN。
输出:stdout 打印每张图的 文件名 + 尺寸(fileName.padEnd(32) 对齐),stderr 仅在失败时由 Edge 写入并被脚本收集。
退出码:成功 0;任一 Edge 进程非 0 退出或 spawn 失败时 1(process.exitCode = 1),并打印 error.stack || error.message。
runEdge(args): Promise<string>
脚本内部唯一的可复用函数(未导出)。
参数:args(string[])——传给 Edge 的完整参数数组。
返回:Promise,resolve 值为收集到的 stderr 字符串(即使成功也可能非空)。
抛出:child.on('error', reject)——spawn 本身失败(典型情况:ARK_UI_EDGE_BIN 指向不存在的路径);退出码非 0 时 reject Edge exited with code N: <stderr>。
promo.js 的隐式接口
| 输入 | 输出(副作用) |
|---|---|
?scene=X&format=Y(X/Y 均在白名单内) | body.dataset.scene=X、body.dataset.format=Y、标题同步更新 |
| 任意非法或缺失参数 | 回退为 cover / landscape,页面仍可渲染 |
| 字体加载完成 | body.dataset.ready = 'true' |
Failure Modes, Edge Cases & Concurrency
| 场景 | 现象 | 处理方式 | 源码依据 |
|---|---|---|---|
| Edge 未安装 / 路径错误 | spawn 立即失败 | child.on('error', reject) → main().catch 打印堆栈,退出码 1 | runEdge 第 32 行 |
| Edge 进程崩溃 | 退出码非 0 | 收集 stderr 后 reject Edge exited with code N: ... | 第 33–37 行 |
| 非法查询参数 | 可能渲染空白/错误场景 | promo.js 白名单回退到 cover/landscape,截图永不空白 | promo.js 第 5–6 行 |
| 字体未加载完就截图 | 文字使用回退字体 | document.fonts.ready + --virtual-time-budget=1200 双保险 | promo.js 第 11 行、脚本第 52 行 |
| showcase 截图缺失 | 图片破图 | 管线不校验——HTML 直接引用 ../showcases/screenshots/*-complex.png,需先运行 capture-showcases.mjs | promo.html 第 29–31 行等 |
| 并发运行多个实例 | 字体缓存/虚拟时间计时竞争 | 脚本内串行 for...of + await,无并行;也未提供并发开关(有意保守) | 第 42–58 行 |
| 产物目录缺失 | 写文件失败 | mkdir(..., { recursive: true }) 预先自举 | 第 41 行 |
| 内容溢出视口 | 被裁切 | html, body { overflow: hidden } + --hide-scrollbars;宣传画布按精确视口设计,无溢出检测逻辑(与 capture-showcases.mjs 的"fail on horizontal overflow"不同) | promo.css 第 18 行 |
边界条件要点:
- 顺序依赖:宣传管线依赖 showcase 截图先存在。这是一个隐式前置条件,管线本身不校验、不自动触发——若需要从零重建,应先跑
capture-showcases.mjs再跑本脚本。 - 横向溢出不校验:本脚本与 showcase 采集脚本的职责差异是刻意的——宣传画布是固定视口的"海报",其布局在 CSS 中绝对定位(如
.cover-copy { left: 4.5rem; top: 10.8rem; width: 43% }),不存在需要验证的流式溢出。 - 可重入:脚本每次全量覆盖
output/下的同名文件,无增量/缓存机制,因此总是幂等的。
Performance & Operational Notes
- 成本模型:8 次串行 Edge 冷启动 ≈ 8 × (进程启动 + 虚拟时间 1200ms + 图片解码)。对一次性再生成任务完全可接受;若未来扩展到几十张图,可考虑复用单个 Edge 实例(
--remote-debugging-port+ CDP),但这与当前"零外部依赖"的设计取舍相悖。 - 确定性:
--disable-gpu、--run-all-compositor-stages-before-draw、--virtual-time-budget三者共同把渲染时间线压平,降低机器间差异;这是把"截图"当作"构建产物"而非"随手抓图"的关键工程化决策。 - 跨平台:默认
ARK_UI_EDGE_BIN指向 macOS 安装路径。Linux/CI 环境需显式设置(例如指向 chromium 二进制或 Edge 的 Linux 包)。脚本未对 Windows 的.exe做任何特判。 - 产物体积:8 张 1600×900 / 1080×1350 的 PNG 全部入库,README 引用其中两张作为门面。重生成会改变 PNG 的字节内容(压缩时间戳等元数据可能变化),review 时应注意只提交必要的重生成。
Extension Points
- 新增一张宣传图:三步即可——(1) 在
promo.html增加<section class="promo-card scene-<name>">;(2) 在promo.css增加body[data-scene="<name>"] .scene-<name> { display: block }与该场景样式;(3) 在capture-promos.mjs的jobs表追加一行。promo.js只需把新场景名加进allowedScenes。 - 新增一种画幅:
format已被参数化(allowedFormats+body.dataset.format),CSS 中可按body[data-format="portrait"]写差异规则;脚本侧仅需在jobs中增加对应视口尺寸行。 - 更换浏览器:
runEdge(args)是一个薄封装,任何支持--headless --screenshot标志集的 Chromium 系浏览器都可通过ARK_UI_EDGE_BIN接入,无需改代码。 - 文案本地化:
promo.html中标题/副标题为中文(不只是一张皮肤、五种风格,不是五套配色等),装饰性英文短语作为排版元素保留;本地化只需改 HTML 文本节点,不影响管线。
Tests
仓库中没有针对宣传管线的自动化测试(无单测、无像素 diff)。质量保障依赖三条间接机制,均可在源码中找到证据:
- 渲染白名单回退(
promo.js)保证参数化入口永远产出可渲染页面; --virtual-time-budget+fonts.ready保证截取时机稳定;- README 中的预览引用(第 28–30 行)让每次浏览 README 即成为对产物存在性的冒烟检查。
Related Links
- README.md 宣传素材章节 — 产物用途与再生成命令
- SKILL.md 资产目录 —
assets/promo/与scripts/capture-promos.mjs的官方定位 - promo.html — 四个场景卡片的完整结构与文案
- promo.css — 品牌 token、场景切换与封面/风格族样式
- capture-promos.mjs — 生成管线完整实现
- 样例界面与截图采集(showcase、
capture-showcases.mjs)属于兄弟页面主题,本页仅引用其产物路径assets/showcases/screenshots/