first-commit
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run

This commit is contained in:
Zakaria
2026-05-04 14:58:14 -04:00
commit a46764fb1b
1210 changed files with 233231 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# Security scan allowlist for html-ppt-skill
# These patterns are false positives from template content, not actual threats.
# Path traversal: templates reference shared assets via relative paths
# e.g. templates/full-decks/weekly-report/ → ../../../assets/
# This is the correct relative path to the skill root assets directory.
traversal:templates/full-decks/*/index.html
# Destructive commands: testing-safety-alert template displays forbidden
# commands as text examples in a security policy demo slide.
# They are HTML content, not executable code.
destructive:templates/full-decks/testing-safety-alert/index.html
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 lewis <sudolewis@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+234
View File
@@ -0,0 +1,234 @@
# html-ppt — HTML PPT Studio
> A world-class AgentSkill for producing professional HTML presentations in
> **36 themes**, **15 full-deck templates**, **31 page layouts**,
> **47 animations** (27 CSS + 20 canvas FX), and a **true presenter mode**
> with pixel-perfect previews + speaker script + timer — all pure static
> HTML/CSS/JS, no build step.
**Author:** lewis &lt;sudolewis@gmail.com&gt;
**License:** MIT
**中文文档:** [README.zh-CN.md](README.zh-CN.md)
![html-ppt — cover with live previews](docs/readme/hero.gif)
> One command installs **36 themes × 20 canvas FX × 31 layouts × 15 full decks + presenter mode**. Every preview above is a live iframe of a real template file rendering inside the deck — no screenshots, no mock-ups.
## 🎤 Presenter Mode (new!)
Press `S` on any deck to pop open a dedicated presenter window with four
draggable, resizable **magnetic cards**: current slide, next slide preview,
speaker script (逐字稿), and timer. Two windows stay in sync via
`BroadcastChannel`.
![Presenter mode with 4 magnetic cards](docs/readme/presenter-mode.png)
**Why previews are pixel-perfect:** each card is an `<iframe>` that loads the
same deck HTML with a `?preview=N` query param. The runtime detects this and
renders only slide N with no chrome — so the preview uses the **same CSS,
theme, fonts and viewport** as the audience view. Colors and layout are
guaranteed identical.
**Smooth (no-reload) navigation:** on slide change, the presenter window
sends `postMessage({type:'preview-goto', idx:N})` to each iframe. The iframe
just toggles `.is-active` between slides — **no reload, no flicker**.
**Speaker script rules (3 golden):**
1. **Prompt signals, not lines to read** — bold the keywords, separate
transition sentences into their own paragraphs
2. **150300 words per slide** — that's the ~23 min/page pace
3. **Write it like you speak** — conversational, not written prose
See [`references/presenter-mode.md`](references/presenter-mode.md) for the
full authoring guide, or copy the ready-made template at
`templates/full-decks/presenter-mode-reveal/` which ships with full 150-300
word speaker scripts on every slide.
## Install (one command)
```bash
npx skills add https://github.com/lewislulu/html-ppt-skill
```
That registers the skill with your agent runtime. After install, any agent
that supports AgentSkills can author presentations by asking things like:
> "做一份 8 页的技术分享 slides,用 cyberpunk 主题"
> "turn this outline into a pitch deck"
> "做一个小红书图文,9 张,白底柔和风"
## What's in the box
| | Count | Where |
|---|---|---|
| 🎤 **Presenter mode** | **NEW** | `S` key / `?preview=N` |
| 🎨 **Themes** | **36** | `assets/themes/*.css` |
| 📑 **Full-deck templates** | **15** | `templates/full-decks/<name>/` |
| 🧩 **Single-page layouts** | **31** | `templates/single-page/*.html` |
| ✨ **CSS animations** | **27** | `assets/animations/animations.css` |
| 💥 **Canvas FX animations** | **20** | `assets/animations/fx/*.js` |
| 🖼️ **Showcase decks** | 4 | `templates/*-showcase.html` |
| 📸 **Verification screenshots** | 56 | `scripts/verify-output/` |
### 36 Themes
`minimal-white`, `editorial-serif`, `soft-pastel`, `sharp-mono`, `arctic-cool`,
`sunset-warm`, `catppuccin-latte`, `catppuccin-mocha`, `dracula`, `tokyo-night`,
`nord`, `solarized-light`, `gruvbox-dark`, `rose-pine`, `neo-brutalism`,
`glassmorphism`, `bauhaus`, `swiss-grid`, `terminal-green`, `xiaohongshu-white`,
`rainbow-gradient`, `aurora`, `blueprint`, `memphis-pop`, `cyberpunk-neon`,
`y2k-chrome`, `retro-tv`, `japanese-minimal`, `vaporwave`, `midcentury`,
`corporate-clean`, `academic-paper`, `news-broadcast`, `pitch-deck-vc`,
`magazine-bold`, `engineering-whiteprint`.
![36 themes · 8 of them](docs/readme/themes.png)
Each is a pure CSS-tokens file — swap one `<link>` to reskin the entire deck.
Browse them all in `templates/theme-showcase.html` (each slide rendered in an
isolated iframe so theme ≠ theme is visually guaranteed).
![14 full-deck templates](docs/readme/templates.png)
### 15 Full-deck templates
Eight extracted from real-world decks, seven generic scenario scaffolds:
**Extracted looks**
- `xhs-white-editorial` — 小红书白底杂志风
- `graphify-dark-graph` — 暗底 + 力导向知识图谱
- `knowledge-arch-blueprint` — 蓝图 / 架构图风
- `hermes-cyber-terminal` — 终端 cyberpunk
- `obsidian-claude-gradient` — 紫色渐变卡
- `testing-safety-alert` — 红 / 琥珀警示风
- `xhs-pastel-card` — 柔和马卡龙图文
- `dir-key-nav-minimal` — 方向键极简
**Scenario decks**
- `pitch-deck`, `product-launch`, `tech-sharing`, `weekly-report`,
`xhs-post` (9-slide 3:4), `course-module`,
**`presenter-mode-reveal`** 🎤 — complete talk template with full 150-300
word speaker scripts on every slide, designed around the `S` key presenter mode
Each is a self-contained folder with scoped `.tpl-<name>` CSS so multiple
decks can be previewed side-by-side without collisions. Browse the full
gallery in `templates/full-decks-index.html`.
![31 single-page layouts](docs/readme/layouts.png)
### 31 Single-page layouts
cover · toc · section-divider · bullets · two-column · three-column ·
big-quote · stat-highlight · kpi-grid · table · code · diff · terminal ·
flow-diagram · timeline · roadmap · mindmap · comparison · pros-cons ·
todo-checklist · gantt · image-hero · image-grid · chart-bar · chart-line ·
chart-pie · chart-radar · arch-diagram · process-steps · cta · thanks
Every layout ships with realistic demo data so you can drop it into a deck
and immediately see it render.
![31 layouts auto-cycling through real template files](docs/readme/layouts-live.gif)
*The big iframe is loading `templates/single-page/<name>.html` directly and cycling through all 31 layouts every 2.8 seconds.*
![47 animations — 27 CSS + 20 canvas FX](docs/readme/animations.png)
### 27 CSS animations + 20 Canvas FX
**CSS (lightweight)** — directional fades, `rise-in`, `zoom-pop`, `blur-in`,
`glitch-in`, `typewriter`, `neon-glow`, `shimmer-sweep`, `gradient-flow`,
`stagger-list`, `counter-up`, `path-draw`, `morph-shape`, `parallax-tilt`,
`card-flip-3d`, `cube-rotate-3d`, `page-turn-3d`, `perspective-zoom`,
`marquee-scroll`, `kenburns`, `ripple-reveal`, `spotlight`, …
**Canvas FX (cinematic)**`particle-burst`, `confetti-cannon`, `firework`,
`starfield`, `matrix-rain`, `knowledge-graph` (force-directed physics),
`neural-net` (signal pulses), `constellation`, `orbit-ring`, `galaxy-swirl`,
`word-cascade`, `letter-explode`, `chain-react`, `magnetic-field`,
`data-stream`, `gradient-blob`, `sparkle-trail`, `shockwave`,
`typewriter-multi`, `counter-explosion`. Each is a real hand-rolled canvas
module auto-initialised on slide enter via `fx-runtime.js`.
## Quick start (manual, after install or git clone)
```bash
# Scaffold a new deck from the base template
./scripts/new-deck.sh my-talk
# Browse everything
open templates/theme-showcase.html # all 36 themes (iframe-isolated)
open templates/layout-showcase.html # all 31 layouts
open templates/animation-showcase.html # all 47 animations
open templates/full-decks-index.html # all 14 full decks
# Render any template to PNG via headless Chrome
./scripts/render.sh templates/theme-showcase.html
./scripts/render.sh examples/my-talk/index.html 12
```
## Keyboard cheat sheet
```
← → Space PgUp PgDn Home End navigate
F fullscreen
S open presenter window (magnetic cards)
N quick notes drawer (bottom)
R reset timer (in presenter window)
O slide overview grid
T cycle themes (syncs to presenter)
A cycle a demo animation on current slide
#/N (URL) deep-link to slide N
?preview=N (URL) preview-only mode (single slide, no chrome)
```
## Project structure
```
html-ppt-skill/
├── SKILL.md agent-facing dispatcher
├── README.md this file
├── references/ detailed catalogs
│ ├── themes.md 36 themes with when-to-use
│ ├── layouts.md 31 layout types
│ ├── animations.md 27 CSS + 20 FX catalog
│ ├── full-decks.md 14 full-deck templates
│ └── authoring-guide.md full workflow
├── assets/
│ ├── base.css shared tokens + primitives
│ ├── fonts.css webfont imports
│ ├── runtime.js keyboard + presenter + overview
│ ├── themes/*.css 36 theme token files
│ └── animations/
│ ├── animations.css 27 named CSS animations
│ ├── fx-runtime.js auto-init [data-fx] on slide enter
│ └── fx/*.js 20 canvas FX modules
├── templates/
│ ├── deck.html minimal starter
│ ├── theme-showcase.html iframe-isolated theme tour
│ ├── layout-showcase.html all 31 layouts
│ ├── animation-showcase.html 47 animation slides
│ ├── full-decks-index.html 14-deck gallery
│ ├── full-decks/<name>/ 14 scoped multi-slide decks
│ └── single-page/*.html 31 layout files with demo data
├── scripts/
│ ├── new-deck.sh scaffold
│ ├── render.sh headless Chrome → PNG
│ └── verify-output/ 56 self-test screenshots
└── examples/demo-deck/ complete working deck
```
## Philosophy
- **Token-driven design system.** All color, radius, shadow, font decisions
live in `assets/base.css` + the current theme file. Change one variable,
the whole deck reflows tastefully.
- **Iframe isolation for previews.** Theme / layout / full-deck showcases all
use `<iframe>` per slide so each preview is a real, independent render.
- **Zero build.** Pure static HTML/CSS/JS. CDN only for webfonts, highlight.js
and chart.js (optional).
- **Senior-designer defaults.** Opinionated type scale, spacing rhythm,
gradients and card treatments — no "Corporate PowerPoint 2006" vibes.
- **Chinese + English first-class.** Noto Sans SC / Noto Serif SC pre-imported.
## License
MIT © 2026 lewis &lt;sudolewis@gmail.com&gt;.
+238
View File
@@ -0,0 +1,238 @@
# html-ppt · HTML PPT 工作室
> 一款专业级的 AgentSkill,让 AI 做出真正能打的 HTML 演示文稿。
> **36 套主题**、**15 套完整 deck 模板**、**31 种页面布局**、**47 个动效**
> (27 个 CSS + 20 个 Canvas FX),加上全新的 **演讲者模式** —— 像素级
> 完美预览 + 逐字稿提词器 + 计时器。纯静态 HTML/CSS/JS,无需构建。
**作者:** lewis &lt;sudolewis@gmail.com&gt;
**协议:** MIT
**English docs:** [README.md](README.md)
![html-ppt 封面 · 实时预览](docs/readme/hero.gif)
> 一行命令装好 **36 主题 × 20 Canvas FX × 31 布局 × 15 完整 deck + 演讲者模式**。
> 上图里的每一个预览都是真实的 iframe 加载真实模板文件 —— 不是截图,不是色卡。
## 🎤 演讲者模式(全新)
在任何 deck 里按 `S` 键,弹出一个独立的演讲者窗口,包含 4 个**可拖拽、
可调整大小的磁吸卡片**:当前页预览、下一页预览、逐字稿、计时器。两个窗口
通过 `BroadcastChannel` 双向同步翻页。
![演讲者模式 · 4 个磁吸卡片](docs/readme/presenter-mode.png)
**为什么预览是像素级完美的:** 每个卡片是一个 `<iframe>`,加载的是**同一
份 deck HTML 文件**,只是 URL 多了 `?preview=N` 参数。runtime 检测到这个
参数后,只渲染第 N 页并隐藏所有 chrome —— 所以预览使用**和观众视图完全相
同的 CSS、主题、字体、viewport**,颜色和排版保证 100% 一致。
**丝滑翻页(零闪烁):** 翻页时演讲者窗口通过 `postMessage({type:'preview-goto',
idx:N})` 通知 iframeiframe 只是切换 `.is-active` class —— **不重新加载、
不白屏、不闪烁**。
**逐字稿 3 条铁律:**
1. **提示信号,不是讲稿** — 关键词加粗,过渡句独立成段
2. **每页 150300 字** — 约 23 分钟/页的节奏
3. **用口语,不用书面语** — "所以" 不是 "因此""这个" 不是 "该"
详见 [`references/presenter-mode.md`](references/presenter-mode.md),或直接复制
`templates/full-decks/presenter-mode-reveal/` 这个现成模板 —— 每一页都带完整
150300 字的示例逐字稿。
## 一行命令安装
```bash
npx skills add https://github.com/lewislulu/html-ppt-skill
```
装好后,任何支持 AgentSkill 的 agentClaude Code / Codex / Cursor / OpenClaw 等)
都能用这套能力做 PPT。对 agent 说:
> "做一份 8 页的技术分享 slides,用 cyberpunk 主题"
> "把这段 outline 变成投资人 pitch deck"
> "做一个小红书图文,9 张,白底柔和风"
> "做一份带演讲者模式的产品分享,我想要有逐字稿"
## Skill 内容一览
| | 数量 | 位置 |
|---|---|---|
| 🎤 **演讲者模式** | **新增** | `S` 键 / `?preview=N` |
| 🎨 **主题** | **36** | `assets/themes/*.css` |
| 📑 **完整 deck 模板** | **15** | `templates/full-decks/<name>/` |
| 🧩 **单页布局** | **31** | `templates/single-page/*.html` |
| ✨ **CSS 动画** | **27** | `assets/animations/animations.css` |
| 💥 **Canvas FX 动画** | **20** | `assets/animations/fx/*.js` |
| 🖼️ **Showcase deck** | 4 | `templates/*-showcase.html` |
| 📸 **验证截图** | 56 | `scripts/verify-output/` |
### 36 套主题
`minimal-white``editorial-serif``soft-pastel``sharp-mono``arctic-cool`
`sunset-warm``catppuccin-latte``catppuccin-mocha``dracula``tokyo-night`
`nord``solarized-light``gruvbox-dark``rose-pine``neo-brutalism`
`glassmorphism``bauhaus``swiss-grid``terminal-green``xiaohongshu-white`
`rainbow-gradient``aurora``blueprint``memphis-pop``cyberpunk-neon`
`y2k-chrome``retro-tv``japanese-minimal``vaporwave``midcentury`
`corporate-clean``academic-paper``news-broadcast``pitch-deck-vc`
`magazine-bold``engineering-whiteprint`
![36 主题 · 其中 8 个](docs/readme/themes.png)
每个主题都是一份纯 CSS token 文件 —— 只需要换一行 `<link>` 就能给整份 deck
换皮。在 `templates/theme-showcase.html` 里可以浏览全部(每一页用独立 iframe
渲染,避免样式互相污染)。
![15 套完整 deck 模板](docs/readme/templates.png)
### 15 套完整 deck 模板
8 个从真实作品提炼的视觉语言,7 个通用场景脚手架:
**提炼款**
- `xhs-white-editorial` — 小红书白底杂志风
- `graphify-dark-graph` — 暗底 + 力导向知识图谱
- `knowledge-arch-blueprint` — 蓝图 / 架构图风
- `hermes-cyber-terminal` — 终端 cyberpunk 风
- `obsidian-claude-gradient` — 紫色渐变卡
- `testing-safety-alert` — 红 / 琥珀警示风
- `xhs-pastel-card` — 柔和马卡龙图文
- `dir-key-nav-minimal` — 方向键极简
**场景款**
- `pitch-deck` — 投资人 pitch
- `product-launch` — 产品发布会
- `tech-sharing` — 技术分享
- `weekly-report` — 周报
- `xhs-post` — 小红书图文(9 页 3:4
- `course-module` — 教学模块
- **`presenter-mode-reveal`** 🎤 — 完整分享模板,**每一页都带 150-300 字
的示例逐字稿**,围绕 `S` 键演讲者模式专门设计
每个模板都是自包含的文件夹,用 scoped `.tpl-<name>` CSS,所以多个模板可以
同时加载不会互相污染。在 `templates/full-decks-index.html` 可以看全套 gallery。
![31 种单页布局](docs/readme/layouts.png)
### 31 种单页布局
cover · toc · section-divider · bullets · two-column · three-column ·
big-quote · stat-highlight · kpi-grid · table · code · diff · terminal ·
flow-diagram · timeline · roadmap · mindmap · comparison · pros-cons ·
todo-checklist · gantt · image-hero · image-grid · chart-bar · chart-line ·
chart-pie · chart-radar · arch-diagram · process-steps · cta · thanks
每个布局都带真实的示例数据,拖进 deck 立即看得到效果。
![31 种布局通过真实模板文件自动循环播放](docs/readme/layouts-live.gif)
*大 iframe 直接加载 `templates/single-page/<name>.html` 文件,每 2.8 秒
自动切换到下一个布局。*
![47 个动效 · 27 CSS + 20 Canvas FX](docs/readme/animations.png)
### 27 个 CSS 动画 + 20 个 Canvas FX
**CSS 动画(轻量)** — 方向性淡入、`rise-in``zoom-pop``blur-in`
`glitch-in``typewriter`(打字机)、`neon-glow`(霓虹光晕)、
`shimmer-sweep`(流光)、`gradient-flow`(渐变流动)、`stagger-list`
(列表错开入场)、`counter-up`(数字滚动)、`path-draw`(路径绘制)、
`morph-shape``parallax-tilt``card-flip-3d``cube-rotate-3d`
`page-turn-3d``perspective-zoom``marquee-scroll``kenburns`
`ripple-reveal``spotlight`、…
**Canvas FX(电影级)**`particle-burst`(粒子爆发)、`confetti-cannon`
(彩带)、`firework`(烟花)、`starfield`(星空)、`matrix-rain`
(代码雨)、`knowledge-graph`(力导向知识图谱)、`neural-net`(神经网络
脉冲)、`constellation`(星座连线)、`orbit-ring`(轨道环)、
`galaxy-swirl`(星系漩涡)、`word-cascade``letter-explode`
`chain-react``magnetic-field``data-stream``gradient-blob`
`sparkle-trail``shockwave``typewriter-multi``counter-explosion`
每一个都是手写的 canvas 模块,进入 slide 时由 `fx-runtime.js` 自动初始化。
## 快速开始(手动 / 安装后 / git clone 后)
```bash
# 从 base 模板新建一个 deck
./scripts/new-deck.sh my-talk
# 浏览所有内容
open templates/theme-showcase.html # 全部 36 主题(iframe 隔离)
open templates/layout-showcase.html # 全部 31 布局
open templates/animation-showcase.html # 全部 47 动效
open templates/full-decks-index.html # 全部 15 个完整 deck
# 用 headless Chrome 导出 PNG
./scripts/render.sh templates/theme-showcase.html
./scripts/render.sh examples/my-talk/index.html 12
```
## 键盘快捷键
```
← → Space PgUp PgDn Home End 翻页
F 全屏
S 打开演讲者窗口(磁吸卡片模式)
N 底部 notes 抽屉
R 重置计时器(演讲者窗口内)
O slide 总览网格
T 切换主题(自动同步到演讲者窗口)
A 在当前 slide 循环演示一个动画
#/N (URL) 深链到第 N 页
?preview=N (URL) 预览模式(只显示单页,隐藏 chrome)
```
## 项目结构
```
html-ppt-skill/
├── SKILL.md agent 入口
├── README.md 英文 README
├── README.zh-CN.md 本文件
├── references/ 详细文档
│ ├── themes.md 36 主题 + 使用场景
│ ├── layouts.md 31 布局
│ ├── animations.md 27 CSS + 20 FX 目录
│ ├── full-decks.md 15 完整 deck 模板
│ ├── presenter-mode.md 🎤 演讲者模式 + 逐字稿指南
│ └── authoring-guide.md 完整工作流
├── assets/
│ ├── base.css 共享 tokens + 基础组件
│ ├── fonts.css web 字体引入
│ ├── runtime.js 键盘导航 + 演讲者模式 + 总览
│ ├── themes/*.css 36 主题 token 文件
│ └── animations/
│ ├── animations.css 27 个命名 CSS 动画
│ ├── fx-runtime.js 进入 slide 自动初始化 [data-fx]
│ └── fx/*.js 20 个 Canvas FX 模块
├── templates/
│ ├── deck.html 最小起步模板
│ ├── theme-showcase.html iframe 隔离的主题 tour
│ ├── layout-showcase.html 全部 31 布局
│ ├── animation-showcase.html 47 动画 slide
│ ├── full-decks-index.html 15 deck gallery
│ ├── full-decks/<name>/ 15 个 scoped 多页 deck 模板
│ └── single-page/*.html 31 个布局文件(带示例数据)
├── scripts/
│ ├── new-deck.sh 脚手架
│ ├── render.sh headless Chrome → PNG
│ └── verify-output/ 56 张自测截图
└── examples/demo-deck/ 完整可运行的示例 deck
```
## 设计理念
- **Token 驱动的设计系统。** 所有颜色、圆角、阴影、字体决策都在
`assets/base.css` + 当前主题文件里。改一个变量,整份 deck 优雅地重排。
- **Iframe 隔离预览。** 主题 / 布局 / 完整 deck 的 showcase 都用 `<iframe>`
确保每个预览都是真实、独立的渲染结果。
- **零构建。** 纯静态 HTML/CSS/JS。只有 webfont / highlight.js / chart.js
(可选) 走 CDN。
- **资深设计师的默认值。** 字号规律、间距节奏、渐变、卡片处理都有态度 ——
绝不是 "PowerPoint 2006" 那种味道。
- **中英双语一等公民。** 预导入了 Noto Sans SC / Noto Serif SC。
## 协议
MIT © 2026 lewis &lt;sudolewis@gmail.com&gt;
+251
View File
@@ -0,0 +1,251 @@
---
name: html-ppt
description: HTML PPT Studio — author professional static HTML presentations in many styles, layouts, and animations, all driven by templates. Use when the user asks for a presentation, PPT, slides, keynote, deck, slideshow, "幻灯片", "演讲稿", "做一份 PPT", "做一份 slides", a reveal-style HTML deck, a 小红书 图文, or any kind of multi-slide pitch/report/sharing document that should look tasteful and be usable with keyboard navigation. Triggers include keywords like "presentation", "ppt", "slides", "deck", "keynote", "reveal", "slideshow", "幻灯片", "演讲稿", "分享稿", "小红书图文", "talk slides", "pitch deck", "tech sharing", "technical presentation".
triggers:
- "ppt"
- "deck"
- "slides"
- "presentation"
- "keynote"
- "reveal"
- "slideshow"
- "幻灯片"
- "演讲稿"
- "分享稿"
- "talk slides"
- "pitch deck"
- "tech sharing"
- "technical presentation"
od:
mode: deck
scenario: marketing
featured: 19
upstream: "https://github.com/lewislulu/html-ppt-skill"
preview:
type: html
entry: index.html
design_system:
requires: false
speaker_notes: true
animations: true
example_prompt: "用 html-ppt 做一份 12 页的 HTML PPT。先帮我确认三件事:内容/页数/受众、主题(从 36 套里推荐 2-3 个)、起点全 deck 模板(pitch-deck / tech-sharing / weekly-report / xhs-post / presenter-mode-reveal 任选一个),对齐之后再开始写 slides。"
---
# html-ppt — HTML PPT Studio
Author professional HTML presentations as static files. One theme file = one
look. One layout file = one page type. One animation class = one entry effect.
All pages share a token-based design system in `assets/base.css`.
## Install
```bash
npx skills add https://github.com/lewislulu/html-ppt-skill
```
One command, no build. Pure static HTML/CSS/JS with only CDN webfonts.
## What the skill gives you
- **36 themes** (`assets/themes/*.css`) — minimal-white, editorial-serif, soft-pastel, sharp-mono, arctic-cool, sunset-warm, catppuccin-latte/mocha, dracula, tokyo-night, nord, solarized-light, gruvbox-dark, rose-pine, neo-brutalism, glassmorphism, bauhaus, swiss-grid, terminal-green, xiaohongshu-white, rainbow-gradient, aurora, blueprint, memphis-pop, cyberpunk-neon, y2k-chrome, retro-tv, japanese-minimal, vaporwave, midcentury, corporate-clean, academic-paper, news-broadcast, pitch-deck-vc, magazine-bold, engineering-whiteprint
- **15 full-deck templates** (`templates/full-decks/<name>/`) — complete multi-slide decks with scoped `.tpl-<name>` CSS. 8 extracted from real-world decks (xhs-white-editorial, graphify-dark-graph, knowledge-arch-blueprint, hermes-cyber-terminal, obsidian-claude-gradient, testing-safety-alert, xhs-pastel-card, dir-key-nav-minimal), 7 scenario scaffolds (pitch-deck, product-launch, tech-sharing, weekly-report, xhs-post 3:4, course-module, **presenter-mode-reveal** — 演讲者模式专用)
- **31 layouts** (`templates/single-page/*.html`) with realistic demo data
- **27 CSS animations** (`assets/animations/animations.css`) via `data-anim`
- **20 canvas FX animations** (`assets/animations/fx/*.js`) via `data-fx` — particle-burst, confetti-cannon, firework, starfield, matrix-rain, knowledge-graph (force-directed), neural-net (pulses), constellation, orbit-ring, galaxy-swirl, word-cascade, letter-explode, chain-react, magnetic-field, data-stream, gradient-blob, sparkle-trail, shockwave, typewriter-multi, counter-explosion
- **Keyboard runtime** (`assets/runtime.js`) — arrows, T (theme), A (anim), F/O, **S (presenter mode: magnetic-card popup with CURRENT / NEXT / SCRIPT / TIMER cards)**, N (notes drawer), R (reset timer in presenter)
- **FX runtime** (`assets/animations/fx-runtime.js`) — auto-inits `[data-fx]` on slide enter, cleans up on leave
- **Showcase decks** for themes / layouts / animations / full-decks gallery
- **Headless Chrome render script** for PNG export
## When to use
Use when the user asks for any kind of slide-based output or wants to turn
text/notes into a presentable deck. Prefer this over building from scratch.
### 🎤 Presenter Mode (演讲者模式 + 逐字稿)
If the user mentions any of: **演讲 / 分享 / 讲稿 / 逐字稿 / speaker notes / presenter view / 演讲者视图 / 提词器**, or says things like "我要去给团队讲 xxx", "要做一场技术分享", "怕讲不流畅", "想要一份带逐字稿的 PPT" — **use the `presenter-mode-reveal` full-deck template** and write 150300 words of 逐字稿 in each slide's `<aside class="notes">`.
See [references/presenter-mode.md](references/presenter-mode.md) for the full authoring guide including the 3 rules of speaker script writing:
1. **不是讲稿,是提示信号** — 加粗核心词 + 过渡句独立成段
2. **每页 150300 字** — 23 分钟/页的节奏
3. **用口语,不用书面语** — "因此"→"所以""该方案"→"这个方案"
All full-deck templates support the S key presenter mode (it's built into `runtime.js`). **S opens a new popup window with 4 magnetic cards**:
- 🔵 **CURRENT** — pixel-perfect iframe preview of the current slide
- 🟣 **NEXT** — pixel-perfect iframe preview of the next slide
- 🟠 **SPEAKER SCRIPT** — large-font 逐字稿 (scrollable)
- 🟢 **TIMER** — elapsed time + slide counter + prev/next/reset buttons
Each card is **draggable by its header** and **resizable by the bottom-right corner handle**. Card positions/sizes persist to `localStorage` per deck. A "Reset layout" button restores the default arrangement.
**Why the previews are pixel-perfect**: each preview is an `<iframe>` that loads the actual deck HTML with a `?preview=N` query param; `runtime.js` detects this and renders only slide N with no chrome. So the preview uses the **same CSS, theme, fonts, and viewport as the audience view** — colors and layout are guaranteed identical.
**Smooth navigation**: on slide change, the presenter window sends `postMessage({type:'preview-goto', idx:N})` to each iframe. The iframe just toggles `.is-active` between slides — **no reload, no flicker**. The two windows also stay in sync via `BroadcastChannel`.
Only `presenter-mode-reveal` is designed from the ground up around the feature with proper example 逐字稿 on every slide.
Keyboard in presenter window: `← →` navigate (syncs audience) · `R` reset timer · `Esc` close popup.
Keyboard in audience window: `S` open presenter · `T` cycle theme · `← →` navigate (syncs presenter) · `F` fullscreen · `O` overview.
## Before you author anything — ALWAYS ask or recommend
**Do not start writing slides until you understand three things.** Either ask
the user directly, or — if they already handed you rich content — propose a
tasteful default and confirm.
1. **Content & audience.** What's the deck about, how many slides, who's
watching (engineers / execs / 小红书读者 / 学生 / VC)?
2. **Style / theme.** Which of the 36 themes fits? If unsure, recommend 2-3
candidates based on tone:
- Business / investor pitch → `pitch-deck-vc`, `corporate-clean`, `swiss-grid`
- Tech sharing / engineering → `tokyo-night`, `dracula`, `catppuccin-mocha`,
`terminal-green`, `blueprint`
- 小红书图文 → `xiaohongshu-white`, `soft-pastel`, `rainbow-gradient`,
`magazine-bold`
- Academic / report → `academic-paper`, `editorial-serif`, `minimal-white`
- Edgy / cyber / launch → `cyberpunk-neon`, `vaporwave`, `y2k-chrome`,
`neo-brutalism`
3. **Starting point.** One of the 14 full-deck templates, or scratch? Point
to the closest `templates/full-decks/<name>/` and ask if it fits. If the
user's content suggests something obvious (e.g. "我要做产品发布会" →
`product-launch`), propose it confidently instead of asking blindly.
A good opening message looks like:
> 我可以给你做这份 PPT!先确认三件事:
> 1. 大致内容 / 页数 / 观众是谁?
> 2. 风格偏好?我建议从这 3 个主题里选一个:`tokyo-night`(技术分享默认好看)、`xiaohongshu-white`(小红书风)、`corporate-clean`(正式汇报)。
> 3. 要不要用我现成的 `tech-sharing` 全 deck 模板打底?
Only after those are clear, scaffold the deck and start writing.
## Quick start
1. **Scaffold a new deck.** From the repo root:
```bash
./scripts/new-deck.sh my-talk
open examples/my-talk/index.html
```
2. **Pick a theme.** Open the deck and press `T` to cycle. Or hard-code it:
```html
<link rel="stylesheet" id="theme-link" href="../assets/themes/aurora.css">
```
Catalog in [references/themes.md](references/themes.md).
3. **Pick layouts.** Copy `<section class="slide">...</section>` blocks out of
files in `templates/single-page/` into your deck. Replace the demo data.
Catalog in [references/layouts.md](references/layouts.md).
4. **Add animations.** Put `data-anim="fade-up"` (or `class="anim-fade-up"`) on
any element. On `<ul>`/grids, use `anim-stagger-list` for sequenced reveals.
For canvas FX, use `<div data-fx="knowledge-graph">...</div>` and include
`<script src="../assets/animations/fx-runtime.js"></script>`.
Catalog in [references/animations.md](references/animations.md).
5. **Use a full-deck template.** Copy `templates/full-decks/<name>/` into
`examples/my-talk/` as a starting point. Each folder is self-contained with
scoped CSS. Catalog in [references/full-decks.md](references/full-decks.md)
and gallery at `templates/full-decks-index.html`.
6. **Render to PNG.**
```bash
./scripts/render.sh templates/theme-showcase.html # one shot
./scripts/render.sh examples/my-talk/index.html 12 # 12 slides
```
## Authoring rules (important)
- **Always start from a template.** Don't author slides from scratch — copy the
closest layout from `templates/single-page/` first, then replace content.
- **Use tokens, not literal colors.** Every color, radius, shadow should come
from CSS variables defined in `assets/base.css` and overridden by a theme.
Good: `color: var(--text-1)`. Bad: `color: #111`.
- **Don't invent new layout files.** Prefer composing existing ones. Only add
a new `templates/single-page/*.html` if none of the 30 fit.
- **Respect chrome slots.** `.deck-header`, `.deck-footer`, `.slide-number`
and the progress bar are provided by `assets/base.css` + `runtime.js`.
- **Keyboard-first.** Always include `<script src="../assets/runtime.js"></script>`
so the deck supports ← → / T / A / F / S / O / hash deep-links.
- **One `.slide` per logical page.** `runtime.js` makes `.slide.is-active`
visible; all others are hidden.
- **Supply notes.** Wrap speaker notes in `<div class="notes">…</div>` inside
each slide. Press S to open the overlay.
- **NEVER put presenter-only text on the slide itself.** Descriptive text like
"这一页展示了……" or "Speaker: 这里可以补充……" or small explanatory captions
aimed at the presenter MUST go inside `<div class="notes">`, NOT as visible
`<p>` / `<span>` elements on the slide. The `.notes` class is `display:none`
by default — it only appears in the S overlay. Slides should contain ONLY
audience-facing content (titles, bullet points, data, charts, images).
## Writing guide
See [references/authoring-guide.md](references/authoring-guide.md) for a
step-by-step walkthrough: file structure, naming, how to transform an outline
into a deck, how to choose layouts and themes per audience, how to do a
Chinese + English deck, and how to export.
## Catalogs (load when needed)
- [references/themes.md](references/themes.md) — all 36 themes with when-to-use.
- [references/layouts.md](references/layouts.md) — all 31 layout types.
- [references/animations.md](references/animations.md) — 27 CSS + 20 canvas FX animations.
- [references/full-decks.md](references/full-decks.md) — all 15 full-deck templates.
- [references/presenter-mode.md](references/presenter-mode.md) — **演讲者模式 + 逐字稿编写指南(技术分享/演讲必看)**.
- [references/authoring-guide.md](references/authoring-guide.md) — full workflow.
## File structure
```
html-ppt/
├── SKILL.md (this file)
├── references/ (detailed catalogs, load as needed)
├── assets/
│ ├── base.css (tokens + primitives — do not edit per deck)
│ ├── fonts.css (webfont imports)
│ ├── runtime.js (keyboard + presenter + overview + theme cycle)
│ ├── themes/*.css (36 token overrides, one per theme)
│ └── animations/
│ ├── animations.css (27 named CSS entry animations)
│ ├── fx-runtime.js (auto-init [data-fx] on slide enter)
│ └── fx/*.js (20 canvas FX modules: particles/graph/fireworks…)
├── templates/
│ ├── deck.html (minimal 6-slide starter)
│ ├── theme-showcase.html (36 slides, iframe-isolated per theme)
│ ├── layout-showcase.html (iframe tour of all 31 layouts)
│ ├── animation-showcase.html (20 FX + 27 CSS animation slides)
│ ├── full-decks-index.html (gallery of all 14 full-deck templates)
│ ├── full-decks/<name>/ (14 scoped multi-slide deck templates)
│ └── single-page/*.html (31 layout files with demo data)
├── scripts/
│ ├── new-deck.sh (scaffold a deck from deck.html)
│ └── render.sh (headless Chrome → PNG)
└── examples/demo-deck/ (complete working deck)
```
## Rendering to PNG
`scripts/render.sh` wraps headless Chrome at
`/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`. For multi-slide
capture, runtime.js exposes `#/N` deep-links, and render.sh iterates 1..N.
```bash
./scripts/render.sh templates/single-page/kpi-grid.html # single page
./scripts/render.sh examples/demo-deck/index.html 8 out-dir # 8 slides, custom dir
```
## Keyboard cheat sheet
```
← → Space PgUp PgDn Home End navigate
F fullscreen
S open presenter window (magnetic cards: current/next/script/timer)
N quick notes drawer (bottom overlay)
R reset timer (in presenter window)
?preview=N URL param — force preview-only mode (single slide, no chrome)
O slide overview grid
T cycle themes (reads data-themes attr)
A cycle demo animation on current slide
#/N in URL deep-link to slide N
Esc close all overlays
```
## License & author
MIT. Copyright (c) 2026 lewis &lt;sudolewis@gmail.com&gt;.
@@ -0,0 +1,138 @@
/* html-ppt :: animations.css
* Apply by adding class="anim-<name>" or data-anim="<name>".
* Durations are deliberately snappy; tweak --anim-dur per element.
*/
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
/* ---------- FADE DIRECTIONALS ---------- */
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
100%{opacity:1;transform:none}}
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
/* ---------- TYPEWRITER ---------- */
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
@keyframes kf-type{to{width:100%}}
@keyframes kf-caret{50%{border-color:transparent}}
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
.anim-shimmer-sweep{position:relative;overflow:hidden}
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
@keyframes kf-shimmer{to{transform:translateX(100%)}}
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
animation:kf-gradflow 4s linear infinite}
@keyframes kf-gradflow{to{background-position:300% 0}}
/* ---------- STAGGER LIST ---------- */
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
.counter{font-variant-numeric:tabular-nums}
/* ---------- SVG PATH DRAW ---------- */
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
@keyframes kf-draw{to{stroke-dashoffset:0}}
/* ---------- PARALLAX TILT (hover) ---------- */
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
/* ---------- CARD FLIP 3D ---------- */
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
to{transform:perspective(1200px) rotateY(0);opacity:1}}
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
/* ---------- CUBE ROTATE 3D ---------- */
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
/* ---------- PAGE TURN 3D ---------- */
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
to{transform:perspective(1600px) rotateY(0);opacity:1}}
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
/* ---------- PERSPECTIVE ZOOM ---------- */
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
to{opacity:1;transform:none}}
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
/* ---------- MARQUEE SCROLL ---------- */
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
/* ---------- KEN BURNS ---------- */
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
.anim-confetti-burst{position:relative}
.anim-confetti-burst::before,.anim-confetti-burst::after{
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
background:var(--accent);box-shadow:
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
/* ---------- SPOTLIGHT ---------- */
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
/* ---------- MORPH SHAPE (SVG) ---------- */
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
/* ---------- RIPPLE REVEAL ---------- */
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
/* reduced motion */
@media (prefers-reduced-motion: reduce){
[class*="anim-"]{animation:none!important;transition:none!important}
}
@@ -0,0 +1,99 @@
/* html-ppt :: fx-runtime.js
* Canvas FX autoloader + lifecycle manager.
* - Dynamically loads all fx modules listed in FX_LIST
* - Initializes [data-fx] elements when their slide becomes active
* - Calls handle.stop() when the slide leaves
*/
(function(){
'use strict';
const FX_LIST = [
'_util',
'particle-burst','confetti-cannon','firework','starfield','matrix-rain',
'knowledge-graph','neural-net','constellation','orbit-ring','galaxy-swirl',
'word-cascade','letter-explode','chain-react','magnetic-field','data-stream',
'gradient-blob','sparkle-trail','shockwave','typewriter-multi','counter-explosion'
];
// Resolve base path of this script so it works from any page location.
const myScript = document.currentScript || (function(){
const all = document.getElementsByTagName('script');
for (const s of all){ if (s.src && s.src.indexOf('fx-runtime.js')>-1) return s; }
return null;
})();
const base = myScript ? myScript.src.replace(/fx-runtime\.js.*$/, 'fx/') : 'assets/animations/fx/';
let loaded = 0;
const total = FX_LIST.length;
const ready = new Promise((resolve) => {
if (!total) return resolve();
FX_LIST.forEach((name) => {
const s = document.createElement('script');
s.src = base + name + '.js';
s.async = false;
s.onload = s.onerror = () => { if (++loaded >= total) resolve(); };
document.head.appendChild(s);
});
});
window.__hpxActive = window.__hpxActive || new Map();
function initFxIn(root){
if (!window.HPX) return;
const els = root.querySelectorAll('[data-fx]');
els.forEach((el) => {
if (window.__hpxActive.has(el)) return;
const name = el.getAttribute('data-fx');
const fn = window.HPX[name];
if (typeof fn !== 'function') return;
try {
const handle = fn(el, {}) || { stop(){} };
window.__hpxActive.set(el, handle);
} catch(e){ console.warn('[hpx-fx]', name, e); }
});
}
function stopFxIn(root){
const els = root.querySelectorAll('[data-fx]');
els.forEach((el) => {
const h = window.__hpxActive.get(el);
if (h && typeof h.stop === 'function'){
try{ h.stop(); }catch(e){}
}
window.__hpxActive.delete(el);
});
}
function reinitFxIn(root){
stopFxIn(root);
initFxIn(root);
}
window.__hpxReinit = reinitFxIn;
function boot(){
ready.then(() => {
const active = document.querySelector('.slide.is-active') || document.querySelector('.slide');
if (active) initFxIn(active);
// Watch all slides for class changes
const slides = document.querySelectorAll('.slide');
slides.forEach((sl) => {
const mo = new MutationObserver((muts) => {
for (const m of muts){
if (m.attributeName === 'class'){
if (sl.classList.contains('is-active')) initFxIn(sl);
else stopFxIn(sl);
}
}
});
mo.observe(sl, { attributes: true, attributeFilter: ['class'] });
});
});
}
if (document.readyState === 'loading'){
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();
@@ -0,0 +1,63 @@
/* html-ppt fx :: shared helpers */
(function(){
window.HPX = window.HPX || {};
const U = window.HPX._u = {};
U.css = (el, name, fb) => {
const v = getComputedStyle(el).getPropertyValue(name).trim();
return v || fb;
};
U.accent = (el, fb) => U.css(el, '--accent', fb || '#7c5cff');
U.accent2 = (el, fb) => U.css(el, '--accent-2', fb || '#22d3ee');
U.text = (el, fb) => U.css(el, '--text-1', fb || '#eaeaf2');
U.palette = (el) => [
U.accent(el, '#7c5cff'),
U.accent2(el, '#22d3ee'),
U.css(el, '--ok', '#22c55e'),
U.css(el, '--warn', '#f59e0b'),
U.css(el, '--danger', '#ef4444'),
];
U.canvas = (el) => {
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const c = document.createElement('canvas');
c.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none;display:block;';
el.appendChild(c);
const ctx = c.getContext('2d');
let w = 0, h = 0, dpr = Math.max(1, Math.min(2, window.devicePixelRatio||1));
const fit = () => {
const r = el.getBoundingClientRect();
w = Math.max(1, r.width|0);
h = Math.max(1, r.height|0);
c.width = (w*dpr)|0;
c.height = (h*dpr)|0;
ctx.setTransform(dpr,0,0,dpr,0,0);
};
fit();
const ro = new ResizeObserver(fit);
ro.observe(el);
return {
c, ctx,
get w(){return w;}, get h(){return h;}, get dpr(){return dpr;},
destroy(){
try{ro.disconnect();}catch(e){}
if (c.parentNode) c.parentNode.removeChild(c);
}
};
};
U.loop = (fn) => {
let raf = 0, stopped = false, t0 = performance.now();
const tick = (t) => {
if (stopped) return;
fn((t - t0)/1000);
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => { stopped = true; cancelAnimationFrame(raf); };
};
U.rand = (a,b) => a + Math.random()*(b-a);
})();
@@ -0,0 +1,41 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['chain-react'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const ac = U.accent(el,'#7c5cff'), ac2 = U.accent2(el,'#22d3ee');
const N = 8;
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
const cy = k.h/2;
const pad = 60;
const dx = (k.w - pad*2)/(N-1);
const period = 2.4;
const phase = (t % period) / period; // 0..1
for (let i=0;i<N;i++){
const x = pad + i*dx;
const my = i/(N-1);
const d = Math.abs(phase - my);
const pulse = Math.max(0, 1 - d*6);
const r = 18 + pulse*18;
// glow
const g = ctx.createRadialGradient(x,cy,0,x,cy,r*2);
g.addColorStop(0, `rgba(124,92,255,${0.4*pulse})`);
g.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = g;
ctx.fillRect(x-r*2, cy-r*2, r*4, r*4);
// circle
ctx.fillStyle = pulse>0.1 ? ac2 : ac;
ctx.beginPath(); ctx.arc(x,cy,r,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='rgba(255,255,255,0.4)'; ctx.lineWidth=2;
ctx.stroke();
// connectors
if (i<N-1){
ctx.strokeStyle='rgba(200,200,230,0.3)'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(x+r,cy); ctx.lineTo(x+dx-r,cy); ctx.stroke();
}
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,49 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['confetti-cannon'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
let parts = [];
const fire = () => {
for (let side=0; side<2; side++){
const x0 = side===0 ? 20 : k.w-20;
const y0 = k.h - 20;
for (let i=0;i<40;i++){
const a = side===0 ? U.rand(-Math.PI*0.7, -Math.PI*0.4) : U.rand(-Math.PI*0.6, -Math.PI*0.3) - Math.PI/2 - Math.PI/6;
const spd = U.rand(300, 520);
parts.push({
x: x0, y: y0,
vx: Math.cos(a)*spd, vy: Math.sin(a)*spd,
w: U.rand(6,12), h: U.rand(3,7),
rot: Math.random()*Math.PI, vr: U.rand(-6,6),
c: pal[(Math.random()*pal.length)|0],
life: 1
});
}
}
};
fire();
let last = 0;
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
if (t - last > 3) { fire(); last = t; }
const dt = 1/60;
parts = parts.filter(p => p.life > 0 && p.y < k.h+40);
for (const p of parts){
p.vy += 520*dt;
p.x += p.vx*dt; p.y += p.vy*dt;
p.rot += p.vr*dt;
p.life -= 0.006;
ctx.save();
ctx.translate(p.x, p.y); ctx.rotate(p.rot);
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.c;
ctx.fillRect(-p.w/2, -p.h/2, p.w, p.h);
ctx.restore();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,44 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['constellation'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const ac = U.accent(el,'#9fb4ff');
const N = 70;
let pts = [];
const seed = () => {
pts = Array.from({length:N}, () => ({
x: Math.random()*k.w, y: Math.random()*k.h,
vx: U.rand(-0.3,0.3), vy: U.rand(-0.3,0.3)
}));
};
seed();
let lw=k.w, lh=k.h;
const stop = U.loop(() => {
if (k.w!==lw||k.h!==lh){ seed(); lw=k.w; lh=k.h; }
ctx.clearRect(0,0,k.w,k.h);
for (const p of pts){
p.x += p.vx; p.y += p.vy;
if (p.x<0||p.x>k.w) p.vx*=-1;
if (p.y<0||p.y>k.h) p.vy*=-1;
}
for (let i=0;i<N;i++){
for (let j=i+1;j<N;j++){
const a=pts[i], b=pts[j];
const d = Math.hypot(a.x-b.x, a.y-b.y);
if (d < 150){
ctx.globalAlpha = 1 - d/150;
ctx.strokeStyle = ac; ctx.lineWidth=1;
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
}
}
}
ctx.globalAlpha = 1;
ctx.fillStyle = ac;
for (const p of pts){
ctx.beginPath(); ctx.arc(p.x,p.y,1.8,0,Math.PI*2); ctx.fill();
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,58 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['counter-explosion'] = function(el){
const U = window.HPX._u;
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const target = parseInt(el.getAttribute('data-fx-to') || '2400', 10);
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
// number overlay
const num = document.createElement('div');
num.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font:900 120px system-ui,sans-serif;color:var(--text-1,#fff);pointer-events:none;text-shadow:0 4px 40px rgba(124,92,255,0.5);';
num.textContent = '0';
el.appendChild(num);
let parts = [];
let state = 'count'; // count | burst | hold
let stateT = 0;
let value = 0;
let cycle = 0;
const burst = () => {
const cx = k.w/2, cy = k.h/2;
for (let i=0;i<120;i++){
const a = Math.random()*Math.PI*2;
const s = U.rand(120, 400);
parts.push({x:cx,y:cy,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,r:U.rand(2,5),c:pal[(Math.random()*pal.length)|0]});
}
};
const stop = U.loop(() => {
ctx.clearRect(0,0,k.w,k.h);
const dt = 1/60;
stateT += dt;
if (state === 'count'){
const dur = 2.2;
const p = Math.min(1, stateT/dur);
const eased = 1 - Math.pow(1-p,3);
value = Math.round(target*eased);
num.textContent = value.toLocaleString();
if (p >= 1){ state='burst'; stateT=0; burst(); }
} else if (state === 'burst'){
if (stateT > 0.05 && stateT < 0.3 && parts.length < 200) {}
if (stateT > 2.5){ state='hold'; stateT=0; }
} else if (state === 'hold'){
if (stateT > 1.5){
state='count'; stateT=0; value=0; num.textContent='0'; cycle++;
}
}
parts = parts.filter(p => p.life > 0);
for (const p of parts){
p.vy += 260*dt; p.vx *= 0.985; p.vy *= 0.985;
p.x += p.vx*dt; p.y += p.vy*dt; p.life -= 0.01;
ctx.globalAlpha = Math.max(0,p.life);
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); if (num.parentNode) num.parentNode.removeChild(num); } };
};
})();
@@ -0,0 +1,45 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['data-stream'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const ac = U.accent(el,'#22d3ee'), ac2 = U.accent2(el,'#7c5cff');
const rows = [];
const rh = 22;
const genRow = (y) => ({
y, dir: Math.random()<0.5?-1:1,
speed: U.rand(30, 90),
offset: Math.random()*2000,
text: Array.from({length:120}, () => {
const r = Math.random();
if (r<0.3) return Math.random()<0.5?'0':'1';
if (r<0.6) return '0x' + Math.floor(Math.random()*256).toString(16).padStart(2,'0');
return Math.random().toString(16).slice(2,6);
}).join(' ')
});
const init = () => {
rows.length = 0;
const n = Math.ceil(k.h/rh);
for (let i=0;i<n;i++) rows.push(genRow(i*rh + rh*0.7));
};
init();
let lh = k.h;
const stop = U.loop((t) => {
if (k.h!==lh){ init(); lh=k.h; }
ctx.fillStyle = 'rgba(5,8,14,0.35)';
ctx.fillRect(0,0,k.w,k.h);
ctx.font = '13px ui-monospace,Menlo,monospace';
for (let i=0;i<rows.length;i++){
const r = rows[i];
const x = r.dir>0
? ((t*r.speed + r.offset) % (k.w+400)) - 400
: k.w - (((t*r.speed + r.offset) % (k.w+400)) - 400);
ctx.fillStyle = (i%3===0)?ac:ac2;
ctx.globalAlpha = 0.65 + (i%2)*0.3;
ctx.fillText(r.text, x, r.y);
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,51 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['firework'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
let rockets = [], sparks = [];
const launch = () => {
rockets.push({
x: U.rand(k.w*0.2, k.w*0.8), y: k.h+10,
vx: U.rand(-30,30), vy: U.rand(-520,-380),
tgtY: U.rand(k.h*0.15, k.h*0.45),
c: pal[(Math.random()*pal.length)|0]
});
};
const burst = (x, y, c) => {
const n = 70;
for (let i=0;i<n;i++){
const a = Math.random()*Math.PI*2;
const s = U.rand(60, 240);
sparks.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,c});
}
};
let last = -1;
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(0,0,0,0.18)';
ctx.fillRect(0,0,k.w,k.h);
if (t - last > 0.7) { launch(); last = t; }
const dt = 1/60;
rockets = rockets.filter(r => {
r.x += r.vx*dt; r.y += r.vy*dt; r.vy += 260*dt;
ctx.fillStyle = r.c;
ctx.beginPath(); ctx.arc(r.x, r.y, 2.5, 0, Math.PI*2); ctx.fill();
if (r.y <= r.tgtY || r.vy >= 0) { burst(r.x, r.y, r.c); return false; }
return true;
});
sparks = sparks.filter(p => p.life > 0);
for (const p of sparks){
p.vy += 90*dt;
p.vx *= 0.98; p.vy *= 0.98;
p.x += p.vx*dt; p.y += p.vy*dt;
p.life -= 0.012;
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(p.x, p.y, 2, 0, Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,33 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['galaxy-swirl'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const N = 800;
const parts = Array.from({length:N}, (_,i) => {
const arm = i%3;
const t = Math.random();
const r = t*180 + 8;
const base = (arm/3)*Math.PI*2;
return { r, a: base + Math.log(r+1)*1.6 + U.rand(-0.2,0.2),
c: pal[arm%pal.length],
s: U.rand(0.8, 2.2) };
});
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillRect(0,0,k.w,k.h);
const cx=k.w/2, cy=k.h/2;
for (const p of parts){
const a = p.a + t*0.15;
const x = cx + Math.cos(a)*p.r;
const y = cy + Math.sin(a)*p.r*0.7;
ctx.fillStyle = p.c;
ctx.globalAlpha = 0.7;
ctx.beginPath(); ctx.arc(x,y,p.s,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,39 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['gradient-blob'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const blobs = Array.from({length:4}, (_,i) => ({
x: U.rand(0,1), y: U.rand(0,1),
vx: U.rand(-0.08,0.08), vy: U.rand(-0.08,0.08),
r: U.rand(180,320),
c: pal[i%pal.length]
}));
const hex2rgb = (h) => {
const m = h.replace('#','').match(/.{2}/g);
if (!m) return [124,92,255];
return m.map(x=>parseInt(x,16));
};
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(10,12,22,0.2)';
ctx.fillRect(0,0,k.w,k.h);
ctx.globalCompositeOperation = 'lighter';
for (const b of blobs){
b.x += b.vx*0.01; b.y += b.vy*0.01;
if (b.x<0||b.x>1) b.vx*=-1;
if (b.y<0||b.y>1) b.vy*=-1;
const px = b.x*k.w, py = b.y*k.h;
const r = b.r + Math.sin(t*0.8 + b.x*6)*30;
const [R,G,B] = hex2rgb(b.c);
const grad = ctx.createRadialGradient(px,py,0,px,py,r);
grad.addColorStop(0, `rgba(${R},${G},${B},0.55)`);
grad.addColorStop(1, `rgba(${R},${G},${B},0)`);
ctx.fillStyle = grad;
ctx.beginPath(); ctx.arc(px,py,r,0,Math.PI*2); ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,69 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['knowledge-graph'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const tx = U.text(el, '#e7e7ef');
const labels = ['AI','ML','LLM','Graph','Node','Edge','Claude','GPT','RAG','Vector',
'Embed','Neural','Agent','Tool','Memory','Logic','Data','Train','Infer','Token',
'Prompt','Chain','Plan','Skill','Cloud','Edge','GPU','Code','Task','Flow'];
const N = 28;
const nodes = Array.from({length:N}, (_,i) => ({
x: U.rand(40, 300), y: U.rand(40, 200),
vx: 0, vy: 0, label: labels[i%labels.length],
c: pal[i%pal.length]
}));
const edges = [];
const made = new Set();
while (edges.length < 50){
const a = (Math.random()*N)|0, b = (Math.random()*N)|0;
if (a===b) continue;
const key = a<b ? a+'-'+b : b+'-'+a;
if (made.has(key)) continue;
made.add(key); edges.push([a,b]);
}
const stop = U.loop(() => {
// physics
for (let i=0;i<N;i++){
for (let j=i+1;j<N;j++){
const a=nodes[i], b=nodes[j];
const dx=b.x-a.x, dy=b.y-a.y;
let d2=dx*dx+dy*dy; if (d2<1) d2=1;
const d=Math.sqrt(d2);
const f=1600/d2;
const fx=(dx/d)*f, fy=(dy/d)*f;
a.vx-=fx; a.vy-=fy; b.vx+=fx; b.vy+=fy;
}
}
for (const [i,j] of edges){
const a=nodes[i], b=nodes[j];
const dx=b.x-a.x, dy=b.y-a.y, d=Math.hypot(dx,dy)||1;
const f=(d-90)*0.008;
const fx=(dx/d)*f, fy=(dy/d)*f;
a.vx+=fx; a.vy+=fy; b.vx-=fx; b.vy-=fy;
}
const cx=k.w/2, cy=k.h/2;
for (const n of nodes){
n.vx += (cx-n.x)*0.002;
n.vy += (cy-n.y)*0.002;
n.vx *= 0.85; n.vy *= 0.85;
n.x += n.vx; n.y += n.vy;
}
ctx.clearRect(0,0,k.w,k.h);
ctx.strokeStyle = 'rgba(180,180,220,0.25)'; ctx.lineWidth=1;
for (const [i,j] of edges){
const a=nodes[i], b=nodes[j];
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
}
ctx.font='11px system-ui,sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
for (const n of nodes){
ctx.fillStyle = n.c;
ctx.beginPath(); ctx.arc(n.x,n.y,7,0,Math.PI*2); ctx.fill();
ctx.fillStyle = tx;
ctx.fillText(n.label, n.x, n.y-14);
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,50 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['letter-explode'] = function(el){
const U = window.HPX._u;
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const src = el.querySelector('[data-fx-text]') || el;
const text = (el.getAttribute('data-fx-text-value') || src.textContent || 'EXPLODE').trim();
// Build a container, hide source text
const wrap = document.createElement('div');
wrap.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none;';
const inner = document.createElement('div');
inner.style.cssText = 'font-size:64px;font-weight:900;letter-spacing:0.02em;color:var(--text-1,#fff);white-space:nowrap;';
wrap.appendChild(inner);
el.appendChild(wrap);
const spans = [];
for (const ch of text){
const s = document.createElement('span');
s.textContent = ch === ' ' ? '\u00A0' : ch;
s.style.display='inline-block';
s.style.transform='translate(0,0)';
s.style.transition='transform 900ms cubic-bezier(.2,.9,.3,1), opacity 900ms';
s.style.opacity='0';
inner.appendChild(s);
spans.push(s);
}
let stopped = false;
const run = () => {
if (stopped) return;
spans.forEach((s,i) => {
const dx = U.rand(-400, 400), dy = U.rand(-300, 300);
s.style.transition='none';
s.style.transform=`translate(${dx}px,${dy}px) rotate(${U.rand(-180,180)}deg)`;
s.style.opacity='0';
});
// force reflow
void inner.offsetWidth;
spans.forEach((s,i) => {
setTimeout(() => {
if (stopped) return;
s.style.transition='transform 900ms cubic-bezier(.2,.9,.3,1), opacity 900ms';
s.style.transform='translate(0,0) rotate(0deg)';
s.style.opacity='1';
}, i*35);
});
};
run();
const iv = setInterval(run, 4500);
return { stop(){ stopped=true; clearInterval(iv); if (wrap.parentNode) wrap.parentNode.removeChild(wrap); } };
};
})();
@@ -0,0 +1,40 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['magnetic-field'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const N = 60;
const parts = Array.from({length:N}, (_,i) => ({
phase: Math.random()*Math.PI*2,
freq: U.rand(0.4, 1.2),
amp: U.rand(30, 90),
y0: U.rand(0.15, 0.85),
c: pal[i%pal.length],
trail: []
}));
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(0,0,0,0.08)';
ctx.fillRect(0,0,k.w,k.h);
for (const p of parts){
const x = ((t*80 + p.phase*50) % (k.w+100)) - 50;
const y = k.h*p.y0 + Math.sin(x*0.02 + p.phase + t*p.freq)*p.amp;
p.trail.push([x,y]);
if (p.trail.length > 18) p.trail.shift();
ctx.strokeStyle = p.c;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i=0;i<p.trail.length;i++){
const [tx,ty] = p.trail[i];
if (i===0) ctx.moveTo(tx,ty); else ctx.lineTo(tx,ty);
}
ctx.globalAlpha = 0.7;
ctx.stroke();
ctx.globalAlpha = 1;
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(x,y,2.5,0,Math.PI*2); ctx.fill();
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,33 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['matrix-rain'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const glyphs = 'アイウエオカキクケコサシスセソタチツテトナニヌネノ0123456789ABCDEF'.split('');
const fs = 16;
let cols = 0, drops = [];
const init = () => {
cols = Math.ceil(k.w/fs);
drops = Array.from({length:cols}, () => U.rand(-20, 0));
};
init();
let lw = k.w, lh = k.h;
const stop = U.loop(() => {
if (k.w!==lw || k.h!==lh){ init(); lw=k.w; lh=k.h; }
ctx.fillStyle = 'rgba(0,0,0,0.08)';
ctx.fillRect(0,0,k.w,k.h);
ctx.font = fs+'px monospace';
for (let i=0;i<cols;i++){
const ch = glyphs[(Math.random()*glyphs.length)|0];
const x = i*fs, y = drops[i]*fs;
ctx.fillStyle = '#9fffc9';
ctx.fillText(ch, x, y);
ctx.fillStyle = '#00ff6a';
ctx.fillText(ch, x, y - fs);
drops[i] += 1;
if (y > k.h && Math.random() > 0.975) drops[i] = 0;
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,75 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['neural-net'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const ac = U.accent(el,'#7c5cff'), ac2 = U.accent2(el,'#22d3ee');
const layers = [4,6,6,3];
let nodes = [], edges = [], pulses = [];
const layout = () => {
nodes = [];
const pad = 40;
const cw = k.w - pad*2, ch = k.h - pad*2;
for (let L=0; L<layers.length; L++){
const x = pad + (cw * L / (layers.length-1));
const n = layers[L];
for (let i=0;i<n;i++){
const y = pad + (ch * (i+0.5) / n);
nodes.push({x,y,L,i});
}
}
edges = [];
for (let L=0; L<layers.length-1; L++){
const a = nodes.filter(n=>n.L===L), b = nodes.filter(n=>n.L===L+1);
for (const x of a) for (const y of b) edges.push([nodes.indexOf(x),nodes.indexOf(y)]);
}
};
layout();
let lw=k.w, lh=k.h, last=0;
const stop = U.loop((t) => {
if (k.w!==lw||k.h!==lh){ layout(); lw=k.w; lh=k.h; }
ctx.clearRect(0,0,k.w,k.h);
ctx.strokeStyle = 'rgba(160,160,200,0.22)'; ctx.lineWidth=1;
for (const [i,j] of edges){
const a=nodes[i], b=nodes[j];
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
}
if (t - last > 0.25){
last = t;
const starts = nodes.filter(n=>n.L===0);
const s = starts[(Math.random()*starts.length)|0];
pulses.push({node:s, L:0, t:0});
}
pulses = pulses.filter(p => p.L < layers.length-1);
for (const p of pulses){
p.t += 0.03;
if (p.t >= 1){
const next = nodes.filter(n=>n.L===p.L+1);
p.node2 = next[(Math.random()*next.length)|0];
if (!p._started){ p._started = true; }
}
}
// animate progression
for (const p of pulses){
if (!p.target){
const next = nodes.filter(n=>n.L===p.L+1);
p.target = next[(Math.random()*next.length)|0];
}
p.t += 0.04;
const a = p.node, b = p.target;
const x = a.x + (b.x-a.x)*Math.min(1,p.t);
const y = a.y + (b.y-a.y)*Math.min(1,p.t);
ctx.fillStyle = ac2;
ctx.beginPath(); ctx.arc(x,y,4,0,Math.PI*2); ctx.fill();
if (p.t >= 1){ p.node = b; p.target=null; p.L++; p.t=0; }
}
for (const n of nodes){
ctx.fillStyle = ac;
ctx.beginPath(); ctx.arc(n.x,n.y,6,0,Math.PI*2); ctx.fill();
ctx.strokeStyle = ac2; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.arc(n.x,n.y,8,0,Math.PI*2); ctx.stroke();
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,38 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['orbit-ring'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const rings = [
{r:40, n:3, sp:1.2, c:pal[0]},
{r:75, n:5, sp:0.8, c:pal[1]},
{r:110, n:8, sp:-0.6, c:pal[2]},
{r:145, n:12, sp:0.4, c:pal[3]},
{r:180, n:16, sp:-0.3, c:pal[4]}
];
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
const cx=k.w/2, cy=k.h/2;
// radial glow
const g = ctx.createRadialGradient(cx,cy,0,cx,cy,210);
g.addColorStop(0,'rgba(124,92,255,0.25)');
g.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle = g; ctx.fillRect(0,0,k.w,k.h);
for (const R of rings){
ctx.strokeStyle = 'rgba(200,200,230,0.2)'; ctx.lineWidth=1;
ctx.beginPath(); ctx.arc(cx,cy,R.r,0,Math.PI*2); ctx.stroke();
for (let i=0;i<R.n;i++){
const a = (i/R.n)*Math.PI*2 + t*R.sp;
const x = cx + Math.cos(a)*R.r;
const y = cy + Math.sin(a)*R.r;
ctx.fillStyle = R.c;
ctx.beginPath(); ctx.arc(x,y,4,0,Math.PI*2); ctx.fill();
}
}
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(cx,cy,5,0,Math.PI*2); ctx.fill();
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,42 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['particle-burst'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
let parts = [];
const spawn = () => {
const cx = k.w/2, cy = k.h/2;
const n = 90;
for (let i=0;i<n;i++){
const a = Math.random()*Math.PI*2;
const s = U.rand(80, 260);
parts.push({
x: cx, y: cy,
vx: Math.cos(a)*s, vy: Math.sin(a)*s,
life: 1, r: U.rand(2,5),
c: pal[(Math.random()*pal.length)|0]
});
}
};
spawn();
let lastSpawn = 0;
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
if (t - lastSpawn > 2.5) { spawn(); lastSpawn = t; }
const dt = 1/60;
parts = parts.filter(p => p.life > 0);
for (const p of parts){
p.vy += 220*dt;
p.vx *= 0.985; p.vy *= 0.985;
p.x += p.vx*dt; p.y += p.vy*dt;
p.life -= 0.012;
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,39 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['shockwave'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const ac = U.accent(el,'#7c5cff'), ac2 = U.accent2(el,'#22d3ee');
let waves = [];
let last = -1;
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(0,0,0,0.12)';
ctx.fillRect(0,0,k.w,k.h);
if (t - last > 0.6){ last = t; waves.push({t:0}); }
const cx=k.w/2, cy=k.h/2;
const max = Math.hypot(k.w,k.h)/2;
waves = waves.filter(w => w.t < 1);
for (const w of waves){
w.t += 0.012;
const r = w.t * max;
const alpha = 1 - w.t;
ctx.strokeStyle = w.t<0.5?ac2:ac;
ctx.globalAlpha = alpha;
ctx.lineWidth = 3 + (1-w.t)*3;
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.stroke();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.globalAlpha = alpha*0.4;
ctx.beginPath(); ctx.arc(cx,cy,r*0.92,0,Math.PI*2); ctx.stroke();
}
ctx.globalAlpha = 1;
// core
const g = ctx.createRadialGradient(cx,cy,0,cx,cy,40);
g.addColorStop(0,'rgba(255,255,255,0.9)');
g.addColorStop(1,'rgba(124,92,255,0)');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(cx,cy,40,0,Math.PI*2); ctx.fill();
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,62 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['sparkle-trail'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
k.c.style.pointerEvents = 'none';
el.style.cursor = 'crosshair';
const pal = U.palette(el);
let sparks = [];
const onMove = (e) => {
const r = el.getBoundingClientRect();
const x = e.clientX - r.left, y = e.clientY - r.top;
for (let i=0;i<3;i++){
sparks.push({
x, y,
vx: U.rand(-60,60), vy: U.rand(-80,20),
life: 1, c: pal[(Math.random()*pal.length)|0],
r: U.rand(1.5,3.5)
});
}
};
// auto-wiggle if no mouse moves
let auto = true, autoT = 0;
const onAny = () => { auto = false; };
el.addEventListener('pointermove', onMove);
el.addEventListener('pointerenter', onAny);
const stop = U.loop(() => {
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillRect(0,0,k.w,k.h);
if (auto){
autoT += 0.04;
const x = k.w/2 + Math.cos(autoT)*k.w*0.3;
const y = k.h/2 + Math.sin(autoT*1.3)*k.h*0.3;
for (let i=0;i<3;i++){
sparks.push({
x, y,
vx: U.rand(-60,60), vy: U.rand(-80,20),
life: 1, c: pal[(Math.random()*pal.length)|0],
r: U.rand(1.5,3.5)
});
}
}
const dt = 1/60;
sparks = sparks.filter(s => s.life > 0);
for (const s of sparks){
s.vy += 160*dt;
s.x += s.vx*dt; s.y += s.vy*dt;
s.life -= 0.018;
ctx.globalAlpha = Math.max(0, s.life);
ctx.fillStyle = s.c;
ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){
el.removeEventListener('pointermove', onMove);
el.removeEventListener('pointerenter', onAny);
el.style.cursor = '';
stop(); k.destroy();
}};
};
})();
@@ -0,0 +1,30 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['starfield'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const tx = U.text(el, '#ffffff');
const N = 260;
const stars = Array.from({length:N}, () => ({
x: U.rand(-1,1), y: U.rand(-1,1), z: Math.random()
}));
const stop = U.loop(() => {
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.fillRect(0,0,k.w,k.h);
const cx = k.w/2, cy = k.h/2;
for (const s of stars){
s.z -= 0.006;
if (s.z <= 0.02) { s.x = U.rand(-1,1); s.y = U.rand(-1,1); s.z = 1; }
const px = cx + (s.x/s.z)*cx;
const py = cy + (s.y/s.z)*cy;
if (px<0||py<0||px>k.w||py>k.h) continue;
const r = (1-s.z)*2.4;
ctx.globalAlpha = 1-s.z;
ctx.fillStyle = tx;
ctx.beginPath(); ctx.arc(px,py,r,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();
@@ -0,0 +1,51 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['typewriter-multi'] = function(el){
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const lines = [
(el.getAttribute('data-fx-line1') || '> initializing knowledge graph...'),
(el.getAttribute('data-fx-line2') || '> loading 28 concept nodes'),
(el.getAttribute('data-fx-line3') || '> agent ready. awaiting prompt_'),
];
const wrap = document.createElement('div');
wrap.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;gap:14px;padding:32px 48px;font:600 22px ui-monospace,Menlo,monospace;color:var(--text-1,#e7e7ef);';
el.appendChild(wrap);
const rows = lines.map((txt) => {
const row = document.createElement('div');
row.style.cssText = 'white-space:pre;display:flex;align-items:center;';
const span = document.createElement('span'); span.textContent = '';
const cur = document.createElement('span');
cur.textContent = '\u2588';
cur.style.cssText = 'display:inline-block;margin-left:2px;color:var(--accent,#22d3ee);animation:hpxBlink 1s steps(2) infinite;';
row.appendChild(span); row.appendChild(cur);
wrap.appendChild(row);
return {row, span, txt, i:0};
});
// inject blink keyframes once
if (!document.getElementById('hpx-blink-kf')){
const st = document.createElement('style');
st.id = 'hpx-blink-kf';
st.textContent = '@keyframes hpxBlink{50%{opacity:0}}';
document.head.appendChild(st);
}
let stopped = false;
const speeds = [55, 70, 45];
rows.forEach((r, idx) => {
const tick = () => {
if (stopped) return;
if (r.i < r.txt.length){
r.span.textContent += r.txt[r.i++];
setTimeout(tick, speeds[idx]);
} else {
setTimeout(() => {
if (stopped) return;
r.i = 0; r.span.textContent = '';
tick();
}, 2200);
}
};
setTimeout(tick, idx*400);
});
return { stop(){ stopped = true; if (wrap.parentNode) wrap.parentNode.removeChild(wrap); } };
};
})();
@@ -0,0 +1,47 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['word-cascade'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const WORDS = ['AI','知识','Graph','Claude','LLM','Agent','Vector','RAG','Token','神经',
'Prompt','Chain','Skill','Code','Cloud','GPU','Flow','推理','Data','Model'];
let items = [];
let last = -1;
let piles = {}; // column -> stack height
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
if (t - last > 0.18){
last = t;
const w = WORDS[(Math.random()*WORDS.length)|0];
items.push({
text: w, x: U.rand(40, k.w-40), y: -20,
vy: 0, c: pal[(Math.random()*pal.length)|0],
size: U.rand(16,26), landed: false
});
}
ctx.textAlign='center'; ctx.textBaseline='middle';
for (const it of items){
if (!it.landed){
it.vy += 0.4;
it.y += it.vy;
const col = Math.round(it.x/60);
const floor = k.h - (piles[col]||0) - it.size*0.6;
if (it.y >= floor){
it.y = floor; it.landed = true;
piles[col] = (piles[col]||0) + it.size*1.1;
if ((piles[col]||0) > k.h*0.8) piles[col] = 0; // reset if too high
}
}
ctx.fillStyle = it.c;
ctx.font = `700 ${it.size}px system-ui,sans-serif`;
ctx.fillText(it.text, it.x, it.y);
}
// prune old landed
if (items.length > 120){
items = items.filter(i => !i.landed).concat(items.filter(i=>i.landed).slice(-60));
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();
+150
View File
@@ -0,0 +1,150 @@
/* html-ppt :: base.css — reset + shared tokens + layout primitives */
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
:root {
--bg: #ffffff;
--bg-soft: #f7f7f8;
--surface: #ffffff;
--surface-2: #f2f2f4;
--border: rgba(0,0,0,.08);
--border-strong: rgba(0,0,0,.16);
--text-1: #111216;
--text-2: #55596a;
--text-3: #8a8f9e;
--accent: #3b6cff;
--accent-2: #7a5cff;
--accent-3: #ff5c8a;
--good: #1aaf6c;
--warn: #f5a524;
--bad: #e0445a;
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
--radius: 18px;
--radius-sm: 12px;
--radius-lg: 26px;
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
--font-display: var(--font-sans);
--letter-tight: -.03em;
--letter-normal: -.01em;
--ease: cubic-bezier(.4,0,.2,1);
}
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
font-family:var(--font-sans);font-weight:400;line-height:1.6;
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
letter-spacing:var(--letter-normal)}
img,svg,video{max-width:100%;display:block}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
code,kbd,pre,samp{font-family:var(--font-mono)}
/* ================= SLIDE SYSTEM ================= */
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
.slide{
position:absolute;inset:0;
display:flex;flex-direction:column;justify-content:center;
padding:72px 96px;
box-sizing:border-box;
opacity:0;pointer-events:none;
transition:opacity .5s var(--ease), transform .5s var(--ease);
transform:translateX(30px);
overflow:hidden;
}
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
.slide.is-prev{transform:translateX(-30px)}
/* single-page standalone (used when a layout file is opened directly) */
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
/* ================= TYPOGRAPHY ================= */
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
.dim{color:var(--text-2)}
.dim2{color:var(--text-3)}
.mono{font-family:var(--font-mono)}
.serif{font-family:var(--font-serif)}
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
/* ================= LAYOUT PRIMITIVES ================= */
.stack>*+*{margin-top:14px}
.row{display:flex;gap:24px;align-items:center}
.row.wrap{flex-wrap:wrap}
.grid{display:grid;gap:24px}
.g2{grid-template-columns:repeat(2,1fr)}
.g3{grid-template-columns:repeat(3,1fr)}
.g4{grid-template-columns:repeat(4,1fr)}
.center{display:flex;align-items:center;justify-content:center;text-align:center}
.fill{flex:1}
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
/* ================= CARDS ================= */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
/* ================= BARS / DIVIDERS ================= */
.divider{height:1px;background:var(--border);width:100%}
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
/* ================= CHROME (header/footer/progress) ================= */
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
.slide-number::before{content:attr(data-current)}
.slide-number::after{content:" / " attr(data-total)}
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
/* ================= PRESENTER / OVERVIEW ================= */
.notes{display:none!important}
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
.notes-overlay.open{transform:translateY(0)}
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
display:none;padding:40px;overflow:auto}
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
font-size:11px;transition:transform .2s var(--ease)}
.overview .thumb:hover{transform:scale(1.04)}
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
/* ================= PRESENTER VIEW ================= */
/* Presenter view opens in a separate popup window (S key).
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
* The audience window (this file) is NOT affected — it stays as normal deck view.
* Only the .notes class below is needed to hide speaker notes from audience. */
/* ================= UTILITY ================= */
.hidden{display:none!important}
.nowrap{white-space:nowrap}
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
.uppercase{text-transform:uppercase;letter-spacing:.12em}
/* ================= PRINT ================= */
@media print{
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
}
+9
View File
@@ -0,0 +1,9 @@
/* html-ppt :: shared webfonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
+960
View File
@@ -0,0 +1,960 @@
/* html-ppt :: runtime.js
* Keyboard-driven deck runtime. Zero dependencies.
*
* Features:
* ← → / space / PgUp PgDn / Home End navigation
* F fullscreen
* S presenter mode (opens a NEW WINDOW with current/next slide preview + notes + timer)
* The original window stays as audience view, synced via BroadcastChannel.
* Slide previews use CSS transform:scale() at design resolution for pixel-perfect layout.
* N quick notes overlay (bottom drawer)
* O slide overview grid
* T cycle themes (reads data-themes on <html> or <body>)
* A cycle demo animation on current slide
* URL hash #/N deep-link to slide N (1-based)
* Progress bar auto-managed
*/
(function () {
'use strict';
const ANIMS = ['fade-up','fade-down','fade-left','fade-right','rise-in','drop-in',
'zoom-pop','blur-in','glitch-in','typewriter','neon-glow','shimmer-sweep',
'gradient-flow','stagger-list','counter-up','path-draw','parallax-tilt',
'card-flip-3d','cube-rotate-3d','page-turn-3d','perspective-zoom',
'marquee-scroll','kenburns','confetti-burst','spotlight','morph-shape','ripple-reveal'];
function ready(fn){ if(document.readyState!='loading')fn(); else document.addEventListener('DOMContentLoaded',fn);}
/* ========== Parse URL for preview-only mode ==========
* When loaded as iframe.src = "index.html?preview=3", runtime enters a
* locked single-slide mode: only slide N is visible, no chrome, no keys,
* no hash updates. This is how the presenter window shows pixel-perfect
* previews — by loading the actual deck file in an iframe and telling it
* to display only a specific slide.
*/
function getPreviewIdx() {
const m = /[?&]preview=(\d+)/.exec(location.search || '');
return m ? parseInt(m[1], 10) - 1 : -1;
}
ready(function () {
const deck = document.querySelector('.deck');
if (!deck) return;
const slides = Array.from(deck.querySelectorAll('.slide'));
if (!slides.length) return;
const previewOnlyIdx = getPreviewIdx();
const isPreviewMode = previewOnlyIdx >= 0 && previewOnlyIdx < slides.length;
/* ===== Preview-only mode: show one slide, hide everything else ===== */
if (isPreviewMode) {
function showSlide(i) {
slides.forEach((s, j) => {
const active = (j === i);
s.classList.toggle('is-active', active);
s.style.display = active ? '' : 'none';
if (active) {
s.style.opacity = '1';
s.style.transform = 'none';
s.style.pointerEvents = 'auto';
}
});
}
showSlide(previewOnlyIdx);
/* Hide chrome that the presenter shouldn't see in preview */
const hideSel = '.progress-bar, .notes-overlay, .overview, .notes, aside.notes, .speaker-notes';
document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; });
document.documentElement.setAttribute('data-preview', '1');
document.body.setAttribute('data-preview', '1');
/* Auto-detect theme base path for theme switching in preview mode */
function getPreviewThemeBase() {
const base = document.documentElement.getAttribute('data-theme-base');
if (base) return base;
const tl = document.getElementById('theme-link');
if (tl) {
const raw = tl.getAttribute('href') || '';
const ls = raw.lastIndexOf('/');
if (ls >= 0) return raw.substring(0, ls + 1);
}
return 'assets/themes/';
}
const previewThemeBase = getPreviewThemeBase();
/* Listen for postMessage from parent presenter window:
* - preview-goto: switch visible slide WITHOUT reloading
* - preview-theme: switch theme CSS link to match audience window */
window.addEventListener('message', function(e) {
if (!e.data) return;
if (e.data.type === 'preview-goto') {
const n = parseInt(e.data.idx, 10);
if (n >= 0 && n < slides.length) showSlide(n);
} else if (e.data.type === 'preview-theme' && e.data.name) {
let link = document.getElementById('theme-link');
if (!link) {
link = document.createElement('link');
link.rel = 'stylesheet';
link.id = 'theme-link';
document.head.appendChild(link);
}
link.href = previewThemeBase + e.data.name + '.css';
document.documentElement.setAttribute('data-theme', e.data.name);
}
});
/* Signal to parent that preview iframe is ready */
try { window.parent && window.parent.postMessage({ type: 'preview-ready' }, '*'); } catch(e) {}
return;
}
let idx = 0;
const total = slides.length;
/* ===== BroadcastChannel for presenter sync ===== */
const CHANNEL_NAME = 'html-ppt-presenter-' + location.pathname;
let bc;
try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) { bc = null; }
// Are we running inside the presenter popup? (legacy flag, now unused)
const isPresenterWindow = false;
/* ===== progress bar ===== */
let bar = document.querySelector('.progress-bar');
if (!bar) {
bar = document.createElement('div');
bar.className = 'progress-bar';
bar.innerHTML = '<span></span>';
document.body.appendChild(bar);
}
const barFill = bar.querySelector('span');
/* ===== notes overlay (N key) ===== */
let notes = document.querySelector('.notes-overlay');
if (!notes) {
notes = document.createElement('div');
notes.className = 'notes-overlay';
document.body.appendChild(notes);
}
/* ===== overview grid (O key) ===== */
let overview = document.querySelector('.overview');
if (!overview) {
overview = document.createElement('div');
overview.className = 'overview';
slides.forEach((s, i) => {
const t = document.createElement('div');
t.className = 'thumb';
// Force 16:9 aspect ratio robustly
t.style.padding = '0 0 56.25% 0';
t.style.height = '0';
t.style.position = 'relative';
t.style.overflow = 'hidden';
const title = s.getAttribute('data-title') ||
(s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1));
// Create a container for the mini-slide
const mini = document.createElement('div');
mini.className = 'mini-slide';
mini.style.position = 'absolute';
mini.style.top = '0';
mini.style.left = '0';
mini.style.width = '1920px';
mini.style.height = '1080px';
mini.style.transformOrigin = 'top left';
mini.style.pointerEvents = 'none';
mini.style.background = 'var(--bg)';
// Clone the slide content
const clone = s.cloneNode(true);
clone.className = 'slide is-active'; // force active styles
clone.style.position = 'absolute';
clone.style.inset = '0';
clone.style.transform = 'none';
clone.style.opacity = '1';
clone.style.padding = '72px 96px'; // ensure padding is kept
mini.appendChild(clone);
t.appendChild(mini);
// Add the number and title overlay
const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.inset = '0';
overlay.style.background = 'linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, transparent 40%, transparent 60%, rgba(0,0,0,0.8) 100%)';
overlay.style.color = '#fff';
overlay.style.zIndex = '10';
overlay.style.pointerEvents = 'none';
const n = document.createElement('div');
n.className = 'n';
n.textContent = i + 1;
n.style.position = 'absolute';
n.style.top = '12px';
n.style.left = '16px';
n.style.fontWeight = '700';
n.style.fontSize = '16px';
n.style.color = '#fff';
n.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)';
const text = document.createElement('div');
text.className = 't';
text.textContent = title.trim().slice(0,80);
text.style.position = 'absolute';
text.style.bottom = '12px';
text.style.left = '16px';
text.style.right = '16px';
text.style.fontWeight = '600';
text.style.fontSize = '14px';
text.style.color = '#fff';
text.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)';
overlay.appendChild(n);
overlay.appendChild(text);
t.appendChild(overlay);
t.addEventListener('click', () => { go(i); toggleOverview(false); });
overview.appendChild(t);
});
document.body.appendChild(overview);
}
/* ===== navigation ===== */
function go(n, fromRemote){
n = Math.max(0, Math.min(total-1, n));
slides.forEach((s,i) => {
s.classList.toggle('is-active', i===n);
s.classList.toggle('is-prev', i<n);
});
idx = n;
barFill.style.width = ((n+1)/total*100)+'%';
const numEl = document.querySelector('.slide-number');
if (numEl) { numEl.setAttribute('data-current', n+1); numEl.setAttribute('data-total', total); }
// notes (bottom overlay)
const note = slides[n].querySelector('.notes, aside.notes, .speaker-notes');
notes.innerHTML = note ? note.innerHTML : '';
// hash
const hashTarget = '#/'+(n+1);
if (location.hash !== hashTarget && !isPresenterWindow) {
history.replaceState(null,'', hashTarget);
}
// re-trigger entry animations
slides[n].querySelectorAll('[data-anim]').forEach(el => {
const a = el.getAttribute('data-anim');
el.classList.remove('anim-'+a);
void el.offsetWidth;
el.classList.add('anim-'+a);
});
// counter-up
slides[n].querySelectorAll('.counter').forEach(el => {
const target = parseFloat(el.getAttribute('data-to')||el.textContent);
const dur = parseInt(el.getAttribute('data-dur')||'1200',10);
const start = performance.now();
const from = 0;
function tick(now){
const t = Math.min(1,(now-start)/dur);
const v = from + (target-from)*(1-Math.pow(1-t,3));
el.textContent = (target % 1 === 0) ? Math.round(v) : v.toFixed(1);
if (t<1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
// Broadcast to other window (audience ↔ presenter)
if (!fromRemote && bc) {
bc.postMessage({ type: 'go', idx: n });
}
}
/* ===== listen for remote navigation / theme changes ===== */
if (bc) {
bc.onmessage = function(e) {
if (!e.data) return;
if (e.data.type === 'go' && typeof e.data.idx === 'number') {
go(e.data.idx, true);
} else if (e.data.type === 'theme' && e.data.name) {
/* Sync theme across windows */
const i = themes.indexOf(e.data.name);
if (i >= 0) themeIdx = i;
applyTheme(e.data.name);
}
};
}
function toggleNotes(force){ notes.classList.toggle('open', force!==undefined?force:!notes.classList.contains('open')); }
function toggleOverview(force){
const isOpen = force!==undefined ? force : !overview.classList.contains('open');
overview.classList.toggle('open', isOpen);
if (isOpen) {
requestAnimationFrame(() => {
const thumbs = overview.querySelectorAll('.thumb');
if (thumbs.length) {
const scale = thumbs[0].clientWidth / 1920;
overview.querySelectorAll('.mini-slide').forEach(m => {
m.style.transform = 'scale(' + scale + ')';
});
}
});
}
}
/* ========== PRESENTER MODE — Magnetic-card popup window ========== */
/* Opens a new window with 4 draggable, resizable cards:
* CURRENT — iframe(?preview=N) pixel-perfect preview of current slide
* NEXT — iframe(?preview=N+1) pixel-perfect preview of next slide
* SCRIPT — large speaker notes (逐字稿)
* TIMER — elapsed timer + page counter + controls
* Cards remember position/size in localStorage.
* Two windows sync via BroadcastChannel.
*/
let presenterWin = null;
function openPresenterWindow() {
if (presenterWin && !presenterWin.closed) {
presenterWin.focus();
return;
}
// Build absolute URL of THIS deck file (without hash/query)
const deckUrl = location.protocol + '//' + location.host + location.pathname;
// Collect slide titles + notes (HTML strings)
const slideMeta = slides.map((s, i) => {
const note = s.querySelector('.notes, aside.notes, .speaker-notes');
return {
title: s.getAttribute('data-title') ||
(s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)),
notes: note ? note.innerHTML : ''
};
});
/* Capture current theme so presenter previews match the audience */
const currentTheme = root.getAttribute('data-theme') || (themes[themeIdx] || '');
const presenterHTML = buildPresenterHTML(deckUrl, slideMeta, total, idx, CHANNEL_NAME, currentTheme);
presenterWin = window.open('', 'html-ppt-presenter', 'width=1280,height=820,menubar=no,toolbar=no');
if (!presenterWin) {
alert('请允许弹出窗口以使用演讲者视图');
return;
}
presenterWin.document.open();
presenterWin.document.write(presenterHTML);
presenterWin.document.close();
}
function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName, currentTheme) {
const metaJSON = JSON.stringify(slideMeta);
const deckUrlJSON = JSON.stringify(deckUrl);
const channelJSON = JSON.stringify(channelName);
const themeJSON = JSON.stringify(currentTheme || '');
const storageKey = 'html-ppt-presenter:' + location.pathname;
// Build the document as a single template string for clarity
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Presenter View</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%; overflow: hidden;
background: #1a1d24;
background-image:
radial-gradient(circle at 20% 30%, rgba(88,166,255,.04), transparent 50%),
radial-gradient(circle at 80% 70%, rgba(188,140,255,.04), transparent 50%);
color: #e6edf3;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
}
/* Stage: positioned area where cards live */
#stage { position: absolute; inset: 0; overflow: hidden; }
/* Magnetic card */
.pcard {
position: absolute;
background: #0d1117;
border: 1px solid rgba(255,255,255,.1);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,.45), 0 0 0 1px rgba(255,255,255,.02);
display: flex; flex-direction: column;
overflow: hidden;
min-width: 180px; min-height: 100px;
transition: box-shadow .2s, border-color .2s;
}
.pcard.dragging { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(88,166,255,.5); border-color: #58a6ff; transition: none; z-index: 9999; }
.pcard.resizing { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(63,185,80,.5); border-color: #3fb950; transition: none; z-index: 9999; }
.pcard:hover { border-color: rgba(88,166,255,.3); }
/* Card header (drag handle) */
.pcard-head {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px;
background: rgba(255,255,255,.04);
border-bottom: 1px solid rgba(255,255,255,.06);
cursor: move;
user-select: none;
flex-shrink: 0;
}
.pcard-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--dot-color, #58a6ff); flex-shrink: 0; }
.pcard-title {
font-size: 11px; letter-spacing: .15em; text-transform: uppercase;
font-weight: 700; color: #8b949e; flex: 1;
}
.pcard-meta { font-size: 11px; color: #6e7681; }
/* Card body */
.pcard-body { flex: 1; position: relative; overflow: hidden; min-height: 0; }
/* Preview cards (CURRENT/NEXT) — iframe-based pixel-perfect render */
.pcard-preview .pcard-body { background: #000; }
.pcard-preview iframe {
position: absolute; top: 0; left: 0;
width: 1920px; height: 1080px;
border: none;
transform-origin: top left;
pointer-events: none;
background: transparent;
}
.pcard-preview .preview-end {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
color: #484f58; font-size: 14px; letter-spacing: .12em;
}
/* Notes card */
.pcard-notes .pcard-body {
padding: 14px 18px;
overflow-y: auto;
font-size: 18px; line-height: 1.75;
color: #d0d7de;
font-family: "Noto Sans SC", -apple-system, sans-serif;
}
.pcard-notes .pcard-body p { margin: 0 0 .7em 0; }
.pcard-notes .pcard-body strong { color: #f0883e; }
.pcard-notes .pcard-body em { color: #58a6ff; font-style: normal; }
.pcard-notes .pcard-body code {
font-family: "SF Mono", monospace; font-size: .9em;
background: rgba(255,255,255,.08); padding: 1px 6px; border-radius: 4px;
}
.pcard-notes .empty { color: #484f58; font-style: italic; }
/* Timer card */
.pcard-timer .pcard-body {
display: flex; flex-direction: column; gap: 14px;
padding: 18px 20px; justify-content: center;
}
.timer-display {
font-family: "SF Mono", "JetBrains Mono", monospace;
font-size: 42px; font-weight: 700;
color: #3fb950;
letter-spacing: .04em;
line-height: 1;
}
.timer-row {
display: flex; align-items: center; gap: 12px;
font-size: 14px; color: #8b949e;
}
.timer-row .label { font-size: 10px; letter-spacing: .15em; text-transform: uppercase; color: #6e7681; }
.timer-row .val { color: #e6edf3; font-weight: 600; font-family: "SF Mono", monospace; }
.timer-controls { display: flex; gap: 8px; flex-wrap: wrap; }
.timer-btn {
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.1);
color: #e6edf3;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
font-family: inherit;
}
.timer-btn:hover { background: rgba(88,166,255,.15); border-color: #58a6ff; }
.timer-btn:active { transform: translateY(1px); }
/* Resize handle */
.pcard-resize {
position: absolute; right: 0; bottom: 0;
width: 18px; height: 18px;
cursor: nwse-resize;
background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,.25) 50%, rgba(255,255,255,.25) 60%, transparent 60%, transparent 70%, rgba(255,255,255,.25) 70%, rgba(255,255,255,.25) 80%, transparent 80%);
z-index: 5;
}
.pcard-resize:hover { background: linear-gradient(135deg, transparent 50%, #58a6ff 50%, #58a6ff 60%, transparent 60%, transparent 70%, #58a6ff 70%, #58a6ff 80%, transparent 80%); }
/* Bottom hint bar */
.hint-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: rgba(0,0,0,.6);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(255,255,255,.08);
padding: 6px 16px;
font-size: 11px; color: #8b949e;
display: flex; gap: 18px; align-items: center;
z-index: 1000;
}
.hint-bar kbd {
background: rgba(255,255,255,.08);
padding: 1px 6px; border-radius: 3px;
font-family: "SF Mono", monospace;
font-size: 10px;
border: 1px solid rgba(255,255,255,.1);
color: #e6edf3;
}
.hint-bar .reset-layout {
margin-left: auto;
background: transparent; border: 1px solid rgba(255,255,255,.15);
color: #8b949e; padding: 3px 10px; border-radius: 4px;
font-size: 11px; cursor: pointer; font-family: inherit;
}
.hint-bar .reset-layout:hover { background: rgba(248,81,73,.15); border-color: #f85149; color: #f85149; }
body.is-dragging-card * { user-select: none !important; }
body.is-dragging-card iframe { pointer-events: none !important; }
</style>
</head>
<body>
<div id="stage">
<div class="pcard pcard-preview" id="card-cur" style="--dot-color:#58a6ff">
<div class="pcard-head" data-drag>
<span class="pcard-dot"></span>
<span class="pcard-title">CURRENT</span>
<span class="pcard-meta" id="cur-meta">—</span>
</div>
<div class="pcard-body"><iframe id="iframe-cur"></iframe></div>
<div class="pcard-resize" data-resize></div>
</div>
<div class="pcard pcard-preview" id="card-nxt" style="--dot-color:#bc8cff">
<div class="pcard-head" data-drag>
<span class="pcard-dot"></span>
<span class="pcard-title">NEXT</span>
<span class="pcard-meta" id="nxt-meta">—</span>
</div>
<div class="pcard-body"><iframe id="iframe-nxt"></iframe></div>
<div class="pcard-resize" data-resize></div>
</div>
<div class="pcard pcard-notes" id="card-notes" style="--dot-color:#f0883e">
<div class="pcard-head" data-drag>
<span class="pcard-dot"></span>
<span class="pcard-title">SPEAKER SCRIPT · 逐字稿</span>
</div>
<div class="pcard-body" id="notes-body"></div>
<div class="pcard-resize" data-resize></div>
</div>
<div class="pcard pcard-timer" id="card-timer" style="--dot-color:#3fb950">
<div class="pcard-head" data-drag>
<span class="pcard-dot"></span>
<span class="pcard-title">TIMER</span>
</div>
<div class="pcard-body">
<div class="timer-display" id="timer-display">00:00</div>
<div class="timer-row">
<span class="label">Slide</span>
<span class="val" id="timer-count">1 / ${total}</span>
</div>
<div class="timer-controls">
<button class="timer-btn" id="btn-prev">← Prev</button>
<button class="timer-btn" id="btn-next">Next →</button>
<button class="timer-btn" id="btn-reset">⏱ Reset</button>
</div>
</div>
<div class="pcard-resize" data-resize></div>
</div>
</div>
<div class="hint-bar">
<span><kbd>← →</kbd> 翻页</span>
<span><kbd>R</kbd> 重置计时</span>
<span><kbd>Esc</kbd> 关闭</span>
<span style="color:#6e7681">拖动卡片头部移动 · 拖动右下角调整大小</span>
<button class="reset-layout" id="reset-layout">重置布局</button>
</div>
<script>
(function(){
var slideMeta = ${metaJSON};
var total = ${total};
var idx = ${startIdx};
var deckUrl = ${deckUrlJSON};
var STORAGE_KEY = ${JSON.stringify(storageKey)};
var bc;
try { bc = new BroadcastChannel(${channelJSON}); } catch(e) {}
var iframeCur = document.getElementById('iframe-cur');
var iframeNxt = document.getElementById('iframe-nxt');
var notesBody = document.getElementById('notes-body');
var curMeta = document.getElementById('cur-meta');
var nxtMeta = document.getElementById('nxt-meta');
var timerDisplay = document.getElementById('timer-display');
var timerCount = document.getElementById('timer-count');
/* ===== Default card layout ===== */
function defaultLayout() {
var w = window.innerWidth;
var h = window.innerHeight - 36; /* leave room for hint bar */
return {
'card-cur': { x: 16, y: 16, w: Math.round(w*0.55) - 24, h: Math.round(h*0.62) - 16 },
'card-nxt': { x: Math.round(w*0.55) + 8, y: 16, w: w - Math.round(w*0.55) - 24, h: Math.round(h*0.42) - 16 },
'card-notes': { x: Math.round(w*0.55) + 8, y: Math.round(h*0.42) + 8, w: w - Math.round(w*0.55) - 24, h: h - Math.round(h*0.42) - 16 },
'card-timer': { x: 16, y: Math.round(h*0.62) + 8, w: Math.round(w*0.55) - 24, h: h - Math.round(h*0.62) - 16 }
};
}
/* ===== Apply / save / restore layout ===== */
function applyLayout(layout) {
Object.keys(layout).forEach(function(id){
var el = document.getElementById(id);
var l = layout[id];
if (el && l) {
el.style.left = l.x + 'px';
el.style.top = l.y + 'px';
el.style.width = l.w + 'px';
el.style.height = l.h + 'px';
}
});
rescaleAll();
}
function readLayout() {
try {
var saved = localStorage.getItem(STORAGE_KEY);
if (saved) return JSON.parse(saved);
} catch(e) {}
return defaultLayout();
}
function saveLayout() {
var layout = {};
['card-cur','card-nxt','card-notes','card-timer'].forEach(function(id){
var el = document.getElementById(id);
if (el) {
layout[id] = {
x: parseInt(el.style.left,10) || 0,
y: parseInt(el.style.top,10) || 0,
w: parseInt(el.style.width,10) || 300,
h: parseInt(el.style.height,10) || 200
};
}
});
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(layout)); } catch(e) {}
}
/* ===== iframe rescale to fit card body ===== */
function rescaleIframe(iframe) {
if (!iframe || iframe.style.display === 'none') return;
var body = iframe.parentElement;
var cw = body.clientWidth, ch = body.clientHeight;
if (!cw || !ch) return;
var s = Math.min(cw / 1920, ch / 1080);
iframe.style.transform = 'scale(' + s + ')';
/* Center the scaled iframe in the body */
var sw = 1920 * s, sh = 1080 * s;
iframe.style.left = Math.max(0, (cw - sw) / 2) + 'px';
iframe.style.top = Math.max(0, (ch - sh) / 2) + 'px';
}
function rescaleAll() {
rescaleIframe(iframeCur);
rescaleIframe(iframeNxt);
}
window.addEventListener('resize', rescaleAll);
/* ===== Drag (move card by header) ===== */
document.querySelectorAll('[data-drag]').forEach(function(handle){
handle.addEventListener('mousedown', function(e){
if (e.button !== 0) return;
var card = handle.closest('.pcard');
if (!card) return;
e.preventDefault();
card.classList.add('dragging');
document.body.classList.add('is-dragging-card');
var startX = e.clientX, startY = e.clientY;
var startL = parseInt(card.style.left,10) || 0;
var startT = parseInt(card.style.top,10) || 0;
function onMove(ev){
var nx = Math.max(0, Math.min(window.innerWidth - 100, startL + ev.clientX - startX));
var ny = Math.max(0, Math.min(window.innerHeight - 50, startT + ev.clientY - startY));
card.style.left = nx + 'px';
card.style.top = ny + 'px';
}
function onUp(){
card.classList.remove('dragging');
document.body.classList.remove('is-dragging-card');
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
saveLayout();
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
});
/* ===== Resize (drag bottom-right corner) ===== */
document.querySelectorAll('[data-resize]').forEach(function(handle){
handle.addEventListener('mousedown', function(e){
if (e.button !== 0) return;
var card = handle.closest('.pcard');
if (!card) return;
e.preventDefault(); e.stopPropagation();
card.classList.add('resizing');
document.body.classList.add('is-dragging-card');
var startX = e.clientX, startY = e.clientY;
var startW = parseInt(card.style.width,10) || card.offsetWidth;
var startH = parseInt(card.style.height,10) || card.offsetHeight;
function onMove(ev){
var nw = Math.max(180, startW + ev.clientX - startX);
var nh = Math.max(100, startH + ev.clientY - startY);
card.style.width = nw + 'px';
card.style.height = nh + 'px';
if (card.querySelector('iframe')) rescaleIframe(card.querySelector('iframe'));
}
function onUp(){
card.classList.remove('resizing');
document.body.classList.remove('is-dragging-card');
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
rescaleAll();
saveLayout();
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
});
/* ===== Preview iframe ready tracking =====
* Each iframe loads the deck ONCE with ?preview=1 on init. Subsequent
* slide changes are sent via postMessage('preview-goto') so the iframe
* just toggles visibility of a different .slide — no reload, no flicker.
*/
var iframeReady = { cur: false, nxt: false };
var currentTheme = ${themeJSON};
window.addEventListener('message', function(e) {
if (!e.data || e.data.type !== 'preview-ready') return;
var iframe = null;
if (e.source === iframeCur.contentWindow) {
iframeReady.cur = true;
iframe = iframeCur;
postPreviewGoto(iframeCur, idx);
} else if (e.source === iframeNxt.contentWindow) {
iframeReady.nxt = true;
iframe = iframeNxt;
postPreviewGoto(iframeNxt, idx + 1 < total ? idx + 1 : idx);
}
/* Sync current theme to the iframe */
if (iframe && currentTheme) {
try { iframe.contentWindow.postMessage({ type: 'preview-theme', name: currentTheme }, '*'); } catch(err) {}
}
if (iframe) rescaleIframe(iframe);
});
function postPreviewGoto(iframe, n) {
try {
iframe.contentWindow.postMessage({ type: 'preview-goto', idx: n }, '*');
} catch(e) {}
}
/* ===== Update content =====
* Smooth (no-reload) navigation: send postMessage to iframes instead of
* resetting src. Iframes stay loaded, just switch visible .slide.
*/
function update(n) {
n = Math.max(0, Math.min(total - 1, n));
idx = n;
/* Current preview — postMessage (smooth) */
if (iframeReady.cur) postPreviewGoto(iframeCur, n);
curMeta.textContent = (n + 1) + '/' + total;
/* Next preview */
if (n + 1 < total) {
iframeNxt.style.display = '';
var endEl = document.querySelector('#card-nxt .preview-end');
if (endEl) endEl.remove();
if (iframeReady.nxt) postPreviewGoto(iframeNxt, n + 1);
nxtMeta.textContent = (n + 2) + '/' + total;
} else {
iframeNxt.style.display = 'none';
var body = document.querySelector('#card-nxt .pcard-body');
if (body && !body.querySelector('.preview-end')) {
var end = document.createElement('div');
end.className = 'preview-end';
end.textContent = '— END OF DECK —';
body.appendChild(end);
}
nxtMeta.textContent = 'END';
}
/* Notes */
var note = slideMeta[n].notes;
notesBody.innerHTML = note || '<span class="empty">(这一页还没有逐字稿)</span>';
/* Timer count */
timerCount.textContent = (n + 1) + ' / ' + total;
}
/* ===== Timer ===== */
var tStart = Date.now();
setInterval(function(){
var s = Math.floor((Date.now() - tStart) / 1000);
var mm = String(Math.floor(s/60)).padStart(2,'0');
var ss = String(s%60).padStart(2,'0');
timerDisplay.textContent = mm + ':' + ss;
}, 1000);
function resetTimer(){ tStart = Date.now(); timerDisplay.textContent = '00:00'; }
/* ===== BroadcastChannel sync ===== */
if (bc) {
bc.onmessage = function(e){
if (!e.data) return;
if (e.data.type === 'go') update(e.data.idx);
else if (e.data.type === 'theme' && e.data.name) {
currentTheme = e.data.name;
/* Forward theme change to preview iframes */
[iframeCur, iframeNxt].forEach(function(iframe){
try {
iframe.contentWindow.postMessage({ type: 'preview-theme', name: e.data.name }, '*');
} catch(err) {}
});
}
};
}
function go(n) {
update(n);
if (bc) bc.postMessage({ type: 'go', idx: idx });
}
/* ===== Buttons ===== */
document.getElementById('btn-prev').addEventListener('click', function(){ go(idx - 1); });
document.getElementById('btn-next').addEventListener('click', function(){ go(idx + 1); });
document.getElementById('btn-reset').addEventListener('click', resetTimer);
document.getElementById('reset-layout').addEventListener('click', function(){
if (confirm('恢复默认卡片布局?')) {
try { localStorage.removeItem(STORAGE_KEY); } catch(e){}
applyLayout(defaultLayout());
}
});
/* ===== Keyboard ===== */
document.addEventListener('keydown', function(e){
if (e.metaKey || e.ctrlKey || e.altKey) return;
switch(e.key) {
case 'ArrowRight': case ' ': case 'PageDown': go(idx + 1); e.preventDefault(); break;
case 'ArrowLeft': case 'PageUp': go(idx - 1); e.preventDefault(); break;
case 'Home': go(0); break;
case 'End': go(total - 1); break;
case 'r': case 'R': resetTimer(); break;
case 'Escape': window.close(); break;
}
});
/* ===== Iframe load → rescale (catches initial size) ===== */
iframeCur.addEventListener('load', function(){ rescaleIframe(iframeCur); });
iframeNxt.addEventListener('load', function(){ rescaleIframe(iframeNxt); });
/* ===== Init =====
* Load each iframe ONCE with the deck file. After they post
* 'preview-ready', all subsequent navigation is via postMessage
* (smooth, no reload, no flicker).
*/
applyLayout(readLayout());
iframeCur.src = deckUrl + '?preview=' + (idx + 1);
if (idx + 1 < total) iframeNxt.src = deckUrl + '?preview=' + (idx + 2);
/* Initialize notes/timer/count without touching iframes */
notesBody.innerHTML = slideMeta[idx].notes || '<span class="empty">(这一页还没有逐字稿)</span>';
curMeta.textContent = (idx + 1) + '/' + total;
nxtMeta.textContent = (idx + 2) + '/' + total;
timerCount.textContent = (idx + 1) + ' / ' + total;
})();
</` + `script>
</body></html>`;
}
function fullscreen(){ const el=document.documentElement;
if (!document.fullscreenElement) el.requestFullscreen&&el.requestFullscreen();
else document.exitFullscreen&&document.exitFullscreen();
}
// theme cycling
const root = document.documentElement;
const themesAttr = root.getAttribute('data-themes') || document.body.getAttribute('data-themes');
const themes = themesAttr ? themesAttr.split(',').map(s=>s.trim()).filter(Boolean) : [];
let themeIdx = 0;
// Auto-detect theme base path from existing <link id="theme-link">
let themeBase = root.getAttribute('data-theme-base');
if (!themeBase) {
const existingLink = document.getElementById('theme-link');
if (existingLink) {
// el.getAttribute('href') gives the raw relative path written in HTML
const rawHref = existingLink.getAttribute('href') || '';
const lastSlash = rawHref.lastIndexOf('/');
themeBase = lastSlash >= 0 ? rawHref.substring(0, lastSlash + 1) : 'assets/themes/';
} else {
themeBase = 'assets/themes/';
}
}
function applyTheme(name) {
let link = document.getElementById('theme-link');
if (!link) {
link = document.createElement('link');
link.rel = 'stylesheet';
link.id = 'theme-link';
document.head.appendChild(link);
}
link.href = themeBase + name + '.css';
root.setAttribute('data-theme', name);
const ind = document.querySelector('.theme-indicator');
if (ind) ind.textContent = name;
}
function cycleTheme(fromRemote){
if (!themes.length) return;
themeIdx = (themeIdx+1) % themes.length;
const name = themes[themeIdx];
applyTheme(name);
/* Broadcast to other window (audience ↔ presenter) */
if (!fromRemote && bc) bc.postMessage({ type: 'theme', name: name });
}
// animation cycling on current slide
let animIdx = 0;
function cycleAnim(){
animIdx = (animIdx+1) % ANIMS.length;
const a = ANIMS[animIdx];
const target = slides[idx].querySelector('[data-anim-target]') || slides[idx];
ANIMS.forEach(x => target.classList.remove('anim-'+x));
void target.offsetWidth;
target.classList.add('anim-'+a);
target.setAttribute('data-anim', a);
const ind = document.querySelector('.anim-indicator');
if (ind) ind.textContent = a;
}
document.addEventListener('keydown', function (e) {
if (e.metaKey||e.ctrlKey||e.altKey) return;
switch (e.key) {
case 'ArrowRight': case ' ': case 'PageDown': case 'Enter': go(idx+1); e.preventDefault(); break;
case 'ArrowLeft': case 'PageUp': case 'Backspace': go(idx-1); e.preventDefault(); break;
case 'Home': go(0); break;
case 'End': go(total-1); break;
case 'f': case 'F': fullscreen(); break;
case 's': case 'S': openPresenterWindow(); break;
case 'n': case 'N': toggleNotes(); break;
case 'o': case 'O': toggleOverview(); break;
case 't': case 'T': cycleTheme(); break;
case 'a': case 'A': cycleAnim(); break;
case 'Escape': toggleOverview(false); toggleNotes(false); break;
}
});
// hash deep-link
function fromHash(){
const m = /^#\/(\d+)/.exec(location.hash||'');
if (m) go(Math.max(0, parseInt(m[1],10)-1));
}
window.addEventListener('hashchange', fromHash);
fromHash();
go(idx);
});
})();
@@ -0,0 +1,23 @@
/* theme: academic-paper — 学术论文 */
:root{
--bg:#fdfcf8;--bg-soft:#f7f5ed;--surface:#ffffff;--surface-2:#f5f3ea;
--border:rgba(20,20,20,.14);--border-strong:rgba(20,20,20,.35);
--text-1:#0a0a0a;--text-2:#333333;--text-3:#707070;
--accent:#1a3a7a;--accent-2:#0a0a0a;--accent-3:#8a1a1a;
--good:#1a5a2a;--warn:#8a6a1a;--bad:#8a1a1a;
--grad:linear-gradient(135deg,#1a3a7a,#0a0a0a);
--grad-soft:linear-gradient(135deg,#e8edf8,#f5f3ea);
--radius:0px;--radius-sm:0px;--radius-lg:0px;
--shadow:none;
--shadow-lg:0 1px 2px rgba(0,0,0,.1);
--font-sans:'Latin Modern Roman','Playfair Display','Noto Serif SC',Georgia,serif;
--font-serif:'Latin Modern Roman','Playfair Display','Noto Serif SC',Georgia,serif;
--font-display:'Latin Modern Roman','Playfair Display','Noto Serif SC',Georgia,serif;
}
body{font-family:var(--font-serif)}
h1.title,h2.title,.h1,.h2{font-weight:700;font-family:var(--font-serif)}
.card{border:1px solid var(--border);box-shadow:none}
.divider{background:var(--text-1);height:1px}
.divider-accent{background:var(--text-1);height:2px;width:100%}
a{color:var(--accent);text-decoration:underline}
.kicker{color:var(--accent);font-style:italic;text-transform:none;letter-spacing:0;font-weight:400}
@@ -0,0 +1,14 @@
/* theme: arctic-cool — 冷色调 蓝/青/石板灰 */
:root{
--bg:#f2f6fb;--bg-soft:#e7eef7;--surface:#ffffff;--surface-2:#edf3fa;
--border:rgba(40,70,110,.12);--border-strong:rgba(40,70,110,.24);
--text-1:#0e1f33;--text-2:#3a5778;--text-3:#6b819b;
--accent:#1e6fb0;--accent-2:#17b1b1;--accent-3:#6f8aa6;
--good:#1aaf84;--warn:#d19030;--bad:#c5485a;
--grad:linear-gradient(135deg,#1e6fb0,#17b1b1 60%,#5fb9d6);
--grad-soft:linear-gradient(135deg,#e7eef7,#dff3f3);
--radius:14px;--radius-sm:10px;--radius-lg:22px;
--shadow:0 10px 28px rgba(40,70,110,.12);
--shadow-lg:0 24px 60px rgba(40,70,110,.18);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
+20
View File
@@ -0,0 +1,20 @@
/* theme: aurora — 极光渐变 */
:root{
--bg:#06091c;--bg-soft:#0a1130;--surface:rgba(255,255,255,.05);--surface-2:rgba(255,255,255,.08);
--border:rgba(180,220,255,.14);--border-strong:rgba(180,220,255,.28);
--text-1:#e8f0ff;--text-2:#b4c4e4;--text-3:#6a7a9e;
--accent:#5ef2c6;--accent-2:#7aa2ff;--accent-3:#c984ff;
--good:#5ef2c6;--warn:#ffd27a;--bad:#ff8ab0;
--grad:linear-gradient(135deg,#5ef2c6,#7aa2ff 50%,#c984ff);
--grad-soft:linear-gradient(135deg,rgba(94,242,198,.2),rgba(201,132,255,.2));
--radius:20px;--radius-sm:14px;--radius-lg:28px;
--shadow:0 20px 60px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.08);
--shadow-lg:0 30px 80px rgba(0,0,0,.55);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
body{background:
radial-gradient(60% 50% at 20% 10%,rgba(94,242,198,.35),transparent 70%),
radial-gradient(55% 50% at 80% 20%,rgba(122,162,255,.32),transparent 70%),
radial-gradient(70% 60% at 50% 100%,rgba(201,132,255,.3),transparent 70%),
#06091c}
.card{backdrop-filter:blur(24px) saturate(160%);-webkit-backdrop-filter:blur(24px) saturate(160%)}
+16
View File
@@ -0,0 +1,16 @@
/* theme: bauhaus — 几何+原色 */
:root{
--bg:#f4efe3;--bg-soft:#e8e2d1;--surface:#ffffff;--surface-2:#f4efe3;
--border:#111111;--border-strong:#111111;
--text-1:#111111;--text-2:#333333;--text-3:#666666;
--accent:#e03c27;--accent-2:#f4c430;--accent-3:#1d4eaf;
--good:#1b8c3c;--warn:#f4c430;--bad:#e03c27;
--grad:linear-gradient(135deg,#e03c27 0 33%,#f4c430 33% 66%,#1d4eaf 66% 100%);
--grad-soft:linear-gradient(135deg,#f4efe3,#e8e2d1);
--radius:0;--radius-sm:0;--radius-lg:0;
--shadow:4px 4px 0 #111;--shadow-lg:8px 8px 0 #111;
--font-sans:'Space Grotesk','Inter','Noto Sans SC',sans-serif;
--font-display:'Archivo Black',sans-serif;
--letter-tight:-.03em;
}
.card{border:2px solid #111}
@@ -0,0 +1,19 @@
/* theme: blueprint — 蓝图工程 */
:root{
--bg:#0b3a6f;--bg-soft:#0a3260;--surface:rgba(255,255,255,.06);--surface-2:rgba(255,255,255,.1);
--border:rgba(190,220,255,.3);--border-strong:rgba(190,220,255,.55);
--text-1:#e8f3ff;--text-2:#b8d4f0;--text-3:#7da8cf;
--accent:#ffffff;--accent-2:#aee1ff;--accent-3:#ffd27a;
--good:#8ef0a6;--warn:#ffd27a;--bad:#ff8a96;
--grad:linear-gradient(135deg,#ffffff,#aee1ff);
--grad-soft:linear-gradient(135deg,#0a3260,#0b3a6f);
--radius:2px;--radius-sm:2px;--radius-lg:4px;
--shadow:none;--shadow-lg:0 16px 40px rgba(0,0,0,.3);
--font-sans:'JetBrains Mono','IBM Plex Mono',monospace;
--font-display:'JetBrains Mono',monospace;
}
body{background:
linear-gradient(rgba(255,255,255,.06) 1px,transparent 1px) 0 0/40px 40px,
linear-gradient(90deg,rgba(255,255,255,.06) 1px,transparent 1px) 0 0/40px 40px,
#0b3a6f}
.card{border:1px dashed rgba(190,220,255,.45);background:rgba(255,255,255,.04)}
@@ -0,0 +1,14 @@
/* theme: catppuccin-latte — catppuccin 浅 */
:root{
--bg:#eff1f5;--bg-soft:#e6e9ef;--surface:#ffffff;--surface-2:#eef0f4;
--border:rgba(76,79,105,.14);--border-strong:rgba(76,79,105,.28);
--text-1:#4c4f69;--text-2:#6c6f85;--text-3:#9ca0b0;
--accent:#8839ef;--accent-2:#1e66f5;--accent-3:#ea76cb;
--good:#40a02b;--warn:#df8e1d;--bad:#d20f39;
--grad:linear-gradient(135deg,#8839ef,#1e66f5 50%,#04a5e5);
--grad-soft:linear-gradient(135deg,#eff1f5,#e6e9ef);
--radius:14px;--radius-sm:10px;--radius-lg:22px;
--shadow:0 8px 24px rgba(76,79,105,.1);
--shadow-lg:0 20px 56px rgba(76,79,105,.16);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
@@ -0,0 +1,14 @@
/* theme: catppuccin-mocha — catppuccin 深 */
:root{
--bg:#1e1e2e;--bg-soft:#181825;--surface:#313244;--surface-2:#45475a;
--border:rgba(205,214,244,.12);--border-strong:rgba(205,214,244,.24);
--text-1:#cdd6f4;--text-2:#a6adc8;--text-3:#7f849c;
--accent:#cba6f7;--accent-2:#89b4fa;--accent-3:#f5c2e7;
--good:#a6e3a1;--warn:#f9e2af;--bad:#f38ba8;
--grad:linear-gradient(135deg,#cba6f7,#89b4fa 50%,#94e2d5);
--grad-soft:linear-gradient(135deg,#313244,#45475a);
--radius:14px;--radius-sm:10px;--radius-lg:22px;
--shadow:0 10px 30px rgba(0,0,0,.35);
--shadow-lg:0 24px 60px rgba(0,0,0,.5);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
@@ -0,0 +1,19 @@
/* theme: corporate-clean — 企业商务 */
:root{
--bg:#ffffff;--bg-soft:#f5f7fa;--surface:#ffffff;--surface-2:#f0f3f7;
--border:rgba(10,37,64,.12);--border-strong:rgba(10,37,64,.28);
--text-1:#0a2540;--text-2:#425466;--text-3:#8898aa;
--accent:#0a2540;--accent-2:#1d4ed8;--accent-3:#64748b;
--good:#0e9f6e;--warn:#d97706;--bad:#dc2626;
--grad:linear-gradient(135deg,#0a2540,#1d4ed8);
--grad-soft:linear-gradient(135deg,#f0f4fb,#e4ecf7);
--radius:6px;--radius-sm:4px;--radius-lg:10px;
--shadow:0 1px 3px rgba(10,37,64,.08),0 4px 12px rgba(10,37,64,.05);
--shadow-lg:0 4px 12px rgba(10,37,64,.1),0 16px 40px rgba(10,37,64,.08);
--font-sans:'Inter','Noto Sans SC',sans-serif;
--font-display:'Inter','Noto Sans SC',sans-serif;
}
.card{border:1px solid var(--border)}
.divider-accent{background:var(--accent);height:3px;width:56px}
.kicker{color:var(--accent-2)}
h1.title,h2.title,.h1,.h2{font-weight:700;color:var(--accent)}
@@ -0,0 +1,23 @@
/* theme: cyberpunk-neon — 赛博朋克霓虹 */
:root{
--bg:#000000;--bg-soft:#0a0a12;--surface:#0f0f1a;--surface-2:#14141f;
--border:rgba(255,0,170,.25);--border-strong:rgba(0,240,255,.55);
--text-1:#f5f7ff;--text-2:#b4b8d4;--text-3:#6b6e8a;
--accent:#ff2bd6;--accent-2:#00f0ff;--accent-3:#f9f871;
--good:#39ff14;--warn:#f9f871;--bad:#ff2bd6;
--grad:linear-gradient(135deg,#ff2bd6,#7a00ff 50%,#00f0ff);
--grad-soft:linear-gradient(135deg,rgba(255,43,214,.18),rgba(0,240,255,.18));
--radius:6px;--radius-sm:3px;--radius-lg:10px;
--shadow:0 0 0 1px rgba(255,43,214,.35),0 0 24px rgba(255,43,214,.35),0 0 48px rgba(0,240,255,.18);
--shadow-lg:0 0 0 1px rgba(0,240,255,.5),0 0 40px rgba(0,240,255,.45),0 0 80px rgba(255,43,214,.3);
--font-sans:'Inter','Noto Sans SC',sans-serif;
--font-display:'JetBrains Mono','IBM Plex Mono',monospace;
}
body{background:
radial-gradient(ellipse at 15% 0%,rgba(255,43,214,.22),transparent 60%),
radial-gradient(ellipse at 85% 100%,rgba(0,240,255,.2),transparent 60%),
#000}
h1.title,h2.title,.h1,.h2{text-shadow:0 0 12px rgba(255,43,214,.6),0 0 30px rgba(0,240,255,.35)}
.kicker{color:var(--accent-2);text-shadow:0 0 8px rgba(0,240,255,.6)}
.card{background:rgba(15,15,26,.72);backdrop-filter:blur(8px)}
.divider-accent{background:var(--grad);box-shadow:0 0 12px var(--accent)}
+14
View File
@@ -0,0 +1,14 @@
/* theme: dracula — dracula 深色 */
:root{
--bg:#282a36;--bg-soft:#21222c;--surface:#343746;--surface-2:#44475a;
--border:rgba(248,248,242,.12);--border-strong:rgba(248,248,242,.24);
--text-1:#f8f8f2;--text-2:#bdbde0;--text-3:#6272a4;
--accent:#bd93f9;--accent-2:#ff79c6;--accent-3:#8be9fd;
--good:#50fa7b;--warn:#f1fa8c;--bad:#ff5555;
--grad:linear-gradient(135deg,#bd93f9,#ff79c6 55%,#8be9fd);
--grad-soft:linear-gradient(135deg,#343746,#44475a);
--radius:12px;--radius-sm:8px;--radius-lg:18px;
--shadow:0 10px 30px rgba(0,0,0,.4);
--shadow-lg:0 22px 60px rgba(0,0,0,.55);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
@@ -0,0 +1,18 @@
/* theme: editorial-serif — 杂志风衬线,高级 */
:root{
--bg:#faf7f2;--bg-soft:#f3efe6;--surface:#ffffff;--surface-2:#f7f2e8;
--border:rgba(40,28,18,.12);--border-strong:rgba(40,28,18,.24);
--text-1:#1b1410;--text-2:#5c4a3e;--text-3:#8a7868;
--accent:#8a2a1c;--accent-2:#c97a4a;--accent-3:#1b1410;
--good:#3f7d4f;--warn:#b07a1f;--bad:#8a2a1c;
--grad:linear-gradient(135deg,#8a2a1c,#c97a4a);
--grad-soft:linear-gradient(135deg,#faf7f2,#f3efe6);
--radius:4px;--radius-sm:2px;--radius-lg:8px;
--shadow:0 2px 12px rgba(40,28,18,.06);
--shadow-lg:0 20px 50px rgba(40,28,18,.14);
--font-sans:'Playfair Display','Noto Serif SC',serif;
--font-display:'Playfair Display','Noto Serif SC',serif;
--font-serif:'Playfair Display','Noto Serif SC',serif;
--letter-tight:-.02em;
}
.h1,.h2,h1.title,h2.title{font-style:italic;font-weight:600}
@@ -0,0 +1,26 @@
/* theme: engineering-whiteprint — 工程白图 */
:root{
--bg:#ffffff;--bg-soft:#f8fafc;--surface:#ffffff;--surface-2:#f4f7fb;
--border:rgba(10,30,70,.22);--border-strong:#0a1e46;
--text-1:#0a1e46;--text-2:#3a4a6a;--text-3:#8090a8;
--accent:#0a1e46;--accent-2:#1e5ac4;--accent-3:#c42a10;
--good:#1a6a3a;--warn:#c47a10;--bad:#c42a10;
--grad:linear-gradient(135deg,#0a1e46,#1e5ac4);
--grad-soft:linear-gradient(135deg,#eaf0fb,#f4f7fb);
--radius:0px;--radius-sm:0px;--radius-lg:0px;
--shadow:none;
--shadow-lg:0 0 0 1px var(--border-strong);
--font-sans:'Inter','Noto Sans SC',sans-serif;
--font-mono:'JetBrains Mono','IBM Plex Mono',monospace;
--font-display:'JetBrains Mono','Inter',monospace;
}
body{background:
repeating-linear-gradient(0deg,rgba(10,30,70,.07) 0 1px,transparent 1px 40px),
repeating-linear-gradient(90deg,rgba(10,30,70,.07) 0 1px,transparent 1px 40px),
#ffffff}
.card{border:1px solid var(--border-strong);box-shadow:none;background:rgba(255,255,255,.85)}
.divider{background:var(--border-strong);height:1px}
.divider-accent{background:var(--border-strong);height:1px;width:100%}
.kicker{font-family:var(--font-mono);color:var(--accent-2);letter-spacing:.18em}
h1.title,h2.title,.h1,.h2{font-weight:600}
.pill{font-family:var(--font-mono);border:1px solid var(--border-strong);border-radius:0}
@@ -0,0 +1,21 @@
/* theme: glassmorphism — 毛玻璃 */
:root{
--bg:#0b1024;--bg-soft:#0e1530;--surface:rgba(255,255,255,.06);--surface-2:rgba(255,255,255,.1);
--border:rgba(255,255,255,.14);--border-strong:rgba(255,255,255,.28);
--text-1:#f2f4ff;--text-2:#c3c8e6;--text-3:#8287a8;
--accent:#7dd3fc;--accent-2:#c084fc;--accent-3:#f0abfc;
--good:#86efac;--warn:#fde68a;--bad:#fca5a5;
--grad:linear-gradient(135deg,#7dd3fc,#c084fc 55%,#f0abfc);
--grad-soft:linear-gradient(135deg,rgba(125,211,252,.18),rgba(192,132,252,.18));
--radius:22px;--radius-sm:14px;--radius-lg:30px;
--shadow:0 20px 60px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,255,255,.12);
--shadow-lg:0 30px 80px rgba(0,0,0,.5);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
body{background:
radial-gradient(60% 60% at 20% 20%,rgba(125,211,252,.3),transparent 60%),
radial-gradient(50% 50% at 80% 30%,rgba(192,132,252,.28),transparent 60%),
radial-gradient(60% 60% at 60% 90%,rgba(240,171,252,.25),transparent 60%),
#0b1024}
.card{backdrop-filter:blur(28px) saturate(180%);-webkit-backdrop-filter:blur(28px) saturate(180%);
background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.18)}
@@ -0,0 +1,14 @@
/* theme: gruvbox-dark */
:root{
--bg:#282828;--bg-soft:#1d2021;--surface:#3c3836;--surface-2:#504945;
--border:rgba(235,219,178,.14);--border-strong:rgba(235,219,178,.28);
--text-1:#ebdbb2;--text-2:#d5c4a1;--text-3:#928374;
--accent:#fabd2f;--accent-2:#fe8019;--accent-3:#b8bb26;
--good:#b8bb26;--warn:#fabd2f;--bad:#fb4934;
--grad:linear-gradient(135deg,#fe8019,#fabd2f 55%,#b8bb26);
--grad-soft:linear-gradient(135deg,#3c3836,#504945);
--radius:6px;--radius-sm:4px;--radius-lg:12px;
--shadow:0 10px 30px rgba(0,0,0,.5);
--shadow-lg:0 24px 60px rgba(0,0,0,.65);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
@@ -0,0 +1,21 @@
/* theme: japanese-minimal — 和风极简 */
:root{
--bg:#fafaf5;--bg-soft:#f2f0e6;--surface:#ffffff;--surface-2:#f5f3ea;
--border:rgba(40,30,20,.1);--border-strong:rgba(40,30,20,.3);
--text-1:#1a1a18;--text-2:#5c564c;--text-3:#9c958a;
--accent:#d93a2a;--accent-2:#1a1a18;--accent-3:#c9a961;
--good:#4a6b3e;--warn:#c9a961;--bad:#d93a2a;
--grad:linear-gradient(135deg,#d93a2a,#1a1a18);
--grad-soft:linear-gradient(135deg,#faeae6,#f5f3ea);
--radius:0px;--radius-sm:0px;--radius-lg:2px;
--shadow:none;
--shadow-lg:0 1px 0 rgba(40,30,20,.12);
--font-sans:'Inter','Noto Sans SC',sans-serif;
--font-serif:'Noto Serif SC','Playfair Display',serif;
--font-display:'Noto Serif SC','Playfair Display',serif;
}
h1.title,h2.title,.h1,.h2{font-weight:500;letter-spacing:.04em}
.card{border:1px solid var(--border);box-shadow:none;padding:36px 40px}
.divider-accent{background:var(--accent);height:2px;width:48px}
.kicker{color:var(--accent);letter-spacing:.2em}
.slide{padding:96px 128px}
@@ -0,0 +1,21 @@
/* theme: magazine-bold — 杂志大标题 */
:root{
--bg:#f5efe2;--bg-soft:#ebe4d2;--surface:#fbf6e8;--surface-2:#ede5d0;
--border:rgba(10,10,10,.16);--border-strong:#0a0a0a;
--text-1:#0a0a0a;--text-2:#2a2a2a;--text-3:#6a6458;
--accent:#ea5a1a;--accent-2:#0a0a0a;--accent-3:#c42a10;
--good:#2a6a2a;--warn:#ea5a1a;--bad:#c42a10;
--grad:linear-gradient(135deg,#ea5a1a,#c42a10);
--grad-soft:linear-gradient(135deg,#fbe4d0,#f5d6c0);
--radius:0px;--radius-sm:0px;--radius-lg:2px;
--shadow:none;
--shadow-lg:6px 6px 0 var(--accent);
--font-sans:'Inter','Noto Sans SC',sans-serif;
--font-serif:'Playfair Display','Noto Serif SC',Georgia,serif;
--font-display:'Playfair Display','Noto Serif SC',Georgia,serif;
}
h1.title,.h1{font-size:120px;line-height:.92;font-weight:900;letter-spacing:-.04em;font-family:var(--font-serif)}
h2.title,.h2{font-size:72px;font-weight:800;font-family:var(--font-serif)}
.card{border:1.5px solid var(--text-1)}
.divider-accent{background:var(--accent);height:6px;width:90px}
.kicker{color:var(--accent);text-transform:uppercase;font-weight:700;letter-spacing:.25em}
@@ -0,0 +1,20 @@
/* theme: memphis-pop — 孟菲斯波普 */
:root{
--bg:#fef6e8;--bg-soft:#fdebc7;--surface:#ffffff;--surface-2:#fff1d1;
--border:#111111;--border-strong:#111111;
--text-1:#111111;--text-2:#333333;--text-3:#666666;
--accent:#ff3d8b;--accent-2:#37c2d7;--accent-3:#ffcc00;
--good:#6ac04c;--warn:#ffcc00;--bad:#ff3d8b;
--grad:linear-gradient(135deg,#ff3d8b,#ffcc00 50%,#37c2d7);
--grad-soft:linear-gradient(135deg,#fdebc7,#fff1d1);
--radius:10px;--radius-sm:6px;--radius-lg:18px;
--shadow:5px 5px 0 #111;--shadow-lg:9px 9px 0 #111;
--font-sans:'Space Grotesk','Inter','Noto Sans SC',sans-serif;
--font-display:'Archivo Black',sans-serif;
}
.card{border:2.5px solid #111}
body{background-image:
radial-gradient(circle at 10% 20%,#ff3d8b 3px,transparent 4px),
radial-gradient(circle at 80% 40%,#37c2d7 3px,transparent 4px),
radial-gradient(circle at 30% 80%,#ffcc00 3px,transparent 4px);
background-size:200px 200px,220px 220px,260px 260px}
@@ -0,0 +1,19 @@
/* theme: midcentury — 世纪中期现代 */
:root{
--bg:#f3ead8;--bg-soft:#ebdfc4;--surface:#f9f2e0;--surface-2:#e8dcbe;
--border:rgba(60,40,20,.18);--border-strong:rgba(60,40,20,.4);
--text-1:#201810;--text-2:#5a4830;--text-3:#9a8868;
--accent:#d4902a;--accent-2:#2a7a7f;--accent-3:#c7502a;
--good:#5a7a3a;--warn:#d4902a;--bad:#c7502a;
--grad:linear-gradient(135deg,#d4902a,#c7502a 55%,#2a7a7f);
--grad-soft:linear-gradient(135deg,#f4e0b6,#eac7a8);
--radius:2px;--radius-sm:0px;--radius-lg:4px;
--shadow:4px 4px 0 rgba(40,25,10,.12);
--shadow-lg:6px 6px 0 rgba(40,25,10,.2),0 10px 24px rgba(40,25,10,.14);
--font-sans:'Inter','Noto Sans SC',sans-serif;
--font-display:'Playfair Display','Noto Serif SC',serif;
}
.card{border:1.5px solid var(--border-strong)}
.divider-accent{background:var(--accent-3);height:4px;width:80px}
.kicker{color:var(--accent-2)}
h1.title,.h1{color:var(--accent-3)}
@@ -0,0 +1,16 @@
/* theme: minimal-white — 极简白,克制高级 */
:root{
--bg:#ffffff;--bg-soft:#fafafa;--surface:#ffffff;--surface-2:#f5f5f6;
--border:rgba(17,18,22,.08);--border-strong:rgba(17,18,22,.16);
--text-1:#0c0d10;--text-2:#55596a;--text-3:#9ca1b0;
--accent:#111216;--accent-2:#3b3f4a;--accent-3:#6b6f7a;
--good:#1aaf6c;--warn:#c98500;--bad:#c13a3a;
--grad:linear-gradient(135deg,#111216,#3b3f4a);
--grad-soft:linear-gradient(135deg,#f5f5f6,#ffffff);
--radius:14px;--radius-sm:8px;--radius-lg:22px;
--shadow:0 1px 2px rgba(17,18,22,.04),0 8px 24px rgba(17,18,22,.06);
--shadow-lg:0 20px 60px rgba(17,18,22,.1);
--font-sans:'Inter','Noto Sans SC',sans-serif;
--font-display:'Inter','Noto Sans SC',sans-serif;
--letter-tight:-.035em;
}
@@ -0,0 +1,17 @@
/* theme: neo-brutalism — 厚描边、硬阴影、明黄 */
:root{
--bg:#fffef0;--bg-soft:#fffbd0;--surface:#ffffff;--surface-2:#fff38a;
--border:#000000;--border-strong:#000000;
--text-1:#000000;--text-2:#222222;--text-3:#555555;
--accent:#ffd400;--accent-2:#ff5ca8;--accent-3:#3a7cff;
--good:#00b36b;--warn:#ff9900;--bad:#ff3a30;
--grad:linear-gradient(135deg,#ffd400,#ff5ca8);
--grad-soft:linear-gradient(135deg,#fffbd0,#fff);
--radius:6px;--radius-sm:4px;--radius-lg:10px;
--shadow:6px 6px 0 #000;--shadow-lg:10px 10px 0 #000;
--font-sans:'Space Grotesk','Inter','Noto Sans SC',sans-serif;
--font-display:'Archivo Black','Space Grotesk',sans-serif;
--letter-tight:-.03em;
}
.card{border:3px solid #000}
.pill{border:2px solid #000;background:#ffd400;color:#000}
@@ -0,0 +1,20 @@
/* theme: news-broadcast — 新闻播报 */
:root{
--bg:#ffffff;--bg-soft:#f4f4f4;--surface:#ffffff;--surface-2:#ececec;
--border:rgba(0,0,0,.14);--border-strong:#0a0a0a;
--text-1:#0a0a0a;--text-2:#3a3a3a;--text-3:#7a7a7a;
--accent:#e11d2d;--accent-2:#0a0a0a;--accent-3:#ffd100;
--good:#0e7c3a;--warn:#ffd100;--bad:#e11d2d;
--grad:linear-gradient(90deg,#e11d2d 0%,#e11d2d 100%);
--grad-soft:linear-gradient(135deg,#fde5e7,#f4f4f4);
--radius:0px;--radius-sm:0px;--radius-lg:2px;
--shadow:none;
--shadow-lg:0 4px 0 var(--accent);
--font-sans:'Oswald','Inter','Noto Sans SC',sans-serif;
--font-display:'Oswald','Inter','Noto Sans SC',sans-serif;
}
h1.title,h2.title,.h1,.h2{font-weight:700;text-transform:uppercase;letter-spacing:-.01em}
.card{border:2px solid var(--text-1);box-shadow:6px 6px 0 var(--accent)}
.divider-accent{background:var(--accent);height:6px;width:100%}
.kicker{background:var(--accent);color:#fff;padding:4px 12px;display:inline-block;letter-spacing:.15em}
.slide::before{content:"";position:absolute;left:0;top:0;bottom:0;width:8px;background:var(--accent);z-index:3}
+14
View File
@@ -0,0 +1,14 @@
/* theme: nord */
:root{
--bg:#2e3440;--bg-soft:#272b35;--surface:#3b4252;--surface-2:#434c5e;
--border:rgba(236,239,244,.12);--border-strong:rgba(236,239,244,.24);
--text-1:#eceff4;--text-2:#d8dee9;--text-3:#7b8394;
--accent:#88c0d0;--accent-2:#81a1c1;--accent-3:#b48ead;
--good:#a3be8c;--warn:#ebcb8b;--bad:#bf616a;
--grad:linear-gradient(135deg,#88c0d0,#81a1c1 50%,#b48ead);
--grad-soft:linear-gradient(135deg,#3b4252,#434c5e);
--radius:12px;--radius-sm:8px;--radius-lg:20px;
--shadow:0 10px 30px rgba(0,0,0,.35);
--shadow-lg:0 22px 60px rgba(0,0,0,.5);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
@@ -0,0 +1,21 @@
/* theme: pitch-deck-vc — YC 风融资 pitch */
:root{
--bg:#ffffff;--bg-soft:#fafbfc;--surface:#ffffff;--surface-2:#f5f7fa;
--border:rgba(20,30,50,.1);--border-strong:rgba(20,30,50,.22);
--text-1:#0b0d12;--text-2:#4a5270;--text-3:#8b93a8;
--accent:#0070f3;--accent-2:#7928ca;--accent-3:#ff4ecb;
--good:#0cce6b;--warn:#f5a524;--bad:#ee0000;
--grad:linear-gradient(135deg,#0070f3,#7928ca);
--grad-soft:linear-gradient(135deg,#e8f0ff,#f3e8ff);
--radius:14px;--radius-sm:8px;--radius-lg:22px;
--shadow:0 2px 8px rgba(20,30,50,.06),0 12px 32px rgba(20,30,50,.06);
--shadow-lg:0 8px 24px rgba(20,30,50,.1),0 30px 80px rgba(20,30,50,.1);
--font-sans:'Inter','Noto Sans SC',sans-serif;
--font-display:'Inter','Noto Sans SC',sans-serif;
}
.slide{padding:88px 120px}
h1.title,.h1{font-weight:800;letter-spacing:-.035em}
h1.title .gradient-text,.h1 .gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.card{border:1px solid var(--border)}
.divider-accent{background:var(--grad);height:4px;width:64px;border-radius:2px}
.kicker{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
@@ -0,0 +1,16 @@
/* theme: rainbow-gradient — 彩虹渐变点缀(白底) */
:root{
--bg:#ffffff;--bg-soft:#f8f8fb;--surface:#ffffff;--surface-2:#f4f4f8;
--border:rgba(20,20,40,.08);--border-strong:rgba(20,20,40,.2);
--text-1:#0c0d10;--text-2:#4d5162;--text-3:#9096a8;
--accent:#ff4d8b;--accent-2:#7a5cff;--accent-3:#36b6ff;
--good:#1aaf6c;--warn:#f5a524;--bad:#e0445a;
--grad:linear-gradient(90deg,#ff0080,#ff4d00,#ff9900,#ffe600,#00c853,#0091ea,#6200ea,#ff0080);
--grad-soft:linear-gradient(135deg,#fff,#f8f8fb);
--radius:16px;--radius-sm:10px;--radius-lg:24px;
--shadow:0 12px 32px rgba(124,92,255,.1);
--shadow-lg:0 24px 60px rgba(124,92,255,.18);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
.gradient-text{background-size:200% auto;animation:rbflow 6s linear infinite}
@keyframes rbflow{to{background-position:200% 0}}
@@ -0,0 +1,22 @@
/* theme: retro-tv — 复古显像管 */
:root{
--bg:#f5ecd7;--bg-soft:#efe4c6;--surface:#fbf5e2;--surface-2:#efe3c2;
--border:rgba(120,70,20,.22);--border-strong:rgba(120,70,20,.45);
--text-1:#2a1a08;--text-2:#6b4a22;--text-3:#a68656;
--accent:#e67e14;--accent-2:#c73a1f;--accent-3:#f2b544;
--good:#3e8940;--warn:#e67e14;--bad:#c73a1f;
--grad:linear-gradient(135deg,#c73a1f,#e67e14 55%,#f2b544);
--grad-soft:linear-gradient(135deg,#fde6c4,#fbd9a0);
--radius:10px;--radius-sm:6px;--radius-lg:16px;
--shadow:0 6px 0 rgba(80,40,0,.12),0 12px 28px rgba(80,40,0,.15);
--shadow-lg:0 10px 0 rgba(80,40,0,.15),0 24px 50px rgba(80,40,0,.2);
--font-sans:'Inter','Noto Sans SC',sans-serif;
--font-display:'Playfair Display','Noto Serif SC',serif;
}
body{background:
repeating-linear-gradient(0deg,rgba(80,40,0,.06) 0 2px,transparent 2px 4px),
radial-gradient(ellipse at center,#f7ecd0 0%,#e8d9b0 85%,#c9b888 100%)}
.slide::before{content:"";position:absolute;inset:0;pointer-events:none;
background:repeating-linear-gradient(0deg,rgba(0,0,0,.035) 0 2px,transparent 2px 4px);z-index:1}
.slide > *{position:relative;z-index:2}
h1.title,.h1{color:var(--accent-2)}
@@ -0,0 +1,14 @@
/* theme: rose-pine */
:root{
--bg:#191724;--bg-soft:#1f1d2e;--surface:#26233a;--surface-2:#2a2740;
--border:rgba(224,222,244,.12);--border-strong:rgba(224,222,244,.24);
--text-1:#e0def4;--text-2:#c4b8d8;--text-3:#6e6a86;
--accent:#ebbcba;--accent-2:#c4a7e7;--accent-3:#9ccfd8;
--good:#31748f;--warn:#f6c177;--bad:#eb6f92;
--grad:linear-gradient(135deg,#ebbcba,#c4a7e7 55%,#9ccfd8);
--grad-soft:linear-gradient(135deg,#26233a,#2a2740);
--radius:14px;--radius-sm:10px;--radius-lg:22px;
--shadow:0 10px 30px rgba(0,0,0,.4);
--shadow-lg:0 22px 58px rgba(0,0,0,.55);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
@@ -0,0 +1,17 @@
/* theme: sharp-mono — 锐利黑白高对比 */
:root{
--bg:#ffffff;--bg-soft:#ffffff;--surface:#ffffff;--surface-2:#000000;
--border:#000000;--border-strong:#000000;
--text-1:#000000;--text-2:#1a1a1a;--text-3:#4a4a4a;
--accent:#000000;--accent-2:#000000;--accent-3:#ff2200;
--good:#008800;--warn:#ff9900;--bad:#ff0000;
--grad:linear-gradient(135deg,#000,#222);
--grad-soft:linear-gradient(135deg,#fff,#eee);
--radius:0;--radius-sm:0;--radius-lg:0;
--shadow:4px 4px 0 #000;--shadow-lg:8px 8px 0 #000;
--font-sans:'Archivo Black','Inter','Noto Sans SC',sans-serif;
--font-display:'Archivo Black',sans-serif;
--letter-tight:-.04em;
}
.h1,.h2,h1.title,h2.title{text-transform:uppercase}
.card{border:2px solid #000}
@@ -0,0 +1,14 @@
/* theme: soft-pastel — 柔和马卡龙 */
:root{
--bg:#fdf7fb;--bg-soft:#fbeef3;--surface:#ffffff;--surface-2:#fdf0f5;
--border:rgba(120,70,110,.12);--border-strong:rgba(120,70,110,.22);
--text-1:#3a1f33;--text-2:#6b4d62;--text-3:#a28a99;
--accent:#f49bb8;--accent-2:#b5d5f0;--accent-3:#f7d08a;
--good:#9dd9a3;--warn:#f7d08a;--bad:#ef9a9a;
--grad:linear-gradient(135deg,#f49bb8,#b5d5f0 55%,#c4a0e8);
--grad-soft:linear-gradient(135deg,#fbeef3,#eaf4fc);
--radius:24px;--radius-sm:16px;--radius-lg:32px;
--shadow:0 8px 28px rgba(244,155,184,.18);
--shadow-lg:0 24px 70px rgba(181,213,240,.3);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
@@ -0,0 +1,14 @@
/* theme: solarized-light */
:root{
--bg:#fdf6e3;--bg-soft:#eee8d5;--surface:#ffffff;--surface-2:#f5efd7;
--border:rgba(88,110,117,.2);--border-strong:rgba(88,110,117,.4);
--text-1:#073642;--text-2:#586e75;--text-3:#93a1a1;
--accent:#268bd2;--accent-2:#2aa198;--accent-3:#d33682;
--good:#859900;--warn:#b58900;--bad:#dc322f;
--grad:linear-gradient(135deg,#268bd2,#2aa198 50%,#859900);
--grad-soft:linear-gradient(135deg,#fdf6e3,#eee8d5);
--radius:10px;--radius-sm:6px;--radius-lg:16px;
--shadow:0 6px 20px rgba(88,110,117,.14);
--shadow-lg:0 18px 50px rgba(88,110,117,.24);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
@@ -0,0 +1,14 @@
/* theme: sunset-warm — 暖色调 橘/珊瑚/琥珀 */
:root{
--bg:#fff7ef;--bg-soft:#ffeedc;--surface:#ffffff;--surface-2:#fff2e0;
--border:rgba(120,60,20,.12);--border-strong:rgba(120,60,20,.22);
--text-1:#2a160a;--text-2:#6b4630;--text-3:#a28572;
--accent:#e36a2d;--accent-2:#f2a341;--accent-3:#d94860;
--good:#5ea35a;--warn:#f2a341;--bad:#d94860;
--grad:linear-gradient(135deg,#d94860,#e36a2d 50%,#f2a341);
--grad-soft:linear-gradient(135deg,#ffeedc,#ffe0d0);
--radius:18px;--radius-sm:12px;--radius-lg:28px;
--shadow:0 12px 32px rgba(227,106,45,.16);
--shadow-lg:0 24px 64px rgba(227,106,45,.22);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
@@ -0,0 +1,17 @@
/* theme: swiss-grid — 瑞士网格,Helvetica 感 */
:root{
--bg:#ffffff;--bg-soft:#f4f4f4;--surface:#ffffff;--surface-2:#f4f4f4;
--border:#111111;--border-strong:#111111;
--text-1:#111111;--text-2:#444444;--text-3:#888888;
--accent:#d6001c;--accent-2:#111111;--accent-3:#888888;
--good:#0f8a2f;--warn:#d38a00;--bad:#d6001c;
--grad:linear-gradient(135deg,#d6001c,#111);
--grad-soft:linear-gradient(135deg,#f4f4f4,#fff);
--radius:0;--radius-sm:0;--radius-lg:0;
--shadow:none;--shadow-lg:none;
--font-sans:'Inter','Helvetica Neue',Helvetica,'Noto Sans SC',sans-serif;
--font-display:'Inter','Helvetica Neue',Helvetica,sans-serif;
--letter-tight:-.04em;
}
.card{border-top:2px solid #111;border-bottom:1px solid #111;border-left:none;border-right:none;box-shadow:none;background:#fff}
.slide{background-image:linear-gradient(90deg,rgba(0,0,0,.04) 1px,transparent 1px);background-size:calc(100%/12) 100%}
@@ -0,0 +1,18 @@
/* theme: terminal-green — 绿屏终端 */
:root{
--bg:#030a04;--bg-soft:#041308;--surface:#0a1b10;--surface-2:#0d2614;
--border:rgba(0,255,120,.22);--border-strong:rgba(0,255,120,.42);
--text-1:#8cff9a;--text-2:#4bd17a;--text-3:#2f8a4d;
--accent:#00ff88;--accent-2:#67ffd0;--accent-3:#b6ff6b;
--good:#00ff88;--warn:#ffe066;--bad:#ff6464;
--grad:linear-gradient(135deg,#00ff88,#67ffd0);
--grad-soft:linear-gradient(135deg,#0a1b10,#0d2614);
--radius:4px;--radius-sm:2px;--radius-lg:8px;
--shadow:0 0 30px rgba(0,255,136,.15);
--shadow-lg:0 0 60px rgba(0,255,136,.28);
--font-sans:'JetBrains Mono','IBM Plex Mono',monospace;
--font-display:'JetBrains Mono',monospace;
--letter-tight:-.01em;
}
body{text-shadow:0 0 2px rgba(0,255,136,.5)}
.card{border:1px solid rgba(0,255,120,.3);background:rgba(10,27,16,.6)}
@@ -0,0 +1,14 @@
/* theme: tokyo-night */
:root{
--bg:#1a1b26;--bg-soft:#16161e;--surface:#24283b;--surface-2:#2f334d;
--border:rgba(192,202,245,.12);--border-strong:rgba(192,202,245,.24);
--text-1:#c0caf5;--text-2:#a9b1d6;--text-3:#565f89;
--accent:#7aa2f7;--accent-2:#bb9af7;--accent-3:#7dcfff;
--good:#9ece6a;--warn:#e0af68;--bad:#f7768e;
--grad:linear-gradient(135deg,#7aa2f7,#bb9af7 55%,#f7768e);
--grad-soft:linear-gradient(135deg,#24283b,#2f334d);
--radius:12px;--radius-sm:8px;--radius-lg:20px;
--shadow:0 10px 30px rgba(0,0,0,.45);
--shadow-lg:0 24px 62px rgba(0,0,0,.6);
--font-sans:'Inter','Noto Sans SC',sans-serif;
}
@@ -0,0 +1,21 @@
/* theme: vaporwave — 蒸汽波 */
:root{
--bg:#1a0938;--bg-soft:#261050;--surface:rgba(255,255,255,.06);--surface-2:rgba(255,255,255,.1);
--border:rgba(255,110,199,.28);--border-strong:rgba(0,245,255,.5);
--text-1:#fdf0ff;--text-2:#d4a8e8;--text-3:#8a6ba8;
--accent:#ff6ec7;--accent-2:#00f5ff;--accent-3:#ffd166;
--grad:linear-gradient(135deg,#ff6ec7 0%,#c94fff 35%,#00f5ff 100%);
--grad-soft:linear-gradient(135deg,rgba(255,110,199,.25),rgba(0,245,255,.25));
--radius:18px;--radius-sm:10px;--radius-lg:28px;
--shadow:0 20px 60px rgba(255,110,199,.2),0 0 1px rgba(0,245,255,.6);
--shadow-lg:0 30px 80px rgba(255,110,199,.3),0 0 2px rgba(0,245,255,.8);
--font-sans:'Space Grotesk','Inter','Noto Sans SC',sans-serif;
--font-display:'Space Grotesk','Inter',sans-serif;
}
body{background:
linear-gradient(180deg,#1a0938 0%,#3a0f5c 45%,#7a1f6b 85%,#e85d9c 100%),
radial-gradient(ellipse at 50% 80%,rgba(0,245,255,.3),transparent 60%)}
h1.title,.h1{background:var(--grad);-webkit-background-clip:text;background-clip:text;
-webkit-text-fill-color:transparent;color:transparent}
.card{backdrop-filter:blur(18px)}
.divider-accent{background:var(--grad);height:4px;width:120px;box-shadow:0 0 20px var(--accent)}
@@ -0,0 +1,16 @@
/* theme: xiaohongshu-white — 小红书白底高级感 */
:root{
--bg:#fffdfb;--bg-soft:#fff6f1;--surface:#ffffff;--surface-2:#fff1ea;
--border:rgba(60,30,20,.1);--border-strong:rgba(60,30,20,.22);
--text-1:#1a1210;--text-2:#4f3a32;--text-3:#a08d85;
--accent:#ff2742;--accent-2:#ff7a90;--accent-3:#ffb38a;
--good:#3ba55c;--warn:#f5a524;--bad:#ff2742;
--grad:linear-gradient(135deg,#ff2742,#ff7a90 55%,#ffb38a);
--grad-soft:linear-gradient(135deg,#fff6f1,#ffeae0);
--radius:20px;--radius-sm:14px;--radius-lg:28px;
--shadow:0 12px 30px rgba(255,39,66,.08);
--shadow-lg:0 24px 60px rgba(255,39,66,.14);
--font-sans:'Noto Sans SC','Inter',sans-serif;
--font-display:'Noto Serif SC','Playfair Display',serif;
--letter-tight:-.02em;
}
@@ -0,0 +1,20 @@
/* theme: y2k-chrome — 千禧银色铬金属 */
:root{
--bg:#dfe4ec;--bg-soft:#eef1f6;--surface:rgba(255,255,255,.72);--surface-2:rgba(255,255,255,.5);
--border:rgba(120,135,170,.32);--border-strong:rgba(80,100,140,.55);
--text-1:#1a1f2e;--text-2:#4a536a;--text-3:#8590a6;
--accent:#8a5cff;--accent-2:#3ccfd8;--accent-3:#ff84c4;
--grad:linear-gradient(135deg,#b8c4d8 0%,#f5f7fb 30%,#8a9ab8 55%,#e8ecf4 80%,#6b7a95 100%);
--grad-soft:linear-gradient(135deg,#c9e4ff,#f5d6ff 50%,#d6fffa);
--radius:26px;--radius-sm:16px;--radius-lg:36px;
--shadow:0 12px 30px rgba(70,90,130,.22),inset 0 1px 0 rgba(255,255,255,.9),inset 0 -1px 0 rgba(80,100,140,.2);
--shadow-lg:0 24px 60px rgba(70,90,130,.35),inset 0 2px 0 rgba(255,255,255,.95);
--font-sans:'Space Grotesk','Inter','Noto Sans SC',sans-serif;
--font-display:'Space Grotesk','Inter',sans-serif;
}
body{background:
linear-gradient(135deg,#c4cfe0 0%,#f0f3f8 25%,#aab8d0 50%,#f5f7fb 75%,#b8c4d8 100%)}
h1.title,.h1{background:linear-gradient(180deg,#f8faff 0%,#9aa8c4 50%,#4a5670 100%);
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
.card{backdrop-filter:blur(16px) saturate(140%);-webkit-backdrop-filter:blur(16px) saturate(140%)}
.pill{background:linear-gradient(180deg,#fff,#d4dcec);border-color:rgba(120,135,170,.4)}
@@ -0,0 +1,56 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>theme cell</title>
<link rel="stylesheet" href="../../assets/fonts.css">
<link rel="stylesheet" href="../../assets/base.css">
<link id="theme-link" rel="stylesheet" href="../../assets/themes/minimal-white.css">
<style>
html,body{height:100%;margin:0;overflow:hidden}
body{
background:var(--bg,#fff);color:var(--text-1,#111);
font-family:var(--font-sans);box-sizing:border-box;
padding:5cqw 6cqw;container-type:size;
}
.k{font:700 2.6cqw/1 var(--font-mono,monospace);color:var(--text-3,#888);letter-spacing:.14em;text-transform:uppercase;margin-bottom:2.5cqh}
h1{font:900 11cqw/.95 var(--font-display,var(--font-sans));letter-spacing:-.025em;margin:0 0 3cqh;color:var(--text-1)}
.lede{font:500 3.2cqw/1.4 var(--font-sans);color:var(--text-2,#555);margin:0 0 3.5cqh;max-width:85cqw}
.row{display:flex;gap:1.4cqw;flex-wrap:wrap}
.pill{padding:1.2cqh 2.4cqw;border-radius:999px;background:var(--surface-2,#f4f4f8);color:var(--text-1);font:600 2.3cqw/1 var(--font-sans);border:1px solid var(--border,#e5e5ea)}
.pill.accent{background:var(--accent,#7c5cff);color:#fff;border-color:transparent}
.kpi{margin-top:4cqh;display:flex;gap:5cqw}
.kpi div{font:900 10cqw/1 var(--font-display,var(--font-sans));letter-spacing:-.03em;color:var(--accent,#7c5cff)}
.kpi div span{display:block;font:500 1.8cqw/1 var(--font-sans);color:var(--text-3,#888);margin-top:1cqh;letter-spacing:.08em;text-transform:uppercase}
.gradient-hero{background:linear-gradient(90deg,var(--accent,#7c5cff),var(--accent-2,#22d3ee),var(--accent-3,#ff4d8d));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
</style>
</head>
<body>
<div class="k" id="kname">theme · minimal-white</div>
<h1 id="h1">2026<br>年度回顾</h1>
<p class="lede">同一份 outline,换一行 theme,排版、字体、色系、装饰全部重写。</p>
<div class="row">
<span class="pill accent">12 里程碑</span>
<span class="pill">团队 +40%</span>
<span class="pill">SAT 98%</span>
</div>
<div class="kpi">
<div>98%<span>Sat</span></div>
<div>12<span>MS</span></div>
<div>7×<span>Faster</span></div>
</div>
<script>
(function(){
const m = /[?&]theme=([\w-]+)/.exec(location.search||'');
if (m){
const name = m[1];
document.getElementById('theme-link').href = '../../assets/themes/'+name+'.css';
document.getElementById('kname').textContent = 'theme · ' + name;
if (/rainbow|aurora|cyberpunk|vaporwave|y2k/.test(name)){
document.getElementById('h1').classList.add('gradient-hero');
}
}
})();
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

@@ -0,0 +1,61 @@
<!doctype html>
<html lang="en" data-theme="aurora">
<head>
<meta charset="utf-8">
<title>47 animations showcase</title>
<link rel="stylesheet" href="../../assets/fonts.css">
<link rel="stylesheet" href="../../assets/base.css">
<link rel="stylesheet" href="../../assets/themes/aurora.css">
<link rel="stylesheet" href="../../assets/animations/animations.css">
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:1920px;height:1080px;overflow:hidden}
body{background:#0a0a14;font-family:"PingFang SC","Noto Sans SC","Inter",-apple-system,sans-serif;color:#fff;padding:48px 56px 44px;display:flex;flex-direction:column;gap:28px}
.hdr{display:flex;align-items:flex-end;justify-content:space-between}
.hdr h2{font:900 48px/1 "Inter",sans-serif;letter-spacing:-.02em;color:#fff}
.hdr h2 b{display:inline-block;font-size:58px;padding-right:14px;background:linear-gradient(90deg,#ff4d8d,#f59e0b,#22d3ee);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.hdr .sub{font:600 16px/1 "JetBrains Mono","SF Mono",monospace;color:#aab0c0;letter-spacing:.1em;text-transform:uppercase}
.grid{flex:1;display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(2,1fr);gap:18px;min-height:0}
.cell{position:relative;border-radius:18px;overflow:hidden;border:1px solid rgba(255,255,255,.08);box-shadow:0 20px 56px rgba(0,0,0,.45);background:#050510;min-height:0}
.cell .fx-host{position:absolute;inset:0}
.cell .label{position:absolute;left:14px;bottom:12px;z-index:5;font:700 11px/1 "JetBrains Mono","SF Mono",monospace;letter-spacing:.1em;padding:6px 12px;border-radius:999px;background:rgba(10,10,20,.72);color:#fff;text-transform:uppercase;border:1px solid rgba(255,255,255,.14);backdrop-filter:blur(6px)}
.cell .kind{position:absolute;left:14px;top:12px;z-index:5;font:700 10px/1 "JetBrains Mono","SF Mono",monospace;letter-spacing:.14em;padding:5px 10px;border-radius:999px;background:rgba(255,255,255,.1);color:#fff;text-transform:uppercase;border:1px solid rgba(255,255,255,.16)}
</style>
</head>
<body>
<div class="hdr">
<h2><b>47</b>Animations — 27 CSS · 20 Canvas FX</h2>
<div class="sub">html-ppt · data-anim="…" / data-fx="…" · pick 8 canvas FX</div>
</div>
<div class="grid">
<div class="cell"><span class="kind">data-fx</span><div class="fx-host" data-fx="knowledge-graph" style="--accent:#7c5cff;--accent-2:#22d3ee;--text-1:#fff"></div><span class="label">knowledge-graph</span></div>
<div class="cell"><span class="kind">data-fx</span><div class="fx-host" data-fx="neural-net" style="--accent:#22d3ee;--accent-2:#ff4d8d"></div><span class="label">neural-net</span></div>
<div class="cell"><span class="kind">data-fx</span><div class="fx-host" data-fx="galaxy-swirl" style="--accent:#7c5cff;--accent-2:#ff4d8d"></div><span class="label">galaxy-swirl</span></div>
<div class="cell"><span class="kind">data-fx</span><div class="fx-host" data-fx="constellation" style="--accent:#9fb4ff"></div><span class="label">constellation</span></div>
<div class="cell"><span class="kind">data-fx</span><div class="fx-host" data-fx="matrix-rain"></div><span class="label">matrix-rain</span></div>
<div class="cell"><span class="kind">data-fx</span><div class="fx-host" data-fx="starfield" style="--accent:#fff;--accent-2:#9fb4ff"></div><span class="label">starfield</span></div>
<div class="cell"><span class="kind">data-fx</span><div class="fx-host" data-fx="firework" style="--accent:#ff4d8d;--accent-2:#22d3ee"></div><span class="label">firework</span></div>
<div class="cell"><span class="kind">data-fx</span><div class="fx-host" data-fx="particle-burst" style="--accent:#ff4d8d;--accent-2:#7c5cff"></div><span class="label">particle-burst</span></div>
</div>
<script src="../../assets/animations/fx/_util.js"></script>
<script src="../../assets/animations/fx/knowledge-graph.js"></script>
<script src="../../assets/animations/fx/neural-net.js"></script>
<script src="../../assets/animations/fx/galaxy-swirl.js"></script>
<script src="../../assets/animations/fx/constellation.js"></script>
<script src="../../assets/animations/fx/matrix-rain.js"></script>
<script src="../../assets/animations/fx/starfield.js"></script>
<script src="../../assets/animations/fx/firework.js"></script>
<script src="../../assets/animations/fx/particle-burst.js"></script>
<script>
(function(){
document.querySelectorAll('[data-fx]').forEach(el => {
const name = el.getAttribute('data-fx');
const fn = window.HPX && window.HPX[name];
if (typeof fn === 'function') {
try { fn(el, {}); } catch(e) { console.warn('[fx]', name, e); }
}
});
})();
</script>
</body>
</html>
@@ -0,0 +1,72 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>31 layouts showcase</title>
<style>
:root{--ink:#0b0b10;--muted:#6b6b78;--line:#e7e7ef}
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:1920px;height:1080px;overflow:hidden}
body{background:#f6f7fa;font-family:"PingFang SC","Noto Sans SC","Inter",-apple-system,sans-serif;color:var(--ink);padding:48px 56px 44px;display:flex;flex-direction:column;gap:28px}
.hdr{display:flex;align-items:flex-end;justify-content:space-between}
.hdr h2{font:900 48px/1 "Inter",sans-serif;letter-spacing:-.02em}
.hdr h2 b{display:inline-block;font-size:58px;padding-right:14px;background:linear-gradient(90deg,#2563eb,#22d3ee);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.hdr .sub{font:600 16px/1 "JetBrains Mono","SF Mono",monospace;color:var(--muted);letter-spacing:.1em;text-transform:uppercase}
.grid{flex:1;display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(2,1fr);gap:18px;min-height:0}
.cell{position:relative;border-radius:18px;overflow:hidden;border:1px solid var(--line);box-shadow:0 16px 48px rgba(10,10,30,.1);background:#fff;min-height:0}
.cell iframe{position:absolute;inset:0;width:1920px;height:1080px;border:0;pointer-events:none;transform-origin:top left}
.cell .label{position:absolute;left:14px;bottom:12px;z-index:5;font:700 11px/1 "JetBrains Mono","SF Mono",monospace;letter-spacing:.1em;padding:6px 12px;border-radius:999px;background:rgba(255,255,255,.94);color:#1a1a22;text-transform:uppercase;border:1px solid rgba(0,0,0,.06)}
</style>
</head>
<body>
<div class="hdr">
<h2><b>31</b>Layouts — batteries included, demo data bundled</h2>
<div class="sub">html-ppt · templates/single-page/*.html · pick 8 of 31</div>
</div>
<div class="grid" id="grid"></div>
<script>
const LAYOUTS = [
['kpi-grid','KPI Grid'],
['chart-bar','Chart · Bar'],
['timeline','Timeline'],
['mindmap','Mindmap'],
['flow-diagram','Flow Diagram'],
['roadmap','Roadmap'],
['pros-cons','Pros / Cons'],
['code','Code']
];
const grid = document.getElementById('grid');
LAYOUTS.forEach(([name,label]) => {
const cell = document.createElement('div');
cell.className = 'cell';
const ifr = document.createElement('iframe');
ifr.src = '../../templates/single-page/' + name + '.html';
ifr.loading = 'eager';
cell.appendChild(ifr);
const lab = document.createElement('span');
lab.className = 'label';
lab.textContent = label + ' · ' + name;
cell.appendChild(lab);
grid.appendChild(cell);
});
function fit(){
document.querySelectorAll('.cell iframe').forEach(ifr => {
const c = ifr.parentElement;
const w = c.clientWidth, h = c.clientHeight;
const s = Math.min(w / 1920, h / 1080);
ifr.style.transform = 'scale('+s+')';
const sw = 1920*s, sh = 1080*s;
ifr.style.left = ((w - sw)/2) + 'px';
ifr.style.top = ((h - sh)/2) + 'px';
ifr.style.position = 'absolute';
});
}
window.addEventListener('resize', fit);
setTimeout(fit, 80);
setTimeout(fit, 400);
setTimeout(fit, 1200);
</script>
</body>
</html>
@@ -0,0 +1,72 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>14 full-deck templates showcase</title>
<style>
:root{--ink:#0b0b10;--muted:#6b6b78;--line:#e7e7ef}
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:1920px;height:1080px;overflow:hidden}
body{background:#f6f7fa;font-family:"PingFang SC","Noto Sans SC","Inter",-apple-system,sans-serif;color:var(--ink);padding:48px 56px 44px;display:flex;flex-direction:column;gap:28px}
.hdr{display:flex;align-items:flex-end;justify-content:space-between}
.hdr h2{font:900 48px/1 "Inter",sans-serif;letter-spacing:-.02em}
.hdr h2 b{display:inline-block;font-size:58px;padding-right:14px;background:linear-gradient(90deg,#f59e0b,#ff4d8d);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.hdr .sub{font:600 16px/1 "JetBrains Mono","SF Mono",monospace;color:var(--muted);letter-spacing:.1em;text-transform:uppercase}
.grid{flex:1;display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr);gap:20px;min-height:0}
.cell{position:relative;border-radius:20px;overflow:hidden;border:1px solid var(--line);box-shadow:0 20px 56px rgba(10,10,30,.12);background:#fff;min-height:0}
.cell.dark{background:#0a0a14;border-color:rgba(255,255,255,.08)}
.cell iframe{position:absolute;inset:0;width:1920px;height:1080px;border:0;pointer-events:none;transform-origin:top left}
.cell .label{position:absolute;left:16px;bottom:14px;z-index:5;font:700 12px/1 "JetBrains Mono","SF Mono",monospace;letter-spacing:.1em;padding:7px 14px;border-radius:999px;background:rgba(255,255,255,.94);color:#1a1a22;text-transform:uppercase;border:1px solid rgba(0,0,0,.06)}
.cell.dark .label{background:rgba(10,10,20,.78);color:#fff;border-color:rgba(255,255,255,.14)}
</style>
</head>
<body>
<div class="hdr">
<h2><b>14</b>Full-Deck Templates — complete world-views</h2>
<div class="sub">html-ppt · templates/full-decks/* · pick 6 of 14</div>
</div>
<div class="grid" id="grid"></div>
<script>
const DECKS = [
['graphify-dark-graph',true],
['xhs-post',false],
['hermes-cyber-terminal',true],
['knowledge-arch-blueprint',false],
['pitch-deck',false],
['xhs-white-editorial',false]
];
const grid = document.getElementById('grid');
DECKS.forEach(([name, dark]) => {
const cell = document.createElement('div');
cell.className = 'cell' + (dark ? ' dark' : '');
const ifr = document.createElement('iframe');
ifr.src = '../../templates/full-decks/' + name + '/index.html';
ifr.loading = 'eager';
cell.appendChild(ifr);
const lab = document.createElement('span');
lab.className = 'label';
lab.textContent = name;
cell.appendChild(lab);
grid.appendChild(cell);
});
function fit(){
document.querySelectorAll('.cell iframe').forEach(ifr => {
const c = ifr.parentElement;
const w = c.clientWidth, h = c.clientHeight;
const s = Math.min(w / 1920, h / 1080);
ifr.style.transform = 'scale('+s+')';
const sw = 1920*s, sh = 1080*s;
ifr.style.left = ((w - sw)/2) + 'px';
ifr.style.top = ((h - sh)/2) + 'px';
ifr.style.position = 'absolute';
});
}
window.addEventListener('resize', fit);
setTimeout(fit, 100);
setTimeout(fit, 500);
setTimeout(fit, 1500);
</script>
</body>
</html>
@@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>36 themes showcase</title>
<style>
:root{--ink:#0b0b10;--muted:#6b6b78;--line:#e7e7ef}
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:1920px;height:1080px;overflow:hidden}
body{background:#f6f7fa;font-family:"PingFang SC","Noto Sans SC","Inter","SF Pro Display",-apple-system,sans-serif;color:var(--ink);padding:48px 56px 44px;display:flex;flex-direction:column;gap:28px}
.hdr{display:flex;align-items:flex-end;justify-content:space-between}
.hdr h2{font:900 48px/1 "Inter",sans-serif;letter-spacing:-.02em}
.hdr h2 b{display:inline-block;font-size:58px;padding-right:14px;background:linear-gradient(90deg,#7c5cff,#22d3ee);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.hdr .sub{font:600 16px/1 "JetBrains Mono","SF Mono",monospace;color:var(--muted);letter-spacing:.1em;text-transform:uppercase}
.grid{flex:1;display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(2,1fr);gap:18px;min-height:0}
.cell{position:relative;border-radius:18px;overflow:hidden;border:1px solid var(--line);box-shadow:0 16px 48px rgba(10,10,30,.1);background:#fff;min-height:0}
.cell iframe{position:absolute;inset:0;width:100%;height:100%;border:0;pointer-events:none}
.cell .label{position:absolute;left:14px;bottom:12px;z-index:5;font:700 11px/1 "JetBrains Mono","SF Mono",monospace;letter-spacing:.1em;padding:6px 12px;border-radius:999px;background:rgba(255,255,255,.94);color:#1a1a22;text-transform:uppercase;border:1px solid rgba(0,0,0,.06)}
.cell.dark .label{background:rgba(10,10,20,.78);color:#fff;border-color:rgba(255,255,255,.14)}
</style>
</head>
<body>
<div class="hdr">
<h2><b>36</b>Themes — one keyword, new identity</h2>
<div class="sub">html-ppt · assets/themes/*.css · pick 8 of 36</div>
</div>
<div class="grid">
<div class="cell"><iframe src="_theme-cell.html?theme=minimal-white"></iframe><span class="label">minimal-white</span></div>
<div class="cell dark"><iframe src="_theme-cell.html?theme=tokyo-night"></iframe><span class="label">tokyo-night</span></div>
<div class="cell dark"><iframe src="_theme-cell.html?theme=aurora"></iframe><span class="label">aurora</span></div>
<div class="cell"><iframe src="_theme-cell.html?theme=xiaohongshu-white"></iframe><span class="label">xiaohongshu-white</span></div>
<div class="cell dark"><iframe src="_theme-cell.html?theme=cyberpunk-neon"></iframe><span class="label">cyberpunk-neon</span></div>
<div class="cell dark"><iframe src="_theme-cell.html?theme=dracula"></iframe><span class="label">dracula</span></div>
<div class="cell"><iframe src="_theme-cell.html?theme=soft-pastel"></iframe><span class="label">soft-pastel</span></div>
<div class="cell"><iframe src="_theme-cell.html?theme=magazine-bold"></iframe><span class="label">magazine-bold</span></div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="aurora">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>html-ppt · Demo Deck</title>
<link rel="stylesheet" href="../../assets/fonts.css">
<link rel="stylesheet" href="../../assets/base.css">
<link rel="stylesheet" id="theme-link" href="../../assets/themes/aurora.css">
<link rel="stylesheet" href="../../assets/animations/animations.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
</head>
<body data-themes="aurora,minimal-white,editorial-serif,tokyo-night,catppuccin-mocha,xiaohongshu-white,neo-brutalism,sunset-warm" data-theme-base="../../assets/themes/">
<div class="deck">
<!-- 1. Cover -->
<section class="slide" data-title="Cover">
<div class="deck-header"><span class="eyebrow">Tech sharing · 2026-04-15</span><span class="eyebrow">html-ppt</span></div>
<p class="kicker">Keynote · Demo</p>
<h1 class="h1 anim-rise-in" data-anim="rise-in">做一份<span class="gradient-text">像杂志</span>一样的<br>技术分享稿</h1>
<p class="lede">24 主题 · 30 版式 · 25 动效 · 零构建</p>
<div class="row wrap mt-l" style="gap:8px">
<span class="pill pill-accent">tokens</span>
<span class="pill">keyboard first</span>
<span class="pill">PNG export</span>
<span class="pill">CN + EN</span>
</div>
<div class="deck-footer"><span class="dim2">lewis · sudolewis@gmail.com</span><span class="slide-number" data-current="1" data-total="8"></span></div>
<div class="notes">Hi,今天我给大家演示一下 html-ppt 这套演讲系统是怎么工作的。这份 demo 本身就是用它做出来的——每一张幻灯片都只是几行 HTML + 一个 class。</div>
</section>
<!-- 2. TOC -->
<section class="slide" data-title="Agenda">
<p class="kicker">Agenda</p>
<h2 class="h2">今天 10 分钟,讲三件事</h2>
<div class="grid g3 mt-l anim-stagger-list" data-anim-target>
<div class="card card-accent"><h4>① 为什么</h4><p class="dim">每次做 PPT 都在重复劳动,而这件事 99% 可以模板化。</p></div>
<div class="card card-accent"><h4>② 怎么做</h4><p class="dim">tokens + layouts + animations,三层分离。</p></div>
<div class="card card-accent"><h4>③ 效果</h4><p class="dim">同一份 deck,一键切 24 种主题。</p></div>
</div>
<div class="deck-footer"><span class="dim2">agenda</span><span class="slide-number" data-current="2" data-total="8"></span></div>
<div class="notes">三段式结构——Why / How / Result。这是最稳的讲法。</div>
</section>
<!-- 3. Big quote -->
<section class="slide center tc" data-title="Quote">
<div style="max-width:1000px">
<div class="serif" style="font-size:120px;line-height:.9;color:var(--accent);opacity:.7">"</div>
<blockquote class="serif anim-fade-up" data-anim="fade-up" style="font-size:52px;line-height:1.3;margin:-30px 0 18px;font-weight:600;font-style:italic">
好的演讲稿是写出来的,<br>不是「做」出来的。
</blockquote>
<p class="dim" style="font-size:18px;letter-spacing:.1em">— 每一个被 PPT 折磨过的人</p>
</div>
<div class="deck-footer"><span class="dim2">quote</span><span class="slide-number" data-current="3" data-total="8"></span></div>
<div class="notes">这里停一秒。让这句话自己说话。</div>
</section>
<!-- 4. Stat -->
<section class="slide center tc" data-title="Stat">
<div>
<p class="kicker">The result</p>
<div style="font-size:240px;line-height:1;font-weight:900">
<span class="counter gradient-text" data-to="92">0</span><span class="gradient-text">%</span>
</div>
<h3 class="mt-s">你花在 PPT 上的时间可以被省下</h3>
<p class="lede" style="margin:14px auto 0">10 份真实 deck 的平均测试数据。</p>
</div>
<div class="deck-footer"><span class="dim2">proof</span><span class="slide-number" data-current="4" data-total="8"></span></div>
<div class="notes">强调:数据来源——自己真实的 10 个 deck。</div>
</section>
<!-- 5. Two column -->
<section class="slide" data-title="Tokens">
<p class="kicker">How · 核心思路</p>
<h2 class="h2">把「看起来像什么」收进 <code>:root</code></h2>
<div class="grid g2 mt-l" style="align-items:start">
<div class="card anim-fade-left" data-anim="fade-left">
<h3>概念</h3>
<p class="dim">每一种视觉属性——颜色、字体、圆角、阴影——都变成语义变量。</p>
<ul class="mt-m">
<li><code>--text-1</code> / <code>--text-2</code> / <code>--text-3</code></li>
<li><code>--surface</code> / <code>--surface-2</code></li>
<li><code>--accent</code> / <code>--accent-2</code> / <code>--accent-3</code></li>
<li><code>--radius</code> / <code>--shadow</code> / <code>--grad</code></li>
</ul>
</div>
<div class="card anim-fade-right" data-anim="fade-right">
<h3>示例</h3>
<pre class="mono" style="font-size:13px;background:var(--surface-2);padding:16px;border-radius:var(--radius-sm);overflow:auto;margin:0">
/* assets/themes/aurora.css */
:root {
--bg: #06091c;
--text-1: #e8f0ff;
--accent: #5ef2c6;
--accent-2: #7aa2ff;
--accent-3: #c984ff;
--radius: 20px;
}</pre>
<p class="dim mt-m" style="font-size:13px">——整个 aurora 主题就这么大。</p>
</div>
</div>
<div class="deck-footer"><span class="dim2">how</span><span class="slide-number" data-current="5" data-total="8"></span></div>
<div class="notes">关键是:base.css 只认变量名,不认具体色值。换主题 = 换一份变量。</div>
</section>
<!-- 6. Chart -->
<section class="slide" data-title="Chart">
<p class="kicker">Numbers · 实际效果</p>
<h2 class="h2">做 deck 的时间分布,使用前/使用后</h2>
<div class="card mt-l" style="height:440px;padding:24px"><canvas id="chart"></canvas></div>
<div class="deck-footer"><span class="dim2">data</span><span class="slide-number" data-current="6" data-total="8"></span></div>
<div class="notes">使用后,「写内容」时间占比大幅上升,其他一切下降——这正是我们想要的。</div>
</section>
<!-- 7. CTA -->
<section class="slide center tc" data-title="CTA">
<div style="max-width:920px">
<p class="kicker">Your turn</p>
<h1 class="h1 anim-rise-in" data-anim="rise-in" style="font-size:96px"><span class="gradient-text">开始</span>做你的第一份</h1>
<p class="lede" style="margin:14px auto 30px">复制一份 deck,换你的内容,按 <b>T</b> 选一个最对味的主题,讲完还能一键导 PNG。</p>
<div class="row" style="justify-content:center;gap:14px">
<div class="card" style="padding:18px 26px"><code>./scripts/new-deck.sh my-talk</code></div>
</div>
<p class="dim2 mt-l" style="font-size:14px">←/→ 翻页 · T 主题 · A 动效 · F 全屏 · O 概览 · S 备注</p>
</div>
<div class="deck-footer"><span class="dim2">cta</span><span class="slide-number" data-current="7" data-total="8"></span></div>
<div class="notes">最后给一个具体的行动:一条命令。别停留在「我回去试试」。</div>
</section>
<!-- 8. Thanks -->
<section class="slide center tc" data-title="Thanks">
<div>
<div class="anim-confetti-burst" style="display:inline-block;padding:20px"></div>
<h1 class="h1" style="font-size:180px;line-height:1"><span class="gradient-text">Thanks</span></h1>
<p class="lede">lewis · sudolewis@gmail.com · MIT 2026</p>
</div>
<div class="deck-footer"><span class="dim2">end</span><span class="slide-number" data-current="8" data-total="8"></span></div>
<div class="notes">谢谢大家。Q&amp;A 时间。</div>
</section>
</div>
<script src="../../assets/runtime.js"></script>
<script>
addEventListener('DOMContentLoaded',()=>{
const css=getComputedStyle(document.documentElement);
const a1=css.getPropertyValue('--accent').trim();
const a2=css.getPropertyValue('--accent-2').trim();
const text2=css.getPropertyValue('--text-2').trim();
const border=css.getPropertyValue('--border').trim();
new Chart(document.getElementById('chart'),{type:'bar',
data:{labels:['写内容','挑版式','调样式','出图','动效'],
datasets:[
{label:'使用前 (分钟)',data:[92,48,36,22,14],backgroundColor:a2,borderRadius:6},
{label:'使用后 (分钟)',data:[18,3,2,1,1],backgroundColor:a1,borderRadius:6}]},
options:{plugins:{legend:{labels:{color:text2}}},
scales:{x:{ticks:{color:text2},grid:{color:border}},
y:{ticks:{color:text2},grid:{color:border}}}}});
});
</script>
</body></html>
+147
View File
@@ -0,0 +1,147 @@
# Animations catalog
All animations live in `assets/animations/animations.css`. Apply them by
adding `class="anim-<name>"` OR `data-anim="<name>"` to any element
(`runtime.js` re-triggers `data-anim` elements whenever a slide becomes
active, so you get the entry effect every time you navigate onto the slide).
Open `templates/animation-showcase.html` to browse all of them — one slide
per animation, auto-playing on slide enter. Press **A** on any slide to cycle
a random animation on the current page.
## Directional fades
| name | effect | use for |
|---|---|---|
| `fade-up` | Translate from +32 px, fade. | Default for paragraph + card entry. |
| `fade-down` | Translate from -32 px, fade. | Headers / banners / callouts. |
| `fade-left` | Translate from -40 px. | Left column in a two-column layout. |
| `fade-right` | Translate from +40 px. | Right column in a two-column layout. |
## Dramatic entries
| name | effect | use for |
|---|---|---|
| `rise-in` | +60 px rise + blur-off. | Slide titles, hero headlines. |
| `drop-in` | -60 px drop + slight scale. | Banners, alert bars. |
| `zoom-pop` | Scale 0.6 → 1.04 → 1. | Buttons, stat numbers, CTAs. |
| `blur-in` | 18 px blur clears. | Cover page reveal. |
| `glitch-in` | Clip-path steps + jitter. | Tech / cyber / error states. |
## Text effects
| name | effect | use for |
|---|---|---|
| `typewriter` | Monospace-like type reveal. | One-liners, slogans. |
| `neon-glow` | Cyclic text-shadow pulse. | Terminal-green / dracula themes. |
| `shimmer-sweep` | White sheen passes across. | Metallic buttons, premium cards. |
| `gradient-flow` | Infinite horizontal gradient slide. | Brand wordmarks. |
## Lists & numbers
| name | effect | use for |
|---|---|---|
| `stagger-list` | Children rise-in one-by-one. | Any `<ul>` or `.grid`. |
| `counter-up` | Number ticks 0 → target. | KPI, stat-highlight pages. |
Counter markup:
```html
<span class="counter" data-to="1248">0</span>
```
## SVG / geometry
| name | effect | use for |
|---|---|---|
| `path-draw` | Strokes draw themselves. | Lines, arrows, diagrams. |
| `morph-shape` | Path `d` morph. | Background shapes. |
Put `class="anim-path-draw"` on `<svg>`; every path/line/circle inside gets drawn.
## 3D & perspective
| name | effect | use for |
|---|---|---|
| `parallax-tilt` | Hover → 3D tilt. | Hero cards, product shots. |
| `card-flip-3d` | Y-axis 90° flip. | Before/after reveal. |
| `cube-rotate-3d` | Rotate in from a cube side. | Section dividers. |
| `page-turn-3d` | Left-hinge page turn. | Editorial / story flows. |
| `perspective-zoom` | Pull from -400 Z. | Cover openings. |
## Ambient / continuous
| name | effect | use for |
|---|---|---|
| `marquee-scroll` | Infinite horizontal loop. | Client logo strips. |
| `kenburns` | 14 s slow zoom on images. | Hero backgrounds. |
| `confetti-burst` | Pseudo-element sparkle burst. | Thanks / win pages. |
| `spotlight` | Circular clip-path reveal. | Big reveal moments. |
| `ripple-reveal` | Corner-origin ripple reveal. | Section transitions. |
## Respecting motion preferences
All animations are disabled automatically when
`prefers-reduced-motion: reduce` is set. Do not override this.
## Tips
- Prefer `data-anim="..."` over `class="anim-..."` so that the runtime
re-triggers the animation whenever the slide becomes active.
- Use at most 1-2 distinct animation types on a single slide. Mixing 5 looks
messy.
- Stagger lists + a single hero entry = clean rhythm.
- For counter-up, pair with `stat-highlight.html` or `kpi-grid.html`.
## FX (canvas)
CSS animations are fire-and-forget entry effects. **FX** are live, continuously
running canvas/DOM effects that start when their slide becomes active and stop
when it leaves. They are loaded by `assets/animations/fx-runtime.js`, which
dynamically pulls every module under `assets/animations/fx/*.js` and watches
`.slide.is-active` to run lifecycle.
Add to any page:
```html
<script src="../assets/animations/fx-runtime.js"></script>
```
Then drop one of these into any slide:
```html
<div data-fx="particle-burst" style="width:100%;height:360px;"></div>
```
The container just needs a size — the FX auto-sizes a canvas to fit with
`ResizeObserver` + DPR correction. Colors read your theme (`--accent`,
`--accent-2`, `--ok`, `--warn`, `--danger`).
| name | effect | use case | trigger |
|---|---|---|---|
| `particle-burst` | Particles explode from center, gravity + fade, re-bursts every 2.5s. | Reveal moments, stat pages. | `<div data-fx="particle-burst">` |
| `confetti-cannon` | Colored rotating rects arcing from both bottom corners. | Thank you / success pages. | `<div data-fx="confetti-cannon">` |
| `firework` | Rockets from bottom explode into colored sparks, continuous. | Celebration, launch slides. | `<div data-fx="firework">` |
| `starfield` | 3D perspective starfield flying outward. | Sci-fi / deep space backgrounds. | `<div data-fx="starfield">` |
| `matrix-rain` | Falling green katakana + hex columns. | Cyber / security / data theme. | `<div data-fx="matrix-rain">` |
| `knowledge-graph` | Force-directed graph, 28 labeled nodes, ~50 edges, live physics. | Knowledge / RAG / graph slides. | `<div data-fx="knowledge-graph">` |
| `neural-net` | 4-6-6-3 feedforward net with pulses traveling along edges. | ML / model architecture slides. | `<div data-fx="neural-net">` |
| `constellation` | Drifting points, linked when within 150 px, opacity by distance. | Ambient hero backgrounds. | `<div data-fx="constellation">` |
| `orbit-ring` | 5 concentric rings with dots at different speeds, radial glow. | System / planet / layered concepts. | `<div data-fx="orbit-ring">` |
| `galaxy-swirl` | Logarithmic spiral of ~800 particles, slow rotation. | Cover pages, intros. | `<div data-fx="galaxy-swirl">` |
| `word-cascade` | Words fall from top, pile up at bottom. | Vocabulary / concept cloud slides. | `<div data-fx="word-cascade">` |
| `letter-explode` | Heading letters fly in from random directions, loops every ~4.5s. | Big titles, hero text. | `<div data-fx="letter-explode" data-fx-text-value="EXPLODE">` |
| `chain-react` | 8 circles with a domino pulse wave traveling across. | Pipeline / sequential flow. | `<div data-fx="chain-react">` |
| `magnetic-field` | Particles travel bezier/sin curves leaving trails. | Energy / flow / abstract. | `<div data-fx="magnetic-field">` |
| `data-stream` | Rows of scrolling hex/binary text, cyberpunk. | Data, API, security. | `<div data-fx="data-stream">` |
| `gradient-blob` | 4 drifting blurred radial gradients (additive). | Soft hero backgrounds. | `<div data-fx="gradient-blob">` |
| `sparkle-trail` | Pointer-driven sparkle emitter (auto-wiggles if idle). | Interactive reveal, hover canvases. | `<div data-fx="sparkle-trail">` |
| `shockwave` | Expanding rings from center on loop. | Impact, launch, alert. | `<div data-fx="shockwave">` |
| `typewriter-multi` | 3 lines typing concurrently with blinking block cursors (DOM). | Terminal, agent boot log. | `<div data-fx="typewriter-multi" data-fx-line1="> boot...">` |
| `counter-explosion` | Number counts 0 → target, bursts particles, resets after 4s. | KPI reveal, record highs. | `<div data-fx="counter-explosion" data-fx-to="2400">` |
FX tips:
- One FX per slide is almost always enough. Mix with regular CSS `data-anim`
effects for layered polish.
- The container needs an explicit size (height) — the canvas fills 100%.
- Every module respects theme custom properties. Set `--accent` / `--accent-2`
on the slide or element to recolor on the fly.
- Lifecycle is automatic: entering a slide starts the FX, leaving stops it and
frees the canvas. You can also call `window.__hpxReinit(el)` manually.
@@ -0,0 +1,141 @@
# Authoring guide
How to turn a user request ("make me a deck about X") into a finished
html-ppt deck. Follow these steps in order.
## 1. Understand the deck
Before touching files, clarify:
1. **Audience** — engineers? designers? executives? consumers?
2. **Length** — 5 min lightning? 20 min share? 45 min talk?
3. **Language** — Chinese, English, bilingual? (Noto Sans SC is preloaded.)
4. **Format** — on-screen live, PDF export, 小红书图文?
5. **Tone** — clinical / playful / editorial / cyber?
The audience + tone map to a theme; the length maps to slide count; the
format maps to runtime features (live → notes + T-cycle; PDF → page-break
CSS, already handled in `base.css`).
## 2. Pick a theme
Use `references/themes.md`. When in doubt:
- **Engineers** → `catppuccin-mocha` / `tokyo-night` / `dracula`.
- **Designers / product** → `editorial-serif` / `aurora` / `soft-pastel`.
- **Execs** → `minimal-white` / `arctic-cool` / `swiss-grid`.
- **Consumers** → `xiaohongshu-white` / `sunset-warm` / `soft-pastel`.
- **Cyber / CLI / infra** → `terminal-green` / `blueprint` / `gruvbox-dark`.
- **Pitch / bold** → `neo-brutalism` / `sharp-mono` / `bauhaus`.
- **Launch / product reveal** → `glassmorphism` / `aurora`.
Wire the theme as `<link id="theme-link" href="../assets/themes/NAME.css">`
and list 3-5 alternatives in `data-themes` so the user can press T to audition.
## 3. Outline the deck
A solid 20-minute deck is usually:
```
cover → toc → section-divider #1 → [2-4 body pages] →
section-divider #2 → [2-4 body pages] → section-divider #3 →
[2-4 body pages] → cta → thanks
```
Pick 1 layout per page from `references/layouts.md`. Don't repeat the same
layout twice in a row.
## 4. Scaffold the deck
```bash
./scripts/new-deck.sh my-talk
```
This copies `templates/deck.html` into `examples/my-talk/index.html` with
paths rewritten. Add/remove `<section class="slide">` blocks to match your
outline.
## 5. Author each slide
For each outline item:
1. Open the matching single-page layout, e.g. `templates/single-page/kpi-grid.html`.
2. Copy the `<section class="slide">…</section>` block.
3. Paste into your deck.
4. Replace demo data with real data. Keep the class structure intact.
5. Set `data-title="..."` (used by the Overview grid).
6. Add `<div class="notes">…</div>` with speaker notes.
## 6. Add animations sparingly
Rules of thumb:
- Cover/title: `rise-in` or `blur-in`.
- Body content: `fade-up` for the hero element, `stagger-list` for grids/lists.
- Stat pages: `counter-up`.
- Section dividers: `perspective-zoom` or `cube-rotate-3d`.
- Closer: `confetti-burst` on the "Thanks" text.
Pick **one** accent animation per slide. Everything else should be calm.
## 7. Chinese + English decks
- Fonts are already imported in `fonts.css` (Noto Sans SC + Noto Serif SC).
- Use `lang="zh-CN"` on `<html>`.
- For bilingual titles, stack lines: `<h1 class="h1">主标题<br><span class="dim">English subtitle</span></h1>`.
- Keep English subtitles in a lighter weight (300) and dim color to avoid
visual competition.
## 8. Review in-browser
```bash
open examples/my-talk/index.html
```
Walk through every slide with ← →. Press:
- **O** — overview grid; catch any layout clipping.
- **T** — cycle themes; make sure nothing looks broken in any theme.
- **S** — open speaker notes; verify every slide has notes.
## 9. Export to PNG
```bash
# single slide
./scripts/render.sh examples/my-talk/index.html
# all slides (autodetect count by looking for .slide sections)
./scripts/render.sh examples/my-talk/index.html all
# explicit slide count + output dir
./scripts/render.sh examples/my-talk/index.html 12 out/my-talk-png
```
Output is 1920×1080 by default. Change in `render.sh` if the user wants 3:4
for 小红书图文 (1242×1660).
## 10. What to NOT do
- Don't hand-author from a blank file.
- Don't use raw hex colors in slide markup. Use tokens.
- Don't load heavy animation frameworks. Everything should stay within the
CSS/JS that already ships.
- Don't add more than one new template file unless a genuinely new layout
type is needed. Prefer composition.
- Don't delete slides from the showcase decks.
- **Don't put presenter-only text on the slide.** Any descriptive text,
narration cues, or explanations meant for the speaker (e.g. "这一页的重点是…",
"Note: mention X here", small grey captions explaining the slide's purpose)
MUST go inside `<div class="notes">`, not as visible elements. The `.notes`
div is hidden (`display:none`) and only shown via the S overlay. Slides
should contain ONLY audience-facing content.
## Troubleshooting
- **Theme doesn't switch with T**: check `data-themes` on `<body>` and
`data-theme-base` pointing to the themes directory relative to the HTML
file.
- **Fonts fall back**: make sure `fonts.css` is linked before the theme.
- **Chart.js colors wrong**: charts read CSS vars in JS; make sure they run
after the DOM is ready (`addEventListener('DOMContentLoaded', …)`).
- **PNG too small**: bump `--window-size` in `scripts/render.sh`.
+98
View File
@@ -0,0 +1,98 @@
# Full-Deck Templates
Self-contained multi-slide HTML decks under `templates/full-decks/<name>/`. Each folder contains:
- `index.html` — complete multi-slide deck (cover / section / content / code / chart or diagram / CTA / thanks, 7+ slides)
- `style.css` — scoped with `.tpl-<name>` class prefix so multiple templates can coexist
- `README.md` — short rationale, inspiration, and use guidance
All templates pull the shared `assets/fonts.css`, `assets/base.css`, and `assets/runtime.js` from the skill root. Navigate with `← →` / `space`, use `F` for fullscreen, `O` for overview.
Use these when you want a coherent, opinionated look for an entire deck — not a mix-and-match of layouts. Each template is visually distinctive enough to be identified at a glance.
---
## 1. xhs-white-editorial — 白底杂志风
- **Source inspiration:** `20260409 升级版知识库/小红书图文/v2-白底版/slide_01_cover.html` + `20260412-AI测试与安全/html/xhs-ai-testing-safety-v2.html`
- **Key visual traits:** pure-white background, top 10-color rainbow bar, 80-110px display headlines, purple→blue→green→orange→pink gradient text, macaron soft-card set (soft-purple/pink/blue/green/orange), black-on-white `.focus` pills, hero quote box.
- **When to use:** dual-purpose XHS image + horizontal deck; dense text with strong emphasis; Chinese-first audience.
- **Path:** `templates/full-decks/xhs-white-editorial/index.html`
## 2. graphify-dark-graph — 暗底知识图谱
- **Source inspiration:** `20260413-graphify/ppt/graphify.html`
- **Key visual traits:** `#06060c→#0e1020` deep-night gradient, drifting blur orbs, SVG force-directed graph overlay on cover, rainbow-shift gradient headlines, JetBrains Mono command-line glow, glass-morphism cards (warm/blue/green/purple/danger). Accent palette: amber `#e8a87c`, mint `#7ed3a4`, mist-blue `#7eb8da`, lilac `#b8a4d6`.
- **When to use:** dev-tool / CLI / knowledge-graph / data-viz launches; live-demo decks that want an "AI-native + sci-fi + warm" vibe.
- **Path:** `templates/full-decks/graphify-dark-graph/index.html`
## 3. knowledge-arch-blueprint — 奶油蓝图架构
- **Source inspiration:** `20260405-Karpathy-知识库/20260405 架构图v2.html`
- **Key visual traits:** cream paper `#F0EAE0` base, single rust accent `#B5392A`, 48px blueprint grid mask, hard 2px black border cards, pipeline step-boxes with one hero raised, right-side rust insight callout, Playfair serif big numbers, SVG dashed feedback-loop arrows. Zero gradients, zero soft shadows.
- **When to use:** system architecture diagrams, data-flow maps, engineering white-papers; you want a serious, printable, README-friendly feel.
- **Path:** `templates/full-decks/knowledge-arch-blueprint/index.html`
## 4. hermes-cyber-terminal — 暗终端 honest-review
- **Source inspiration:** `20260414-hermes-agent/ppt/hermes-record.html` + `hermes-vs-openclaw.html`
- **Key visual traits:** `#0a0c10` black, 56px cyber grid + CRT vignette + scanlines, window traffic-light chrome, `$ prompt` command-line headlines, mint-green `#7ed3a4` glow big text, JetBrains Mono throughout, stroke-only bar charts, blinking cursor, amber/green/red tag hierarchy, dark code box.
- **When to use:** reviews of CLI / agent / dev tools with trace, diff, and benchmarks; when you want the "honest technical reviewer" voice.
- **Path:** `templates/full-decks/hermes-cyber-terminal/index.html`
## 5. obsidian-claude-gradient — GitHub 暗紫渐变
- **Source inspiration:** `20260406-obsidian-claude/slides.html`
- **Key visual traits:** GitHub-dark `#0d1117`, purple+blue radial ambient plus 60px masked grid, center-aligned layout, purple pill tags, three-stop gradient text `#a855f7→#60a5fa→#34d399`, GitHub-ish code palette (`#010409` bg + purple/blue/orange/green tokens), purple-left-border highlight block.
- **When to use:** developer workflow / MCP / Agent / dev-tool tutorials; feels like GitHub Blog / Linear Changelog; config + steps heavy content.
- **Path:** `templates/full-decks/obsidian-claude-gradient/index.html`
## 6. testing-safety-alert — 红琥珀警示
- **Source inspiration:** `20260412-AI测试与安全/html/xhs-ai-testing-safety-v2.html`
- **Key visual traits:** top and bottom 45° red-black hazard stripes, red strike-through negation headlines, L1/L2/L3 green/amber/red tier cards, alert-box with circular status dot, policy-yaml code block with red left border and `bad` keyword highlighting, red/green checklist, Q1 incident stacked bar chart.
- **When to use:** safety / risk / incident post-mortem / red-team / pre-launch AI review / policy-as-code; when the audience needs to feel "this is serious, don't skim".
- **Path:** `templates/full-decks/testing-safety-alert/index.html`
## 7. xhs-pastel-card — 柔和马卡龙慢生活
- **Source inspiration:** `20260412-obsidian-skills/html/xhs-obsidian-skills.html` + pastel patterns shared with `20260409` v2-白底版
- **Key visual traits:** cream `#fef8f1` base, three soft blurred blobs, Playfair italic serif display headlines mixed with sans body, full-color 28px rounded macaron cards (peach / mint / sky / lilac / lemon / rose), italic Playfair `01-04` numerals, SVG donut chart, chip+page topbar.
- **When to use:** lifestyle / personal-growth / slow-living / emotional content; when you want a "magazine, handmade, not-so-techy" feel; themes like rest, pause, softness.
- **Path:** `templates/full-decks/xhs-pastel-card/index.html`
## 8. dir-key-nav-minimal — 方向键 8 色极简
- **Source inspiration:** `20260405-Karpathy-知识库/20260405 演示幻灯片【方向键版】.html`
- **Key visual traits:** 8 slides each on its own mono background (indigo / cream / crimson / emerald / slate / violet / white / charcoal), each with its own accent color, 160px display headline + 4px stubby accent line divider, arrow `→` prefixed Mono list, bottom-left `← →` kbd hint plus bottom-right page label, huge breathing negative space.
- **When to use:** keynote-style minimalist talk where you have something to say and not much to show; one idea per slide; talks / launches / public presentations.
- **Path:** `templates/full-decks/dir-key-nav-minimal/index.html`
---
## Scenario decks (generic, reusable)
These are not extracted from a single source — they are generic scaffolds for the most common presentation jobs. Each is visually distinctive and content-rich out of the box.
| # | Name | Slides | Feel | When to use |
|---|---|---|---|---|
| 9 | `pitch-deck` | 10 | White + blue→purple gradient, YC/VC vibe, big numbers, traction chart | Fundraising, startup pitch, investor meeting |
| 10 | `product-launch` | 8 | Dark hero + light content, warm orange→peach, feature cards, pricing tiers, CTA | Announcing a product, launch keynote |
| 11 | `tech-sharing` | 8 | GitHub-dark, JetBrains Mono, terminal code blocks, agenda + Q&A | 技术分享, internal tech talk, conference talk |
| 12 | `weekly-report` | 7 | Corporate clarity, 8-cell KPI grid, shipped list, 8-week bar chart, next-week table | 周报, team status update, business review |
| 13 | `xhs-post` | 9 | **3:4 @ 810×1080**, warm pastel, dashed sticker cards, page dots | 小红书 图文 post, Instagram carousel |
| 14 | `course-module` | 7 | Warm paper + Playfair serif, persistent left sidebar of learning objectives, MCQ self-check | 教学模块, online course, workshop module |
| 15 | `presenter-mode-reveal` 🎤 | 6 | **演讲者模式专用** · tokyo-night 默认 · 5 主题 T 键切换 · 每页带 150–300 字逐字稿示例 | **技术分享/演讲/课程**—需要按 S 键看逐字稿的场景 ✨ |
Each folder: `index.html`, scoped `style.css` (prefixed `.tpl-<name>`), `README.md`. The `xhs-post` template overrides the default `.slide` box to fixed `810×1080` for 3:4 portrait.
> 🎤 **任何演讲场景(技术分享 / 课程 / 路演)都推荐用 `presenter-mode-reveal`**,或者参考 [presenter-mode.md](./presenter-mode.md) 指南给其他模板加 `<aside class="notes">` 逐字稿。
---
## Authoring notes
- Every template scopes its CSS under `.tpl-<name>` so two or more templates can load on the same page without collisions.
- Swap demo content, but keep the structural classes — they are what gives each template its identity.
- The shared runtime (`assets/runtime.js`) provides keyboard nav, fullscreen, overview grid, theme cycling — you don't need to add any JS.
- Charts are hand-rolled SVG (no CDN dependency). Feel free to replace with chart.js / echarts if you need interactive data.
+103
View File
@@ -0,0 +1,103 @@
# Layouts catalog
Every layout lives in `templates/single-page/<name>.html` as a fully
functional standalone page with realistic demo data. Open any file directly
in Chrome to see it working.
To compose a new deck: open the file, copy the `<section class="slide">…</section>`
block (or multiple blocks) into your deck HTML, and replace the demo data.
Shared CSS (base, theme, animations) is already wired by `deck.html`.
## Openers & transitions
| file | purpose |
|---|---|
| `cover.html` | Deck cover. Kicker + huge title + lede + pill row. |
| `toc.html` | Table of contents. 2×3 grid of numbered cards. |
| `section-divider.html` | Big numbered section break (02 · Theme). |
## Text-centric
| file | purpose |
|---|---|
| `bullets.html` | Classic bullet list with card-wrapped items. |
| `two-column.html` | Concept + example side by side. |
| `three-column.html` | Three equal pillars with icons. |
| `big-quote.html` | Full-bleed pull quote in editorial-serif style. |
## Numbers & data
| file | purpose |
|---|---|
| `stat-highlight.html` | One giant number + subtitle (uses `.counter` animation). |
| `kpi-grid.html` | 4 KPIs in a row with up/down deltas. |
| `table.html` | Data table with hover rows, right-aligned numerics. |
| `chart-bar.html` | Chart.js bar chart, theme-aware colors. |
| `chart-line.html` | Chart.js dual-line chart with filled area. |
| `chart-pie.html` | Chart.js doughnut + takeaways card. |
| `chart-radar.html` | Chart.js radar comparing 2 products on 6 axes. |
## Code & terminal
| file | purpose |
|---|---|
| `code.html` | Syntax-highlighted code via highlight.js (JS example). |
| `diff.html` | Hand-rolled +/- diff view. |
| `terminal.html` | Terminal window mock with traffic-light header. |
## Diagrams & flows
| file | purpose |
|---|---|
| `flow-diagram.html` | 5-node pipeline with arrows and one highlighted node. |
| `arch-diagram.html` | 3-tier architecture grid. |
| `process-steps.html` | 4 numbered steps in cards. |
| `mindmap.html` | Radial mindmap with SVG path-draw animation. |
## Plans & comparisons
| file | purpose |
|---|---|
| `timeline.html` | 5-point horizontal timeline with dots. |
| `roadmap.html` | 4-column NOW / NEXT / LATER / VISION. |
| `gantt.html` | 12-week gantt chart with 5 parallel tracks. |
| `comparison.html` | Before vs After two-panel card. |
| `pros-cons.html` | Pros and cons two-card layout. |
| `todo-checklist.html` | Checklist with checked/unchecked states. |
## Visuals
| file | purpose |
|---|---|
| `image-hero.html` | Full-bleed hero with Ken Burns gradient background. |
| `image-grid.html` | 7-cell bento grid with gradient placeholders. |
## Closers
| file | purpose |
|---|---|
| `cta.html` | Call-to-action with big gradient headline + buttons. |
| `thanks.html` | Final "Thanks" page with confetti burst. |
## Picking a layout
- **Opener**: `cover.html`, often followed by `toc.html`.
- **Section break**: `section-divider.html` before every major section.
- **Core content**: `bullets.html`, `two-column.html`, `three-column.html`.
- **Show numbers**: `stat-highlight.html` (single) or `kpi-grid.html` (4-up).
- **Show plot**: `chart-bar.html` / `chart-line.html` / `chart-pie.html` / `chart-radar.html`.
- **Show a diff or change**: `comparison.html`, `diff.html`, `pros-cons.html`.
- **Show a plan**: `timeline.html`, `roadmap.html`, `gantt.html`, `process-steps.html`.
- **Show architecture**: `arch-diagram.html`, `flow-diagram.html`, `mindmap.html`.
- **Code / demo**: `code.html`, `terminal.html`.
- **Closer**: `cta.html``thanks.html`.
## Naming / structure conventions
- Each slide is `<section class="slide" data-title="...">`.
- Header pills: `<p class="kicker">…</p>`, eyebrow: `<p class="eyebrow">…</p>`.
- Titles: `<h1 class="h1">…</h1>` / `<h2 class="h2">…</h2>`.
- Lede: `<p class="lede">…</p>`.
- Cards: `<div class="card">…</div>` (variants: `card-soft`, `card-outline`, `card-accent`).
- Grids: `.grid.g2`, `.grid.g3`, `.grid.g4`.
- Notes: `<div class="notes">…</div>` per slide.
@@ -0,0 +1,240 @@
# Presenter Mode Guide · 演讲者模式指南
这份文档说明如何在 html-ppt skill 里做出**带逐字稿的演讲者模式 PPT**。
## 何时使用演讲者模式
当用户的需求涉及以下任何一项时,**优先使用演讲者模式**:
- 提到"**演讲**"、"**分享**"、"**讲稿**"、"**逐字稿**"、"**speaker notes**"
- 提到"**presenter view**"、"**演讲者视图**"、"**演讲者模式**"
- 需要"**30 分钟 / 45 分钟 / 1 小时**的分享"
- 说"我要去给团队讲 xxx"、"要做一场技术分享"、"要做路演"
- 强调"**不想忘词**"、"**怕讲不流畅**"、"**需要提词器**"
如果用户只要做一份"静态好看的 PPT"(例如小红书图文、产品图册、汇报 slides 自己不讲),**不需要**演讲者模式。
## 两种做法
### ✅ 推荐做法:直接用 `presenter-mode-reveal` 模板
```bash
cp -r templates/full-decks/presenter-mode-reveal examples/my-talk
```
这个模板已经预设好所有必需元素:
- 支持 S 键切换演讲者视图
- 5 个主题可用 T 键循环(tokyo-night / dracula / catppuccin-mocha / nord / corporate-clean
- 左右键翻页
- 每一页都有 150–300 字的示例逐字稿
- 底部有键位提示
直接改内容即可。
### 🔧 进阶做法:给任意已有模板加演讲者模式
html-ppt 的 **S 键演讲者视图是 `runtime.js` 内置的,所有 full-deck 模板都自动支持**。你只需要做两件事:
1. **每张 slide 末尾加 `<aside class="notes">`**(或 `<div class="notes">`),里面写逐字稿
2. **确认 HTML 引入了 `assets/runtime.js`**
```html
<section class="slide">
<h2>你的标题</h2>
<p>内容...</p>
<aside class="notes">
<p>这里是演讲时要说的话,150-300 字...</p>
</aside>
</section>
```
## 逐字稿写作三铁律
这是整个方法论的核心。AI 在帮用户写逐字稿时必须遵守:
### 铁律 1:不是讲稿,是"提示信号"
**错误写法**(像在念稿):
```
大家好,欢迎来到今天的分享。今天我将要给大家介绍一下我们团队在过去三个月做的工作。
首先,我们来看一下背景情况。在过去的三个月中,我们遇到了以下几个问题……
```
**正确写法**(提示信号 + 加粗核心):
```
<p>欢迎!今天分享我们团队<strong>过去 3 个月</strong>的工作。</p>
<p>先说<em>背景</em>——三个月前我们遇到了<strong>三个核心问题</strong>
延迟高、成本炸、稳定性差。</p>
<p>接下来逐个讲解怎么解的。</p>
```
**差别**:正确版本把关键词加粗,过渡句独立成段,看一眼就能接上。
### 铁律 2:每页 150300 字
- **少于 150 字**:提示不够,讲到一半会卡
- **多于 300 字**:你根本来不及扫完
- **2–3 分钟/页** 是最舒服的节奏
### 铁律 3:用口语,不用书面语
| ❌ 书面语 | ✅ 口语 |
|---|---|
| 因此 | 所以 |
| 该方案 | 这个方案 |
| 然而 | 但是 / 不过 |
| 进行优化 | 优化一下 |
| 我们将会 | 我们会 / 接下来 |
| 综上所述 | 所以简单来说 |
**检查方法**:写完读一遍,听起来像说话才对。
## 必备 HTML 结构
```html
<!DOCTYPE html>
<html lang="zh-CN" data-themes="tokyo-night,dracula,corporate-clean">
<head>
<meta charset="utf-8">
<title>...</title>
<link rel="stylesheet" href="../../../assets/fonts.css">
<link rel="stylesheet" href="../../../assets/base.css">
<link rel="stylesheet" id="theme-link" href="../../../assets/themes/tokyo-night.css">
<link rel="stylesheet" href="../../../assets/animations/animations.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="deck">
<section class="slide" data-title="Cover">
<h1>你的标题</h1>
<p>副标题</p>
<aside class="notes">
<p>讲稿段落 1(加<strong>加粗关键词</strong>)。</p>
<p>讲稿段落 2(过渡句独立成段)。</p>
<p>讲稿段落 3(自然收尾,引出下一页)。</p>
</aside>
</section>
<!-- 更多 slide ... -->
</div>
<script src="../../../assets/runtime.js"></script>
</body>
</html>
```
## 演讲者视图显示的内容
`S` 键后,**弹出一个独立的演讲者窗口**(原页面保持观众视图不变)。演讲者窗口是 **4 个独立的磁吸卡片**
```
观众窗口(原页面) 演讲者窗口(磁吸卡片)
┌─────────────────┐ ┌─────────────────────┬──────────────────┐
│ │ │ 🔵 CURRENT │ 🟣 NEXT │
│ 正常 slide │ │ ━━━━━━━━━━━━━━━━ │ ━━━━━━━━━━━━━ │
│ 全屏展示 │◄►│ │ iframe preview │
│ │ │ iframe preview │ (下一页) │
│ │ │ (当前页) ├──────────────────┤
│ │ │ │ 🟠 SPEAKER SCRIPT │
│ │ │ │ ━━━━━━━━━━━━━ │
│ │ ├─────────────────────┤ [大字号逐字稿] │
│ │ │ 🟢 TIMER │ [可滚动] │
│ │ │ ⏱ 12:34 3 / 8 │ │
│ │ │ [← Prev][Next →] │ │
└─────────────────┘ └─────────────────────┴──────────────────┘
↑ BroadcastChannel 双向同步翻页 ↑
```
卡片交互规则:
- **拖动卡片 header**(带彩色圆点和标题的顶部条)→ 移动卡片位置
- **拖动卡片右下角的三角手柄** → 调整卡片大小
- **位置/尺寸自动保存到 localStorage**,下次打开恢复
- 底部 "重置布局" 按钮恢复默认排列
卡片内容:
- 🔵 **CURRENT** — 当前页 **像素级完美预览**iframe 加载原 HTML 文件的 `?preview=N` 模式,错色不可能)
- 🟣 **NEXT** — 下一页预览,同样像素级完美
- 🟠 **SPEAKER SCRIPT** — 逐字稿,字号 18px,支持 `<strong>` (橘色加粗)、`<em>` (蓝色强调)、`<code>` 等 inline 样式
- 🟢 **TIMER** — 计时器不会丢失焦点,带切页按钮
两窗口同步:在任一窗口按 ← → 翻页,另一个窗口自动同步(BroadcastChannel)。
丝滑翻页:iframe 只加载一次,后续翻页用 `postMessage` 切换可见的 slide,**不重新加载、不闪烁**。
## 键盘快捷键(演讲者模式)
| 键 | 动作 |
|---|---|
| `S` | 打开演讲者窗口(弹出新窗口,原页面保持观众视图) |
| `←` `→` / Space / PgDn | 翻页(即使在演讲者视图里) |
| `T` | 切换主题 |
| `R` | 重置计时器(仅演讲者视图下) |
| `F` | 全屏 |
| `O` | 总览 |
| `Esc` | 关闭所有浮层 |
## 双屏演讲的标准流程
1. 打开 `index.html`,按 `S` → 弹出演讲者窗口
2. 把**观众窗口**(原页面)拖到投影 / 外接屏,按 `F` 全屏
3. 把**演讲者窗口**(弹窗)留在你面前的屏幕
4. 在任一窗口按 ← → 翻页,两边自动同步
5. 演讲者窗口里看逐字稿 + 下一页 + 计时器
> 💡 **为什么预览像素级完美**:每个预览是一个 `<iframe>`,它加载的就是同一个 deck HTML 文件,只是 URL 多了 `?preview=N` 参数。`runtime.js` 检测到这个参数时只渲染第 N 页、隐藏所有 chrome。**iframe 使用与观众视图完全相同的 CSS、主题、字体和 viewport**——颜色和排版保证一致。外层用 CSS `transform: scale()` 把 1920×1080 缩到卡片宽高,等比缩放不变形。
> 💡 **为什么不闪烁**:iframe 初次加载后就常驻,翻页时 presenter 窗口通过 `postMessage({type:'preview-goto', idx:N})` 告诉 iframe 切换到第 N 页。iframe 内的 runtime.js 只切换 `.is-active` class**不重新加载、不渲染白屏**。
## 常见错误
### ❌ 把逐字稿写在 slide 可见位置
```html
<!-- 错误:这段文字观众会看到 -->
<p style="font-size:12px;color:gray">
这里讲 xxx,然后讲 yyy...
</p>
```
✅ 正确:
```html
<aside class="notes">
<p>这里讲 xxx,然后讲 yyy...</p>
</aside>
```
`.notes` 类默认 `display:none`,只在演讲者视图可见。
### ❌ 忘记引入 runtime.js
没有 `<script src="../../../assets/runtime.js"></script>` = 没有 S 键、没有演讲者视图、没有翻页。
### ❌ 逐字稿用书面语
念出来像 AI 机器人。**写完一定读一遍**。
### ❌ 每页 50 字
提示不够,照样忘词。
### ❌ 每页 500 字
眼睛根本扫不过来,等于没写。
## 用 AI 生成逐字稿的标准 prompt
> "请为每一张 slide 写一段 **150-300 字**的逐字稿,放在 `<aside class="notes">` 里。
> 要求:
> 1. 用**口语**,不要书面语(所以/但是/接下来,不是因此/然而/综上所述)
> 2. 把**核心关键词**用 `<strong>` 加粗
> 3. 过渡句独立成段(每段 1-3 句)
> 4. 读起来像说话,不像念稿
> 5. 结尾要有自然的过渡,引出下一页"
## 推荐搭配
- **主题**`tokyo-night`(深色,技术分享首选)、`corporate-clean`(浅色,商务汇报)、`dracula`(深色备选)
- **字体**:默认 Noto Sans SC + JetBrains Mono,无需更改
- **动效**:克制使用,`fade-up` / `rise-in` 最自然,不要用 `glitch-in` / `confetti-burst` 之类花哨的
- **页数**30 分钟分享 = 812 页;45 分钟 = 1216 页;1 小时 = 1622 页
+107
View File
@@ -0,0 +1,107 @@
# Themes catalog
Every theme is a short CSS file in `assets/themes/` that overrides tokens
defined in `assets/base.css`. Switch themes by changing the `href` of
`<link id="theme-link">` or by pressing **T** if the deck has a
`data-themes="a,b,c"` attribute on `<body>` or `<html>`.
All themes define the same variables: `--bg`, `--bg-soft`, `--surface`,
`--surface-2`, `--border`, `--text-1/2/3`, `--accent`, `--accent-2/3`,
`--good`, `--warn`, `--bad`, `--grad`, `--grad-soft`, `--radius*`, `--shadow*`,
`--font-sans`, `--font-display`.
## Light & calm
| name | description | when to use |
|---|---|---|
| `minimal-white` | 极简白,克制高级。Inter,强文字层级,极低阴影。 | 内部汇报、一对一技术评审、不抢内容的严肃话题 |
| `editorial-serif` | 杂志风 Playfair 衬线 + 奶油底。 | 品牌故事、文字密度大的长文演讲 |
| `soft-pastel` | 柔和马卡龙三色渐变。 | 产品发布、面向消费者、轻松话题 |
| `xiaohongshu-white` | 小红书白底 + 暖红 accent + 衬线标题。 | 小红书图文、生活/美学类内容 |
| `solarized-light` | 经典低眩光配色。 | 长时间观看的工作坊、教学 |
| `catppuccin-latte` | catppuccin 浅色。 | 开发者、极客友好的技术分享 |
## Bold & statement
| name | description | when to use |
|---|---|---|
| `sharp-mono` | 纯黑白 + Archivo Black + 硬阴影。 | 宣言类、极具冲击力的视觉 |
| `neo-brutalism` | 厚描边、硬阴影、明黄 accent。 | 创业路演、敢说敢做的调性 |
| `bauhaus` | 几何 + 红黄蓝原色。 | 设计 talk、艺术史/产品美学主题 |
| `swiss-grid` | 瑞士网格 + Helvetica 感 + 12 栏底纹。 | 严肃排版、设计行业 |
| `memphis-pop` | 孟菲斯波普背景点 + 大字标题。 | 年轻、潮流、品牌合作 |
## Cool & dark
| name | description | when to use |
|---|---|---|
| `catppuccin-mocha` | catppuccin 深。 | 开发者内部分享、长时间观看 |
| `dracula` | 经典 Dracula 紫红主色。 | 代码密集的技术分享 |
| `tokyo-night` | Tokyo Night 蓝夜。 | 偏冷技术分享、基础设施 |
| `nord` | 北欧清冷蓝白。 | 基础设施、云产品 |
| `gruvbox-dark` | 温暖复古深色。 | Terminal / vim / *nix 社群 |
| `rose-pine` | 玫瑰松,柔和暗色。 | 设计+开发交界、审美向技术 |
| `arctic-cool` | 蓝/青/石板灰 浅色版。 | 商业分析、金融、冷静理性 |
## Warm & vibrant
| name | description | when to use |
|---|---|---|
| `sunset-warm` | 橘 / 珊瑚 / 琥珀三色渐变。 | 生活方式、奖项颁发、情绪正向 |
## Effect-heavy
| name | description | when to use |
|---|---|---|
| `glassmorphism` | 毛玻璃 + 多色光斑背景。 | Apple 式发布会、产品特性展示 |
| `aurora` | 极光渐变 + blur + saturate。 | 封面 / CTA / 结语页 |
| `rainbow-gradient` | 白底 + 彩虹流动渐变 accent。 | 欢乐向、节日、庆祝页 |
| `blueprint` | 蓝图工程 + 网格底纹 + 蒙太奇字体。 | 系统架构、工程蓝图 |
| `terminal-green` | 绿屏终端 + 等宽 + 发光文字。 | CLI/black-hat/复古朋克 |
## v2 additions
### Light & professional
| name | description | when to use |
|---|---|---|
| `corporate-clean` | 纯白 + 海军蓝 accent + Inter + 保守边框。 | 董事会汇报、B2B 销售、金融保险 |
| `pitch-deck-vc` | YC 风白底 + 蓝紫渐变 accent + 大留白。 | 融资路演、种子轮、VC meeting |
| `academic-paper` | 论文白 + 衬线正文 + 黑墨 + 蓝链接。 | 学术报告、研究分享、会议论文 |
| `japanese-minimal` | 象牙白 + 朱红 accent + 极大留白 + Noto Serif。 | 品牌升级、匠人故事、禅意叙事 |
| `engineering-whiteprint` | 白底 + 坐标纸网格 + 海军墨线 + 等宽字。 | 系统设计、API 文档、架构白皮书 |
### Bold & editorial
| name | description | when to use |
|---|---|---|
| `magazine-bold` | 奶油底 + 超大 Playfair 衬线 + 橙色 spot。 | 专栏文章、封面故事、品牌月刊 |
| `news-broadcast` | 白底 + 红色竖条 + Oswald 大写 + 硬阴影。 | 突发新闻、发布通稿、数据播报 |
| `midcentury` | 奶油底 + 芥末/青/焦橙 + 锐利几何。 | 设计史、家居美学、复古品牌 |
| `retro-tv` | 暖奶油 + CRT 扫描线 + 琥珀橙 accent。 | 怀旧叙事、八零九零年代主题 |
### Effect-heavy / dramatic
| name | description | when to use |
|---|---|---|
| `cyberpunk-neon` | 纯黑 + 霓虹粉青黄 + 发光 + JetBrains Mono。 | 黑客、地下文化、赛博 talk |
| `vaporwave` | 深紫 + 粉红青蓝渐变 + 晕染光斑。 | 音乐、潮流艺术、A E S T H E T I C |
| `y2k-chrome` | 银铬渐变 + 彩虹 accent + 大圆角 + Space Grotesk。 | 千禧怀旧、时尚品牌、Gen-Z |
## How to apply
```html
<link rel="stylesheet" id="theme-link" href="../assets/themes/aurora.css">
```
Or enable `T`-cycling by listing themes on the body:
```html
<body data-themes="minimal-white,aurora,catppuccin-mocha" data-theme-base="../assets/themes/">
```
## How to extend
Copy an existing theme, rename it, and override only the variables you want to
change. Keep each theme under ~200 lines. Prefer adjusting tokens to adding
new selectors.
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# html-ppt :: new-deck.sh — scaffold a new deck from templates/deck.html
#
# Usage:
# new-deck.sh <name> [output-parent-dir]
#
# Creates <parent>/<name>/index.html with paths rewritten to point at the
# skill's shared assets/themes/animations. Defaults to ./examples/.
set -euo pipefail
NAME="${1:-}"
if [[ -z "$NAME" ]]; then
echo "usage: new-deck.sh <name> [parent-dir]" >&2
exit 1
fi
PARENT="${2:-examples}"
HERE="$(cd "$(dirname "$0")/.." && pwd)"
TEMPLATE="$HERE/templates/deck.html"
if [[ ! -f "$TEMPLATE" ]]; then
echo "error: template not found at $TEMPLATE" >&2
exit 1
fi
OUT_DIR="$HERE/$PARENT/$NAME"
if [[ -e "$OUT_DIR" ]]; then
echo "error: $OUT_DIR already exists" >&2
exit 1
fi
mkdir -p "$OUT_DIR"
# templates/deck.html references ../assets/...; for examples/<name>/index.html
# that same relative path (../../assets/...) needs one more ../.
sed 's|href="../assets/|href="../../assets/|g; s|src="../assets/|src="../../assets/|g; s|data-theme-base="../assets/|data-theme-base="../../assets/|g' \
"$TEMPLATE" > "$OUT_DIR/index.html"
echo "✔ created $OUT_DIR/index.html"
echo ""
echo "next steps:"
echo " open $OUT_DIR/index.html"
echo " # press T to cycle themes, ← → to navigate, O for overview"
echo ""
echo " # render to PNG:"
echo " $HERE/scripts/render.sh $OUT_DIR/index.html all"
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# html-ppt :: render.sh — headless Chrome screenshot(s)
#
# Usage:
# render.sh <html-file> # one PNG, slide 1
# render.sh <html-file> <N> # N PNGs, slides 1..N, via #/k
# render.sh <html-file> all # autodetect .slide count
# render.sh <html-file> <N> <out-dir> # custom output dir
#
# Requires: Google Chrome at /Applications/Google Chrome.app (macOS).
set -euo pipefail
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
if [[ ! -x "$CHROME" ]]; then
echo "error: Chrome not found at $CHROME" >&2
exit 1
fi
FILE="${1:-}"
if [[ -z "$FILE" ]]; then
echo "usage: render.sh <html> [N|all] [out-dir]" >&2
exit 1
fi
if [[ ! -f "$FILE" ]]; then
echo "error: $FILE not found" >&2
exit 1
fi
COUNT="${2:-1}"
OUT="${3:-}"
ABS="$(cd "$(dirname "$FILE")" && pwd)/$(basename "$FILE")"
STEM="$(basename "${FILE%.*}")"
if [[ "$COUNT" == "all" ]]; then
COUNT="$(grep -c 'class="slide"' "$FILE" || true)"
[[ -z "$COUNT" || "$COUNT" -lt 1 ]] && COUNT=1
fi
if [[ -z "$OUT" ]]; then
if [[ "$COUNT" -gt 1 ]]; then
OUT="$(dirname "$FILE")/${STEM}-png"
mkdir -p "$OUT"
fi
fi
render_one() {
local url="$1" target="$2"
"$CHROME" \
--headless=new \
--disable-gpu \
--hide-scrollbars \
--no-sandbox \
--virtual-time-budget=4000 \
--window-size=1920,1080 \
--screenshot="$target" \
"$url" >/dev/null 2>&1
echo "$target"
}
if [[ "$COUNT" == "1" ]]; then
OUT_FILE="${OUT:-$(dirname "$FILE")/${STEM}.png}"
render_one "file://$ABS" "$OUT_FILE"
else
for i in $(seq 1 "$COUNT"); do
render_one "file://$ABS#/$i" "$OUT/${STEM}_$(printf '%02d' "$i").png"
done
fi
echo "done: rendered $COUNT slide(s) from $FILE"
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Some files were not shown because too many files have changed in this diff Show More