mirror of
https://github.com/aculix/huashu-design.git
synced 2026-10-27 07:15:54 +00:00
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:
1 parent
f3692aac10
commit
b127f8baa3
24 files changed
+8206
-281
No files matched your search
+212
-33
@@ -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 模板 + 常见错误速查)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user