launch huashu-design v1.0: 9 demos + marketing-first README

- 新增 9 个能力 demo(workflow + capability),每个 MP4 + GIF + 60fps
- README 重写:营销 hook 开头 · 功能优先结构 · 自然嵌入 GIF
- 修复 render-video.js Google Fonts networkidle 超时
- 修复 c1-ios-prototype 刘海/Home Indicator 背景色
- 补充 references/editable-pptx.md 和 scripts/export_deck_stage_pdf.mjs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
alchainandClaude Opus 4.7 committed 2026-04-20 18:19:48 +08:00
1 parent f3692aac10
commit b127f8baa3
24 files changed
+8206 -281

No files matched your search

+212 -33
View File
@@ -1,8 +1,68 @@
# Slide Decks:HTML幻灯片制作规范
做幻灯片是设计工作的高频场景。这份文档说明怎么做好HTML幻灯片。
做幻灯片是设计工作的高频场景。这份文档说明怎么做好HTML幻灯片——从架构选型、单页设计,到 PDF/PPTX 导出的完整路径。
**和huashu-slides skill的区别**:huashu-slides focus on「AI演示文稿全流程」(含PPT导出);本文档focus on「用HTML本身作为呈现媒介」的设计方法论。两者可以配合使用——用这里的方法做高质量的HTML deck,再用huashu-slides导出成PPTX。
**本 skill 的能力覆盖**:
- HTML 播放/PDF 导出 → 本文档 + `scripts/export_deck_pdf.mjs` / `scripts/export_deck_stage_pdf.mjs`
- 可编辑 PPTX 导出 → `references/editable-pptx.md` + `scripts/html2pptx.js` + `scripts/export_deck_pptx.mjs --mode editable`
- 图片铺底 PPTX(不可编辑但视觉保真)→ `scripts/export_deck_pptx.mjs --mode image`
---
## 🛑 开工前先确认交付格式(最硬的 checkpoint)
**这个决策比「单文件还是多文件」更先。** 2026-04-20 期权私董会项目实测:**不在动手前确认交付格式 = 2-3 小时返工。**
### 决策树
```
│ 问:最终要交付什么?
├── 只要浏览器全屏演讲 / 本地 HTML → 视觉最自由,随便做
├── 要 PDF(打印 / 发群 / 存档) → 视觉最自由,任何架构都能导出
└── 要可编辑 PPTX(同事会改文字) → 🛑 从第一行 HTML 开始就按 `references/editable-pptx.md` 的 4 条硬约束写
```
### 为什么「要 PPTX 就得从头走 Path A」
PPTX 可编辑的前提是 `html2pptx.js` 能把 DOM 逐元素翻译为 PowerPoint 对象。它需要 **4 条硬约束**:
1. body 固定 720pt × 405pt(不是 1920×1080px)
2. 所有文字包在 `<p>`/`<h1>`-`<h6>` 里(禁止 div 直接放文字,禁止用 `<span>` 承载主文字)
3. `<p>`/`<h*>` 自身不能有 background/border/shadow(放外层 div)
4. `<div>` 不能用 `background-image`(用 `<img>` 标签)
5. 不用 CSS gradient、不用 web component、不用复杂 SVG 装饰
**本 skill 默认的 HTML 视觉自由度高**——大量 span、嵌套 flex、复杂 SVG、web component(如 `<deck-stage>`)、CSS 渐变——**几乎没有一条能天然过 html2pptx 的约束**(实测视觉驱动的 HTML 直接上 html2pptx,pass 率 < 30%)。
### 两条真实路径的代价对比(2026-04-20 真实踩坑)
| 路径 | 做法 | 结果 | 代价 |
|------|------|------|------|
| ❌ **先自由写 HTML,事后补救 PPTX** | 单文件 deck-stage + 大量 SVG/span 装饰 | 要可编辑 PPTX 只剩两条路:<br>A. 手写 pptxgenjs 几百行 hardcode 坐标<br>B. 重写 17 页 HTML 成 Path A 格式 | 2-3 小时返工,且手写版**维护成本永续**(HTML 改一个字,PPTX 要再人肉同步) |
| ✅ **从第一步按 Path A 约束写** | 每页独立 HTML + 4 条硬约束 + 720×405pt | 一条命令导出 100% 可编辑 PPTX,同时也能浏览器全屏演讲(Path A HTML 就是浏览器可播放的标准 HTML) | 写 HTML 时多花 5 分钟想「文字怎么包进 `<p>`」,零返工 |
### 开工话术(抄走即用)
> 动手之前先确认交付格式:
> - **浏览器演讲 / PDF** → 我按设计自由度最大的方式做(可以用动画、web component、复杂 SVG、CSS 渐变)
> - **需要可编辑 PPTX**(同事会改文字) → 我必须从一开始就按 `references/editable-pptx.md` 的 4 条硬约束写 HTML。视觉能力会少一些(无渐变、无 web component、无复杂 SVG),但导出就是一条命令的事
>
> 你选哪条?
### 混合交付怎么办
用户说「我要 HTML 演讲 **和** 可编辑 PPTX」——**这不是混合**,是 PPTX 需求覆盖 HTML 需求。按 Path A 写出来的 HTML 本身就能浏览器全屏演讲(加个 `deck_index.html` 拼接器就行)。**没有额外代价。**
用户说「我要 PPTX **和** 动画 / web component」——**这是真矛盾**。告诉用户:要可编辑 PPTX 就得牺牲这些视觉能力。让他做取舍,不要偷偷做手写 pptxgenjs 方案(会变成永续维护债)。
### 事后才知道要 PPTX 怎么办(紧急补救)
极个别情况:HTML 已经写好了才发现要 PPTX。此时两个选项都不完美:
1. **图片铺底 PPTX**(`scripts/export_deck_pptx.mjs` image 模式)——视觉 100% 保真但文字不可编辑。适合「演讲用 PPT 播放、不改内容」场景
2. **手写 pptxgenjs 重建**(为每页手写 addText/addShape + 图形 PNG 嵌入)——文字可编辑,但位置、字体、对齐都要手调,维护成本高。**只有用户明确接受「HTML 源要改就得重新手调 PPTX」才走这条**
永远优先把选择告诉用户,让他决定。**永远不要第一反应就开始手写 pptxgenjs**——那是最后的兜底手段。
---
@@ -142,49 +202,113 @@ Playwright 截图也是直接 `goto(file://.../slides/05-personas.html)`,不
### 基本用法
1. 从 `assets/deck_stage.js` 读取内容,嵌入 HTML 的 `<script>`(或 `<script src="deck_stage.js">`)
2. 在 body 里用 `<deck-stage>` 包 slide:
2. 在 body 里用 `<deck-stage>` 包 slide
3. 🛑 **script 标签必须放在 `</deck-stage>` 之后**(见下方硬约束)
```html
<deck-stage>
<section>
<h1>Slide 1</h1>
</section>
<section>
<h1>Slide 2</h1>
</section>
</deck-stage>
<body>
<deck-stage>
<section>
<h1>Slide 1</h1>
</section>
<section>
<h1>Slide 2</h1>
</section>
</deck-stage>
<!-- ✅ 正确:script 在 deck-stage 之后 -->
<script src="deck_stage.js"></script>
</body>
```
### 🛑 Script 位置硬约束(2026-04-20 真实踩坑)
**不能把 `<script src="deck_stage.js">` 放在 `<head>` 里。** 即使它在 `<head>` 里能定义 `customElements`,parser 在解析到 `<deck-stage>` 开始标签时就会触发 `connectedCallback`——此时子 `<section>` 还没被 parse,`_collectSlides()` 拿到空数组,counter 显示 `1 / 0`,所有页同时叠加渲染。
**三条合规写法**(任选其一):
```html
<!-- ✅ 最推荐:script 在 </deck-stage> 之后 -->
</deck-stage>
<script src="deck_stage.js"></script>
<!-- ✅ 也可:script 在 head 但加 defer -->
<head><script src="deck_stage.js" defer></script></head>
<!-- ✅ 也可:module 脚本天然 defer -->
<head><script src="deck_stage.js" type="module"></script></head>
```
`deck_stage.js` 本身已内置 `DOMContentLoaded` 延迟收集防御,即使 script 放 head 也不会彻底炸掉——但 `defer` 或放 body 底部仍然是更干净的做法,避免依赖防御分支。
### ⚠️ 单文件架构的 CSS 陷阱(务必阅读)
单文件架构最常见的坑——**`display` 属性被单页样式偷走**。
如果你给某张 slide 的样式写了 `display: flex/grid`:
常见错误姿势 1(直接写 display: flex 到 section):
```css
.emotion-slide { display: grid; } /* 特异性: 10 */
```
它会压过 deck 级的「隐藏非 active 页」规则:
```css
deck-stage > section { display: none; } /* 特异性: 2 */
```
结果所有 slide 同时渲染叠加。
**修复模式**(写 deck 时必做):
```css
/* ✅ 用 :not(.active) + !important 锁死「非激活即隐藏」 */
deck-stage > section:not(.active) {
display: none !important;
/* ❌ 外部 CSS 特异性 2,覆盖了 shadow DOM 的 ::slotted(section){display:none}(也是 2)*/
deck-stage > section {
display: flex; /* 所有页会同时叠加渲染! */
flex-direction: column;
padding: 80px;
...
}
```
特异性+权重双保险,单页 layout class 的 `display: grid/flex` 就不会影响可见性。
常见错误姿势 2(section 有特异性更高的 class):
另一个替代方案:**把单页的 flex/grid 写到内部 wrapper div 上**,section 本身只管 `display: block/none`。
```css
.emotion-slide { display: grid; } /* 特异性: 10,更糟 */
```
两种都会让 **所有 slide 同时叠加渲染**——counter 可能显示 `1 / 10` 假装正常,但视觉上第一页盖着第二页盖着第三页。
### ✅ Starter CSS(开工直接 copy,不踩坑)
**section 自身**只管「可见/不可见」;**layout(flex/grid 等)写到 `.active` 上**:
```css
/* section 只定义非 display 的通用样式 */
deck-stage > section {
background: var(--paper);
padding: 80px 120px;
overflow: hidden;
position: relative;
/* ⚠️ 不要在这里写 display! */
}
/* 锁死「非激活即隐藏」——特异性+权重双保险 */
deck-stage > section:not(.active) {
display: none !important;
}
/* 激活页才写需要的 display + layout */
deck-stage > section.active {
display: flex;
flex-direction: column;
justify-content: center;
}
/* 打印模式:所有页都要显示,覆盖 :not(.active) */
@media print {
deck-stage > section { display: flex !important; }
deck-stage > section:not(.active) { display: flex !important; }
}
```
替代方案:**把单页的 flex/grid 写到内部 wrapper `<div>` 上**,section 本身永远只是 `display: block/none` 的切换器。这是最干净的做法:
```html
<deck-stage>
<section>
<div class="slide-content flex-layout">...</div>
</section>
</deck-stage>
```
### 自定义尺寸
@@ -316,7 +440,7 @@ Deck 需要 **intentional variety**:
HTML 优先是第一公民。但用户经常需要 PPTX/PDF 交付。提供两个通用脚本,**任何多文件 deck 都能用**,位于 `scripts/` 下:
### `export_deck_pdf.mjs` — 导出矢量 PDF(推荐)
### `export_deck_pdf.mjs` — 导出矢量 PDF(多文件架构)
```bash
node scripts/export_deck_pdf.mjs --slides <slides-dir> --out deck.pdf
@@ -332,6 +456,61 @@ node scripts/export_deck_pdf.mjs --slides <slides-dir> --out deck.pdf
**限制**:PDF 不能再编辑文字——要改回到 HTML 改。
### `export_deck_stage_pdf.mjs` — 单文件 deck-stage 架构专用 ⚠️
**什么时候用**:deck 是单 HTML 文件 + `<deck-stage>` web component 包裹 N 个 `<section>`(即路径 B 架构)。此时 `export_deck_pdf.mjs` 那套「每个 HTML 一次 `page.pdf()`」走不通,需要走这个专用脚本。
```bash
node scripts/export_deck_stage_pdf.mjs --html deck.html --out deck.pdf
```
**为什么不能复用 export_deck_pdf.mjs**(2026-04-20 真实踩坑记录):
1. **Shadow DOM 赢过 `!important`**:deck-stage 的 shadow CSS 里有 `::slotted(section) { display: none }`(只 active 的那张 `display: block`)。即使在 light DOM 用 `@media print { deck-stage > section { display: block !important } }` 也压不住——`page.pdf()` 触发 print 媒体后 Chromium 最终渲染只有 active 那一张,结果**整个 PDF 只有 1 页**(当前 active slide 的重复)。
2. **循环 goto 每页还是只出 1 页**:直觉解法「对每个 `#slide-N` navigate 一次再 `page.pdf({pageRanges:'1'})`」也失败——因为 print CSS 在 shadow DOM 之外也有 `deck-stage > section { display: block }` 规则被 override 后,最终渲染永远是 section 列表的第一个(不是你 navigate 到的那一页)。结果 17 次循环得到 17 张 P01 封面。
3. **absolute 子元素跑到下一页**:即使成功让所有 section 渲染出来,section 本身若 `position: static`,其 absolute 定位的 `cover-footer`/`slide-footer` 会相对 initial containing block 定位——当 section 被 print 强制为 1080px 高度,absolute footer 可能被推到下一页(表现为 PDF 比 section 数量多 1 页,多出来的那页只含 footer 孤儿)。
**修复策略**(脚本已实现):
```js
// 打开 HTML 后,用 page.evaluate 把 section 从 deck-stage slot 中提出来,
// 直接挂到 body 下一个普通 div 里,并内联 style 确保 position:relative + 固定尺寸
await page.evaluate(() => {
const stage = document.querySelector('deck-stage');
const sections = Array.from(stage.querySelectorAll(':scope > section'));
document.head.appendChild(Object.assign(document.createElement('style'), {
textContent: `
@page { size: 1920px 1080px; margin: 0; }
html, body { margin: 0 !important; padding: 0 !important; }
deck-stage { display: none !important; }
`,
}));
const container = document.createElement('div');
sections.forEach(s => {
s.style.cssText = 'width:1920px!important;height:1080px!important;display:block!important;position:relative!important;overflow:hidden!important;page-break-after:always!important;break-after:page!important;background:#F7F4EF;margin:0!important;padding:0!important;';
container.appendChild(s);
});
// 最后一页禁分页,避免尾部空白页
sections[sections.length - 1].style.pageBreakAfter = 'auto';
sections[sections.length - 1].style.breakAfter = 'auto';
document.body.appendChild(container);
});
await page.pdf({ width: '1920px', height: '1080px', printBackground: true, preferCSSPageSize: true });
```
**为什么这能 work**:
- 把 section 从 shadow DOM slot 拔到 light DOM 的普通 div——彻底绕过 `::slotted(section) { display: none }` 规则
- 内联 `position: relative` 让 absolute 子元素相对 section 定位,不会溢出
- `page-break-after: always` 让浏览器 print 时每 section 独立一页
- `:last-child` 不分页避免尾部空白页
**用 `mdls -name kMDItemNumberOfPages` 验证时注意**:macOS 的 Spotlight metadata 有缓存,PDF 重写后要跑 `mdimport file.pdf` 强制刷新,否则显示旧的页数。用 `pdfinfo` 或 `pdftoppm` 数文件数才是真数。
---
### `export_deck_pptx.mjs` — 导出 PPTX(两种模式)
```bash
@@ -393,7 +572,7 @@ node scripts/export_deck_pptx.mjs --slides <dir> --out deck.pptx --mode editable
## 导出为可编辑 PPTX 的深度路径(仅长期项目)
如果你的 deck 会长期维护、反复修改、团队协作——建议**一开始就按 html2pptx 约束写 HTML**,让 `--mode editable` 稳定通过。详见 `huashu-slides` skill 的 `references/prompt-templates.md` 第 2 节(4 条硬性约束)。
如果你的 deck 会长期维护、反复修改、团队协作——建议**一开始就按 html2pptx 约束写 HTML**,让 `--mode editable` 稳定通过。详见 `references/editable-pptx.md`(4 条硬约束 + HTML 模板 + 常见错误速查)。
---