Separate Chinese display from plugin selector
Keep the installable plugin selector ASCII while restoring the Chinese user-facing display name for the patent drafting skill.\n\nConstraint: Codex marketplace entry name is the install selector and must remain ASCII kebab-case.\nRejected: Put Chinese in marketplace.json name | Codex list/add skips or fails to resolve that selector.\nConfidence: high\nScope-risk: narrow\nDirective: Use interface.displayName for Chinese aliases; never put localized text in plugin ids, directories, manifest name, or marketplace entry name.\nTested: python scripts/validate_marketplace.py; python /Users/eapil/.codex/skills/.system/plugin-creator/scripts/validate_plugin.py plugins/codex/plugins/patent-application-drafting-skill; temporary CODEX_HOME marketplace list and plugin add smoke test\nNot-tested: Interactive in-chat skill invocation was not rerun for this display-only change
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "html-ppt",
|
||||
"version": "0.1.0",
|
||||
"description": "HTML PPT Studio,用模板、主题、布局和动画创作专业静态 HTML 演示文稿、小红书图文和多页分享稿。",
|
||||
"author": {
|
||||
"name": "lewislulu",
|
||||
"url": "https://github.com/lewislulu"
|
||||
},
|
||||
"homepage": "https://github.com/lewislulu/html-ppt-skill",
|
||||
"repository": "https://github.com/lewislulu/html-ppt-skill",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"html-ppt",
|
||||
"presentation",
|
||||
"slides",
|
||||
"templates",
|
||||
"animations"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"interface": {
|
||||
"displayName": "HTML PPT Studio",
|
||||
"shortDescription": "用主题、布局和动画模板创作专业 HTML 演示文稿。",
|
||||
"longDescription": "HTML PPT Studio 提供多套主题、完整 deck 模板、单页布局、CSS/Canvas 动画、演讲者模式和导出脚本,用于生成演示文稿、技术分享、路演稿、小红书图文和静态 HTML slides。",
|
||||
"developerName": "lewislulu",
|
||||
"category": "文档处理",
|
||||
"capabilities": [
|
||||
"Read",
|
||||
"Write"
|
||||
],
|
||||
"defaultPrompt": [
|
||||
"使用 HTML PPT Studio 帮我制作一份演示文稿。",
|
||||
"使用 HTML PPT Studio 帮我生成带演讲者备注的 HTML slides。"
|
||||
],
|
||||
"websiteURL": "https://github.com/lewislulu/html-ppt-skill",
|
||||
"privacyPolicyURL": "https://github.com/lewislulu/html-ppt-skill",
|
||||
"termsOfServiceURL": "https://github.com/lewislulu/html-ppt-skill",
|
||||
"brandColor": "#0F766E",
|
||||
"screenshots": []
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,224 @@
|
||||
# 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 <sudolewis@gmail.com>
|
||||
**License:** MIT
|
||||
**中文文档:** [README.zh-CN.md](README.zh-CN.md)
|
||||
|
||||
|
||||
|
||||
## 🎤 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`.
|
||||
|
||||
|
||||
**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. **150–300 words per slide** — that's the ~2–3 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` |
|
||||
|
||||
### 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`.
|
||||
|
||||
|
||||
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).
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
||||
|
||||
*The big iframe is loading `templates/single-page/<name>.html` directly and cycling through all 31 layouts every 2.8 seconds.*
|
||||
|
||||
|
||||
### 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
|
||||
└── 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 <sudolewis@gmail.com>.
|
||||
@@ -0,0 +1,228 @@
|
||||
# html-ppt · HTML PPT 工作室
|
||||
|
||||
> 一款专业级的 AgentSkill,让 AI 做出真正能打的 HTML 演示文稿。
|
||||
> **36 套主题**、**15 套完整 deck 模板**、**31 种页面布局**、**47 个动效**
|
||||
> (27 个 CSS + 20 个 Canvas FX),加上全新的 **演讲者模式** —— 像素级
|
||||
> 完美预览 + 逐字稿提词器 + 计时器。纯静态 HTML/CSS/JS,无需构建。
|
||||
|
||||
**作者:** lewis <sudolewis@gmail.com>
|
||||
**协议:** MIT
|
||||
**English docs:** [README.md](README.md)
|
||||
|
||||
|
||||
> 一行命令装好 **36 主题 × 20 Canvas FX × 31 布局 × 15 完整 deck + 演讲者模式**。
|
||||
|
||||
## 🎤 演讲者模式(全新)
|
||||
|
||||
在任何 deck 里按 `S` 键,弹出一个独立的演讲者窗口,包含 4 个**可拖拽、
|
||||
可调整大小的磁吸卡片**:当前页预览、下一页预览、逐字稿、计时器。两个窗口
|
||||
通过 `BroadcastChannel` 双向同步翻页。
|
||||
|
||||
|
||||
**为什么预览是像素级完美的:** 每个卡片是一个 `<iframe>`,加载的是**同一
|
||||
份 deck HTML 文件**,只是 URL 多了 `?preview=N` 参数。runtime 检测到这个
|
||||
参数后,只渲染第 N 页并隐藏所有 chrome —— 所以预览使用**和观众视图完全相
|
||||
同的 CSS、主题、字体、viewport**,颜色和排版保证 100% 一致。
|
||||
|
||||
**丝滑翻页(零闪烁):** 翻页时演讲者窗口通过 `postMessage({type:'preview-goto',
|
||||
idx:N})` 通知 iframe,iframe 只是切换 `.is-active` class —— **不重新加载、
|
||||
不白屏、不闪烁**。
|
||||
|
||||
**逐字稿 3 条铁律:**
|
||||
1. **提示信号,不是讲稿** — 关键词加粗,过渡句独立成段
|
||||
2. **每页 150–300 字** — 约 2–3 分钟/页的节奏
|
||||
3. **用口语,不用书面语** — "所以" 不是 "因此","这个" 不是 "该"
|
||||
|
||||
详见 [`references/presenter-mode.md`](references/presenter-mode.md),或直接复制
|
||||
`templates/full-decks/presenter-mode-reveal/` 这个现成模板 —— 每一页都带完整
|
||||
150–300 字的示例逐字稿。
|
||||
|
||||
## 一行命令安装
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/lewislulu/html-ppt-skill
|
||||
```
|
||||
|
||||
装好后,任何支持 AgentSkill 的 agent(Claude 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` |
|
||||
|
||||
### 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`
|
||||
|
||||
|
||||
每个主题都是一份纯 CSS token 文件 —— 只需要换一行 `<link>` 就能给整份 deck
|
||||
换皮。在 `templates/theme-showcase.html` 里可以浏览全部(每一页用独立 iframe
|
||||
渲染,避免样式互相污染)。
|
||||
|
||||
|
||||
### 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 种单页布局
|
||||
|
||||
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 立即看得到效果。
|
||||
|
||||
|
||||
*大 iframe 直接加载 `templates/single-page/<name>.html` 文件,每 2.8 秒
|
||||
自动切换到下一个布局。*
|
||||
|
||||
|
||||
### 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
|
||||
└── 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 <sudolewis@gmail.com>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"sourceId": "html-ppt",
|
||||
"repo": "https://github.com/lewislulu/html-ppt-skill.git",
|
||||
"ref": "main",
|
||||
"commit": "f3a8435d3901697d5ac5e64d356c933637e43107",
|
||||
"adapter": "claude-skill",
|
||||
"sourcePath": ".",
|
||||
"syncedAt": "2026-06-11T03:57:53Z"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,224 @@
|
||||
# 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 <sudolewis@gmail.com>
|
||||
**License:** MIT
|
||||
**中文文档:** [README.zh-CN.md](README.zh-CN.md)
|
||||
|
||||
|
||||
|
||||
## 🎤 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`.
|
||||
|
||||
|
||||
**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. **150–300 words per slide** — that's the ~2–3 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` |
|
||||
|
||||
### 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`.
|
||||
|
||||
|
||||
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).
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
||||
|
||||
*The big iframe is loading `templates/single-page/<name>.html` directly and cycling through all 31 layouts every 2.8 seconds.*
|
||||
|
||||
|
||||
### 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
|
||||
└── 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 <sudolewis@gmail.com>.
|
||||
@@ -0,0 +1,228 @@
|
||||
# html-ppt · HTML PPT 工作室
|
||||
|
||||
> 一款专业级的 AgentSkill,让 AI 做出真正能打的 HTML 演示文稿。
|
||||
> **36 套主题**、**15 套完整 deck 模板**、**31 种页面布局**、**47 个动效**
|
||||
> (27 个 CSS + 20 个 Canvas FX),加上全新的 **演讲者模式** —— 像素级
|
||||
> 完美预览 + 逐字稿提词器 + 计时器。纯静态 HTML/CSS/JS,无需构建。
|
||||
|
||||
**作者:** lewis <sudolewis@gmail.com>
|
||||
**协议:** MIT
|
||||
**English docs:** [README.md](README.md)
|
||||
|
||||
|
||||
> 一行命令装好 **36 主题 × 20 Canvas FX × 31 布局 × 15 完整 deck + 演讲者模式**。
|
||||
|
||||
## 🎤 演讲者模式(全新)
|
||||
|
||||
在任何 deck 里按 `S` 键,弹出一个独立的演讲者窗口,包含 4 个**可拖拽、
|
||||
可调整大小的磁吸卡片**:当前页预览、下一页预览、逐字稿、计时器。两个窗口
|
||||
通过 `BroadcastChannel` 双向同步翻页。
|
||||
|
||||
|
||||
**为什么预览是像素级完美的:** 每个卡片是一个 `<iframe>`,加载的是**同一
|
||||
份 deck HTML 文件**,只是 URL 多了 `?preview=N` 参数。runtime 检测到这个
|
||||
参数后,只渲染第 N 页并隐藏所有 chrome —— 所以预览使用**和观众视图完全相
|
||||
同的 CSS、主题、字体、viewport**,颜色和排版保证 100% 一致。
|
||||
|
||||
**丝滑翻页(零闪烁):** 翻页时演讲者窗口通过 `postMessage({type:'preview-goto',
|
||||
idx:N})` 通知 iframe,iframe 只是切换 `.is-active` class —— **不重新加载、
|
||||
不白屏、不闪烁**。
|
||||
|
||||
**逐字稿 3 条铁律:**
|
||||
1. **提示信号,不是讲稿** — 关键词加粗,过渡句独立成段
|
||||
2. **每页 150–300 字** — 约 2–3 分钟/页的节奏
|
||||
3. **用口语,不用书面语** — "所以" 不是 "因此","这个" 不是 "该"
|
||||
|
||||
详见 [`references/presenter-mode.md`](references/presenter-mode.md),或直接复制
|
||||
`templates/full-decks/presenter-mode-reveal/` 这个现成模板 —— 每一页都带完整
|
||||
150–300 字的示例逐字稿。
|
||||
|
||||
## 一行命令安装
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/lewislulu/html-ppt-skill
|
||||
```
|
||||
|
||||
装好后,任何支持 AgentSkill 的 agent(Claude 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` |
|
||||
|
||||
### 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`
|
||||
|
||||
|
||||
每个主题都是一份纯 CSS token 文件 —— 只需要换一行 `<link>` 就能给整份 deck
|
||||
换皮。在 `templates/theme-showcase.html` 里可以浏览全部(每一页用独立 iframe
|
||||
渲染,避免样式互相污染)。
|
||||
|
||||
|
||||
### 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 种单页布局
|
||||
|
||||
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 立即看得到效果。
|
||||
|
||||
|
||||
*大 iframe 直接加载 `templates/single-page/<name>.html` 文件,每 2.8 秒
|
||||
自动切换到下一个布局。*
|
||||
|
||||
|
||||
### 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
|
||||
└── 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 <sudolewis@gmail.com>
|
||||
@@ -0,0 +1,223 @@
|
||||
---
|
||||
name: html-ppt
|
||||
description: "用主题、布局、动画和演讲者模式模板创作专业静态 HTML 演示文稿、小红书图文和多页分享稿。"
|
||||
---
|
||||
|
||||
# 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 150–300 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. **每页 150–300 字** — 2–3 分钟/页的节奏
|
||||
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 <sudolewis@gmail.com>.
|
||||
@@ -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(); } };
|
||||
};
|
||||
})();
|
||||
+49
@@ -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(); } };
|
||||
};
|
||||
})();
|
||||
+58
@@ -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(); } };
|
||||
};
|
||||
})();
|
||||
+69
@@ -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(); } };
|
||||
};
|
||||
})();
|
||||
+51
@@ -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(); } };
|
||||
};
|
||||
})();
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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%)}
|
||||
@@ -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)}
|
||||
@@ -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}
|
||||
+26
@@ -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}
|
||||
@@ -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,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&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>
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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:每页 150–300 字
|
||||
|
||||
- **少于 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 分钟分享 = 8–12 页;45 分钟 = 12–16 页;1 小时 = 16–22 页
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="aurora">
|
||||
<head>
|
||||
<meta charset="utf-8"><title>Animation + FX Showcase — html-ppt</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">
|
||||
<style>
|
||||
.fx-stage{
|
||||
position:relative;
|
||||
margin:20px auto 0;
|
||||
width:min(900px, 92%);
|
||||
height:380px;
|
||||
border-radius:var(--radius-lg, 16px);
|
||||
background:rgba(10,12,22,0.55);
|
||||
border:1px solid var(--border,#2a2a3a);
|
||||
box-shadow:var(--shadow-lg, 0 20px 60px rgba(0,0,0,.4));
|
||||
overflow:hidden;
|
||||
}
|
||||
.fx-label{
|
||||
position:absolute;top:14px;left:16px;z-index:5;
|
||||
font-family:var(--font-mono, ui-monospace,Menlo,monospace);
|
||||
font-size:11px;letter-spacing:.14em;color:var(--text-3,#a0a0b8);
|
||||
text-transform:uppercase;
|
||||
background:rgba(0,0,0,0.35);padding:4px 10px;border-radius:999px;
|
||||
border:1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.fx-replay{
|
||||
position:absolute;top:12px;right:14px;z-index:5;
|
||||
appearance:none;cursor:pointer;
|
||||
padding:7px 14px;border-radius:999px;
|
||||
background:linear-gradient(135deg, var(--accent,#7c5cff), var(--accent-2,#22d3ee));
|
||||
color:#fff;font:600 12px system-ui,sans-serif;
|
||||
border:0;letter-spacing:.05em;
|
||||
box-shadow:0 4px 16px rgba(124,92,255,0.35);
|
||||
}
|
||||
.fx-replay:hover{ filter:brightness(1.1); transform:translateY(-1px); }
|
||||
.fx-indicator{position:absolute;top:24px;right:40px;font-family:var(--font-mono);font-size:11px;color:var(--text-3);letter-spacing:.1em}
|
||||
.anim-box{margin:24px auto 0;width:640px;height:240px;border-radius:var(--radius-lg);background:var(--grad-soft);display:flex;align-items:center;justify-content:center;font-size:44px;font-weight:800;color:var(--text-1);box-shadow:var(--shadow-lg);border:1px solid var(--border)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="deck"></div>
|
||||
|
||||
<script>
|
||||
const FX = [
|
||||
['particle-burst', 'Particles explode from center, gravity + fade, re-bursts every ~2.5s.'],
|
||||
['confetti-cannon', 'Colored rotating rects arcing from both bottom corners.'],
|
||||
['firework', 'Rockets launch from the bottom and burst into colored sparks.'],
|
||||
['starfield', '3D perspective starfield — infinite flythrough.'],
|
||||
['matrix-rain', 'Classic green katakana + hex columns raining down.'],
|
||||
['knowledge-graph', 'Force-directed graph, 28 labeled nodes, live physics + springs.'],
|
||||
['neural-net', '4-6-6-3 feedforward net with pulses traveling along edges.'],
|
||||
['constellation', 'Drifting points connect when close — ambient background.'],
|
||||
['orbit-ring', '5 concentric rings with dots rotating at different speeds.'],
|
||||
['galaxy-swirl', 'Logarithmic spiral with ~800 particles.'],
|
||||
['word-cascade', 'Words fall from top and pile up at the bottom.'],
|
||||
['letter-explode', 'Letters fly in from random directions, loops.'],
|
||||
['chain-react', 'Row of 8 circles — a wave pulse dominoes across them.'],
|
||||
['magnetic-field', 'Particles follow sine curves leaving gradient trails.'],
|
||||
['data-stream', 'Rows of scrolling hex/binary text, cyberpunk feel.'],
|
||||
['gradient-blob', '4 drifting blurred radial gradients (additive blending).'],
|
||||
['sparkle-trail', 'Sparkles emit at your cursor (auto-wiggles if idle).'],
|
||||
['shockwave', 'Expanding rings emanating from center, looping.'],
|
||||
['typewriter-multi', 'Three lines typing concurrently with blinking block cursors.'],
|
||||
['counter-explosion', 'Number counts 0 → 2400, bursts particles, resets.']
|
||||
];
|
||||
|
||||
const CSS_ANIMS = [
|
||||
['fade-up','Translate from +32 px, fade.'],
|
||||
['fade-down','Translate from -32 px, fade.'],
|
||||
['fade-left','From left.'],
|
||||
['fade-right','From right.'],
|
||||
['rise-in','Rise + blur-off.'],
|
||||
['drop-in','Drop from above.'],
|
||||
['zoom-pop','Elastic scale pop.'],
|
||||
['blur-in','Blur clears.'],
|
||||
['glitch-in','Glitch jitter.'],
|
||||
['typewriter','Typewriter reveal.'],
|
||||
['neon-glow','Neon pulse.'],
|
||||
['shimmer-sweep','Sheen sweep.'],
|
||||
['gradient-flow','Gradient flow.'],
|
||||
['stagger-list','Staggered children.'],
|
||||
['counter-up','Number tick.'],
|
||||
['path-draw','SVG strokes draw.'],
|
||||
['parallax-tilt','3D hover tilt.'],
|
||||
['card-flip-3d','Y-axis flip.'],
|
||||
['cube-rotate-3d','Cube rotate.'],
|
||||
['page-turn-3d','Page turn.'],
|
||||
['perspective-zoom','Pull from -400 Z.'],
|
||||
['marquee-scroll','Infinite marquee.'],
|
||||
['kenburns','Ken Burns zoom.'],
|
||||
['confetti-burst','Pseudo confetti.'],
|
||||
['spotlight','Circular clip reveal.'],
|
||||
['morph-shape','SVG d morph.'],
|
||||
['ripple-reveal','Corner ripple.']
|
||||
];
|
||||
|
||||
const deck = document.querySelector('.deck');
|
||||
const total = FX.length + CSS_ANIMS.length;
|
||||
|
||||
// Build FX slides (1..20)
|
||||
FX.forEach((f, i) => {
|
||||
const idx = i + 1;
|
||||
const [name, desc] = f;
|
||||
const sec = document.createElement('section');
|
||||
sec.className = 'slide';
|
||||
sec.setAttribute('data-title', 'fx / ' + name);
|
||||
|
||||
const extraAttrs = name === 'letter-explode'
|
||||
? 'data-fx-text-value="' + name.toUpperCase() + '"'
|
||||
: name === 'counter-explosion'
|
||||
? 'data-fx-to="2400"'
|
||||
: name === 'typewriter-multi'
|
||||
? 'data-fx-line1="> initializing knowledge graph..." data-fx-line2="> loading 28 concept nodes" data-fx-line3="> agent ready. awaiting prompt_"'
|
||||
: '';
|
||||
|
||||
sec.innerHTML = `
|
||||
<span class="fx-indicator">${idx}/${total}</span>
|
||||
<p class="kicker">FX · canvas · ${String(idx).padStart(2,'0')}</p>
|
||||
<h1 class="h1"><span class="gradient-text">${name}</span></h1>
|
||||
<p class="lede">${desc}</p>
|
||||
<div class="fx-stage">
|
||||
<span class="fx-label">data-fx="${name}"</span>
|
||||
<button class="fx-replay" type="button">Replay</button>
|
||||
<div class="fx-host" style="position:absolute;inset:0;" data-fx="${name}" ${extraAttrs}></div>
|
||||
</div>
|
||||
<div class="deck-footer"><span class="dim2">Press → for next slide</span><span class="slide-number" data-current="${idx}" data-total="${total}"></span></div>
|
||||
`;
|
||||
deck.appendChild(sec);
|
||||
|
||||
// Wire Replay button
|
||||
sec.querySelector('.fx-replay').addEventListener('click', () => {
|
||||
const host = sec.querySelector('.fx-host');
|
||||
const name2 = host.getAttribute('data-fx');
|
||||
const attrs = {};
|
||||
for (const a of host.attributes) attrs[a.name] = a.value;
|
||||
const parent = host.parentNode;
|
||||
// stop existing
|
||||
if (window.__hpxActive && window.__hpxActive.has(host)){
|
||||
try{ window.__hpxActive.get(host).stop(); }catch(e){}
|
||||
window.__hpxActive.delete(host);
|
||||
}
|
||||
const fresh = document.createElement('div');
|
||||
for (const k in attrs) fresh.setAttribute(k, attrs[k]);
|
||||
fresh.style.cssText = host.style.cssText;
|
||||
parent.replaceChild(fresh, host);
|
||||
if (window.__hpxReinit) window.__hpxReinit(sec);
|
||||
});
|
||||
});
|
||||
|
||||
// Build CSS animation slides (legacy, kept for completeness)
|
||||
CSS_ANIMS.forEach((a, i) => {
|
||||
const idx = FX.length + i + 1;
|
||||
const sec = document.createElement('section');
|
||||
sec.className = 'slide';
|
||||
sec.setAttribute('data-title', a[0]);
|
||||
sec.innerHTML = `
|
||||
<span class="fx-indicator">${idx}/${total}</span>
|
||||
<p class="kicker">CSS anim · ${String(idx).padStart(2,'0')}</p>
|
||||
<h1 class="h1"><span class="gradient-text">${a[0]}</span></h1>
|
||||
<p class="lede">${a[1]}</p>
|
||||
<div class="anim-box anim-${a[0]}" data-anim="${a[0]}" data-anim-target>${a[0]}</div>
|
||||
<div class="deck-footer"><span class="dim2">Press A to cycle</span><span class="slide-number" data-current="${idx}" data-total="${total}"></span></div>
|
||||
`;
|
||||
deck.appendChild(sec);
|
||||
});
|
||||
</script>
|
||||
<script src="../assets/runtime.js"></script>
|
||||
<script src="../assets/animations/fx-runtime.js"></script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="minimal-white">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>html-ppt · 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/minimal-white.css">
|
||||
<link rel="stylesheet" href="../assets/animations/animations.css">
|
||||
</head>
|
||||
<body data-themes="minimal-white,editorial-serif,soft-pastel,arctic-cool,sunset-warm,catppuccin-mocha,tokyo-night,aurora,xiaohongshu-white,neo-brutalism" data-theme-base="../assets/themes/">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. Cover -->
|
||||
<section class="slide" data-title="Cover">
|
||||
<p class="kicker">html-ppt · 2026</p>
|
||||
<h1 class="h1 anim-fade-up" data-anim="fade-up">用模板,<span class="gradient-text">换主题</span><br>讲任何事情</h1>
|
||||
<p class="lede">24 themes · 30 layouts · 25 animations · zero build</p>
|
||||
<div class="deck-footer"><span class="dim2">lewis</span><span class="slide-number" data-current="1" data-total="6"></span></div>
|
||||
<div class="notes">这是一个最小可用的 deck。你可以复制这个文件作为新 deck 的起点。</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. TOC -->
|
||||
<section class="slide" data-title="目录">
|
||||
<p class="kicker">Agenda</p>
|
||||
<h2 class="h2">我们会讲三件事</h2>
|
||||
<div class="grid g3 mt-l anim-stagger-list" data-anim-target>
|
||||
<div class="card"><h4>01 · Tokens</h4><p class="dim">把颜色/字体/圆角收进 CSS 变量。</p></div>
|
||||
<div class="card"><h4>02 · Layouts</h4><p class="dim">30 种可复用单页。</p></div>
|
||||
<div class="card"><h4>03 · Runtime</h4><p class="dim">键盘驱动、按 T 换主题。</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Stat -->
|
||||
<section class="slide center tc" data-title="Stat">
|
||||
<div>
|
||||
<p class="kicker">Result</p>
|
||||
<div style="font-size:220px;font-weight:900;line-height:1"><span class="counter gradient-text" data-to="92">0</span><span class="gradient-text">%</span></div>
|
||||
<h3>的准备时间被省下</h3>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Two column -->
|
||||
<section class="slide" data-title="Tokens">
|
||||
<p class="kicker">Under the hood</p>
|
||||
<h2 class="h2">换主题 = 换一组变量</h2>
|
||||
<div class="grid g2 mt-l">
|
||||
<div class="card"><h4>语义变量</h4><p class="dim">写 <code>var(--surface)</code>,不写具体色值。</p></div>
|
||||
<div class="card"><h4>一键切换</h4><p class="dim">按 T 循环所有主题——所有 slide 同步更新。</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. CTA -->
|
||||
<section class="slide center tc" data-title="CTA">
|
||||
<div>
|
||||
<p class="kicker">Your turn</p>
|
||||
<h1 class="h1 anim-rise-in" data-anim="rise-in">开始做你的 deck</h1>
|
||||
<p class="lede" style="margin:16px auto">按 ← → 翻页 · T 切主题 · A 切动效 · F 全屏 · O 概览 · S 备注</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. Thanks -->
|
||||
<section class="slide center tc" data-title="Thanks">
|
||||
<h1 class="h1" style="font-size:160px;line-height:1"><span class="gradient-text">Thanks</span></h1>
|
||||
<p class="lede">lewis · sudolewis@gmail.com</p>
|
||||
</section>
|
||||
</div>
|
||||
<script src="../assets/runtime.js"></script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"><title>Full-Deck Gallery — html-ppt v2</title>
|
||||
<link rel="stylesheet" href="../assets/fonts.css">
|
||||
<link rel="stylesheet" href="../assets/base.css">
|
||||
<style>
|
||||
html,body{background:#0b0c10;color:#e8ebf4;font-family:var(--font-sans)}
|
||||
.deck{background:#0b0c10}
|
||||
.slide{padding:60px 80px;color:#e8ebf4;background:transparent;display:flex;flex-direction:column}
|
||||
.slide h1{color:#fff;font-size:48px;margin:0 0 6px;letter-spacing:-.02em}
|
||||
.slide .sub{color:#aab0c0;font-size:18px;margin:0 0 22px}
|
||||
.frame-wrap{flex:1;border-radius:14px;overflow:hidden;border:1px solid rgba(255,255,255,.12);
|
||||
box-shadow:0 30px 80px rgba(0,0,0,.5);position:relative;background:#fff}
|
||||
iframe.tpl{position:absolute;inset:0;width:200%;height:200%;border:0;
|
||||
transform:scale(.5);transform-origin:top left}
|
||||
.meta{position:absolute;top:24px;right:40px;font-family:'JetBrains Mono',monospace;
|
||||
font-size:12px;color:#6a7086;letter-spacing:.14em;text-transform:uppercase;z-index:30}
|
||||
.tag{display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(255,255,255,.08);
|
||||
color:#cfd3dc;font-size:11px;margin-right:6px}
|
||||
.cover{align-items:center;justify-content:center;text-align:center}
|
||||
.cover h1{font-size:84px;background:linear-gradient(135deg,#60a5fa,#a78bfa,#f472b6);
|
||||
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.cover p{color:#aab0c0;max-width:60ch;font-size:20px}
|
||||
.legend{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="deck">
|
||||
|
||||
<section class="slide cover">
|
||||
<p class="kicker" style="color:#a78bfa">HTML-PPT v2 · Full-Deck Gallery</p>
|
||||
<h1>14 full-deck templates</h1>
|
||||
<p>Press → to browse. Each slide is a live iframe preview of a complete, multi-slide deck template. Open any <code>templates/full-decks/<name>/index.html</code> to see the full deck, or copy the folder to scaffold your own.</p>
|
||||
<div class="legend">
|
||||
<span class="tag">8 extracted from real decks</span>
|
||||
<span class="tag">6 scenario scaffolds</span>
|
||||
<span class="tag">scoped .tpl-<name> CSS</span>
|
||||
<span class="tag">36 themes compatible</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Template preview slides generated via JS below -->
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const TPLS = [
|
||||
['xhs-white-editorial', '白底杂志风', 'extracted', 'xhs posts, editorial lifestyle'],
|
||||
['graphify-dark-graph', '暗底知识图谱', 'extracted', 'AI/graph/data products'],
|
||||
['knowledge-arch-blueprint', '奶油蓝图架构', 'extracted', 'architecture, systems thinking'],
|
||||
['hermes-cyber-terminal', '暗终端 cyber', 'extracted', 'devtool, honest-review, agent demos'],
|
||||
['obsidian-claude-gradient', 'GitHub 暗紫渐变', 'extracted', 'tool walkthroughs, LLM product'],
|
||||
['testing-safety-alert', '红琥珀警示', 'extracted', 'security, incident review, AI safety'],
|
||||
['xhs-pastel-card', '柔和马卡龙', 'extracted', 'lifestyle, soft emotional'],
|
||||
['dir-key-nav-minimal', '方向键 8 色极简', 'extracted', 'keynote, one-idea-per-slide'],
|
||||
['pitch-deck', 'Pitch Deck YC 风', 'scenario', 'fundraising, startup pitch'],
|
||||
['product-launch', 'Product Launch', 'scenario', 'product announcement, launch keynote'],
|
||||
['tech-sharing', 'Tech Sharing 技术分享','scenario','internal tech talk, conference talk'],
|
||||
['weekly-report', 'Weekly Report 周报','scenario', 'status update, business review'],
|
||||
['xhs-post', '小红书 图文 9 屏 3:4','scenario', 'xiaohongshu / ig carousel'],
|
||||
['course-module', 'Course Module 教学模块','scenario','online course, workshop module'],
|
||||
['presenter-mode-reveal', '🎤 Presenter Mode 演讲者模式','scenario','tech sharing, talk with 逐字稿, speaker view']
|
||||
];
|
||||
|
||||
const deck = document.querySelector('.deck');
|
||||
TPLS.forEach((t,i)=>{
|
||||
const s = document.createElement('section');
|
||||
s.className = 'slide';
|
||||
s.setAttribute('data-title',t[0]);
|
||||
s.innerHTML = `
|
||||
<span class="meta">${i+1}/${TPLS.length+1}</span>
|
||||
<h1>${t[0]}</h1>
|
||||
<p class="sub">${t[1]} · <span class="tag">${t[2]}</span> ${t[3]}</p>
|
||||
<div class="frame-wrap">
|
||||
<iframe class="tpl" src="full-decks/${t[0]}/index.html" loading="eager" title="${t[0]}"></iframe>
|
||||
</div>`;
|
||||
deck.appendChild(s);
|
||||
});
|
||||
</script>
|
||||
<script src="../assets/runtime.js"></script>
|
||||
</body></html>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
# course-module · 教学模块
|
||||
|
||||
7-slide teaching module: cover (title + meta), objectives, core concept, worked example, exercise, check-your-understanding (MCQ), summary.
|
||||
|
||||
Academic but friendly look: warm off-white paper, Playfair Display display type, a green/terracotta accent pair. A persistent **left sidebar** on content slides lists the module's learning objectives and checks them off as you progress — students always know where they are.
|
||||
|
||||
**Use when:** online course modules, lecture handouts, onboarding curricula, workshop units.
|
||||
**Feel:** a good textbook opened to a chapter — structured, quiet, encouraging.
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Module 04 · Recursion · CS101</title>
|
||||
<link rel="stylesheet" href="../../../assets/fonts.css">
|
||||
<link rel="stylesheet" href="../../../assets/base.css">
|
||||
<link rel="stylesheet" href="../../../assets/animations/animations.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="tpl-course-module">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. Cover -->
|
||||
<section class="slide full" data-title="Cover">
|
||||
<p class="kicker">CS 101 · MODULE 04</p>
|
||||
<h1 class="h1 mt-s">Recursion: solving<br>problems by <em>calling yourself</em>.</h1>
|
||||
<p class="lede mt-l" style="max-width:62ch">In this module you'll learn why a function that calls itself is not a trick, but the most natural way to describe problems that contain smaller copies of themselves.</p>
|
||||
<div class="row mt-l" style="gap:16px">
|
||||
<span class="pill-academic">~ 45 min read</span>
|
||||
<span class="pill-academic">prereq · functions, if/else</span>
|
||||
<span class="pill-academic">lang · Python</span>
|
||||
</div>
|
||||
<div class="deck-footer"><span>Dr. A. Rivera · Spring 2026</span><span class="slide-number" data-current="1" data-total="7"></span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Objectives -->
|
||||
<section class="slide" data-title="Objectives">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">CS 101 · M04</div>
|
||||
<h5>Learning objectives</h5>
|
||||
<ul class="obj-list">
|
||||
<li class="current">Define recursion</li>
|
||||
<li>Identify a base case</li>
|
||||
<li>Trace a recursive call</li>
|
||||
<li>Convert loop ↔ recursion</li>
|
||||
<li>Recognize when recursion helps</li>
|
||||
</ul>
|
||||
<h5>Module progress</h5>
|
||||
<p class="dim" style="font-size:13px">Page 2 of 7 · ~5 min in</p>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<p class="kicker">OBJECTIVES</p>
|
||||
<h2 class="h2 mt-s">By the end, you will be able to…</h2>
|
||||
<div class="stack mt-l">
|
||||
<div class="concept-box"><h4>① Explain recursion in one sentence.</h4><p class="dim">"A function that solves a problem by calling itself on a smaller version of that problem."</p></div>
|
||||
<div class="concept-box"><h4>② Write a base case that always terminates.</h4><p class="dim">Every recursive function must have an exit door, or it runs forever.</p></div>
|
||||
<div class="concept-box"><h4>③ Trace a call stack on paper.</h4><p class="dim">Given <code>fact(4)</code>, draw the stack frames top-to-bottom.</p></div>
|
||||
<div class="concept-box"><h4>④ Convert a while-loop to a recursive equivalent.</h4><p class="dim">And explain when one is clearer than the other.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Concept -->
|
||||
<section class="slide" data-title="Concept">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">CS 101 · M04</div>
|
||||
<h5>Learning objectives</h5>
|
||||
<ul class="obj-list">
|
||||
<li class="done">Define recursion</li>
|
||||
<li class="current">Identify a base case</li>
|
||||
<li>Trace a recursive call</li>
|
||||
<li>Convert loop ↔ recursion</li>
|
||||
<li>Recognize when recursion helps</li>
|
||||
</ul>
|
||||
<h5>Key terms</h5>
|
||||
<p class="dim" style="font-size:13px">base case · recursive case · call stack · tail call</p>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<p class="kicker">CORE CONCEPT</p>
|
||||
<h2 class="h2 mt-s">Two parts, always.</h2>
|
||||
<p class="lede mt-m">A recursive function has exactly two things inside it: a <b>base case</b> (when to stop) and a <b>recursive case</b> (how to shrink the problem before calling yourself).</p>
|
||||
<div class="callout">
|
||||
<b>Rule of thumb.</b> If you can't name the base case out loud, don't write the recursion yet. Draw it on paper first.
|
||||
</div>
|
||||
<div class="grid g2 mt-l">
|
||||
<div class="concept-box"><h4>Base case</h4><p class="dim">The smallest possible input — one the function answers directly, without calling itself.</p><p class="pill-academic">e.g. <b>n == 0</b></p></div>
|
||||
<div class="concept-box"><h4>Recursive case</h4><p class="dim">Every other input — delegate to a smaller version of the same problem.</p><p class="pill-academic">e.g. <b>n × fact(n-1)</b></p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Example -->
|
||||
<section class="slide" data-title="Example">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">CS 101 · M04</div>
|
||||
<h5>Learning objectives</h5>
|
||||
<ul class="obj-list">
|
||||
<li class="done">Define recursion</li>
|
||||
<li class="done">Identify a base case</li>
|
||||
<li class="current">Trace a recursive call</li>
|
||||
<li>Convert loop ↔ recursion</li>
|
||||
<li>Recognize when recursion helps</li>
|
||||
</ul>
|
||||
<h5>Try it yourself</h5>
|
||||
<p class="dim" style="font-size:13px">Open repl.it and run the code on the right. Then try <code>fact(10)</code>.</p>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<p class="kicker">WORKED EXAMPLE</p>
|
||||
<h2 class="h2 mt-s">Factorial, 7 lines.</h2>
|
||||
<div class="code mt-m"><pre style="margin:0"><span class="cmt"># fact(n) = n × (n-1) × … × 1, and fact(0) = 1</span>
|
||||
<span class="kw">def</span> fact(n):
|
||||
<span class="kw">if</span> n == <span class="str">0</span>: <span class="cmt"># base case</span>
|
||||
<span class="kw">return</span> <span class="str">1</span>
|
||||
<span class="kw">return</span> n * fact(n - <span class="str">1</span>) <span class="cmt"># recursive case</span>
|
||||
|
||||
<span class="kw">print</span>(fact(<span class="str">4</span>)) <span class="cmt"># → 24</span></pre></div>
|
||||
<div class="callout">
|
||||
<b>Trace fact(4).</b> 4 × fact(3) → 4 × (3 × fact(2)) → 4 × 3 × (2 × fact(1)) → 4 × 3 × 2 × 1 × fact(0) → 4 × 3 × 2 × 1 × 1 = <b>24</b>.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. Exercise -->
|
||||
<section class="slide" data-title="Exercise">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">CS 101 · M04</div>
|
||||
<h5>Learning objectives</h5>
|
||||
<ul class="obj-list">
|
||||
<li class="done">Define recursion</li>
|
||||
<li class="done">Identify a base case</li>
|
||||
<li class="done">Trace a recursive call</li>
|
||||
<li class="current">Convert loop ↔ recursion</li>
|
||||
<li>Recognize when recursion helps</li>
|
||||
</ul>
|
||||
<h5>Time</h5>
|
||||
<p class="dim" style="font-size:13px">~10 minutes · solo</p>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<p class="kicker">EXERCISE 4.1</p>
|
||||
<h2 class="h2 mt-s">Write <em>sum_to(n)</em>.</h2>
|
||||
<p class="lede mt-m">Return <code>1 + 2 + … + n</code> using recursion — no loops allowed.</p>
|
||||
<div class="exercise mt-l">
|
||||
<p style="margin:0;font-size:18px;color:var(--text-1)"><b>Your task</b></p>
|
||||
<ol style="color:var(--text-2);line-height:1.8;margin:10px 0 0">
|
||||
<li>Write the base case. What does <code>sum_to(0)</code> return?</li>
|
||||
<li>Write the recursive case in terms of <code>sum_to(n - 1)</code>.</li>
|
||||
<li>Test it: <code>sum_to(5) == 15</code>, <code>sum_to(10) == 55</code>.</li>
|
||||
<li>Bonus: what happens if you call <code>sum_to(-3)</code>? Fix it.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p class="dim mt-m" style="font-size:14px">Stuck? Remember: a base case is the smallest input you already know the answer to.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. Check understanding -->
|
||||
<section class="slide" data-title="Check">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">CS 101 · M04</div>
|
||||
<h5>Learning objectives</h5>
|
||||
<ul class="obj-list">
|
||||
<li class="done">Define recursion</li>
|
||||
<li class="done">Identify a base case</li>
|
||||
<li class="done">Trace a recursive call</li>
|
||||
<li class="done">Convert loop ↔ recursion</li>
|
||||
<li class="current">Recognize when recursion helps</li>
|
||||
</ul>
|
||||
<h5>Self-assess</h5>
|
||||
<p class="dim" style="font-size:13px">You should get 3/3.</p>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<p class="kicker">CHECK YOUR UNDERSTANDING</p>
|
||||
<h2 class="h2 mt-s">Which function will recurse forever?</h2>
|
||||
<div class="mt-l">
|
||||
<div class="mcq"><div class="letter">A</div><div><b>def f(n): return 1 if n == 0 else n * f(n - 1)</b><p class="dim" style="font-size:13px;margin:4px 0 0">Base case <code>n == 0</code>, shrinks toward it. Terminates.</p></div></div>
|
||||
<div class="mcq correct"><div class="letter">B</div><div><b>def f(n): return n + f(n + 1)</b><p class="dim" style="font-size:13px;margin:4px 0 0"><b style="color:var(--accent)">✓ Correct.</b> No base case, and <code>n</code> grows — infinite recursion.</p></div></div>
|
||||
<div class="mcq"><div class="letter">C</div><div><b>def f(n): return n if n < 2 else f(n - 1) + f(n - 2)</b><p class="dim" style="font-size:13px;margin:4px 0 0">Classic Fibonacci. Base case on <code>n < 2</code>. Terminates.</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 7. Summary -->
|
||||
<section class="slide full" data-title="Summary">
|
||||
<p class="kicker">SUMMARY · MODULE 04</p>
|
||||
<h1 class="h1 mt-s">You can now…</h1>
|
||||
<div class="grid g2 mt-l">
|
||||
<div class="concept-box"><h4>✓ Define recursion</h4><p class="dim">A function that calls itself on a smaller input.</p></div>
|
||||
<div class="concept-box"><h4>✓ Write a safe base case</h4><p class="dim">Every recursion needs an exit door.</p></div>
|
||||
<div class="concept-box"><h4>✓ Trace a call stack</h4><p class="dim">You can unwind <code>fact(4)</code> by hand.</p></div>
|
||||
<div class="concept-box"><h4>✓ Judge when to use it</h4><p class="dim">Trees and self-similar problems → recursion. Flat iteration → loop.</p></div>
|
||||
</div>
|
||||
<div class="callout mt-l">
|
||||
<b>Up next · Module 05.</b> Divide & conquer: merge sort. We'll use everything you just learned — but on lists, not numbers.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<script src="../../../assets/runtime.js"></script>
|
||||
</body></html>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/* course-module — academic but friendly */
|
||||
.tpl-course-module{
|
||||
--bg:#fbfaf6;--bg-soft:#f4f1e8;--surface:#ffffff;--surface-2:#f6f3ea;
|
||||
--border:rgba(60,45,20,.12);--border-strong:rgba(60,45,20,.24);
|
||||
--text-1:#2a2418;--text-2:#5a5140;--text-3:#8a7f68;
|
||||
--accent:#2d7d6e;--accent-2:#d88a3a;--accent-3:#c4593f;
|
||||
--grad:linear-gradient(135deg,#2d7d6e,#4ea893);
|
||||
--radius:14px;--radius-lg:20px;
|
||||
--shadow:0 12px 30px rgba(60,45,20,.07);
|
||||
font-family:'Inter','Noto Sans SC',sans-serif;
|
||||
}
|
||||
.tpl-course-module .slide{padding:64px 80px;background:var(--bg);display:grid;grid-template-columns:260px 1fr;gap:56px;align-content:start}
|
||||
.tpl-course-module .slide.full{grid-template-columns:1fr;display:flex;flex-direction:column;justify-content:center}
|
||||
.tpl-course-module .sidebar{border-right:1px solid var(--border);padding-right:32px;position:relative}
|
||||
.tpl-course-module .sidebar .brand{font-family:'Playfair Display',serif;font-size:22px;font-weight:700;color:var(--accent)}
|
||||
.tpl-course-module .sidebar .brand::before{content:"✦ ";color:var(--accent-2)}
|
||||
.tpl-course-module .sidebar h5{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:var(--text-3);margin:32px 0 12px}
|
||||
.tpl-course-module .obj-list{list-style:none;padding:0;margin:0;font-size:13px;color:var(--text-2);line-height:1.5}
|
||||
.tpl-course-module .obj-list li{padding:8px 0 8px 22px;position:relative;border-bottom:1px dashed var(--border)}
|
||||
.tpl-course-module .obj-list li::before{content:"○";position:absolute;left:0;top:8px;color:var(--accent)}
|
||||
.tpl-course-module .obj-list li.done::before{content:"●";color:var(--accent)}
|
||||
.tpl-course-module .obj-list li.current{color:var(--text-1);font-weight:700}
|
||||
.tpl-course-module .obj-list li.current::before{content:"▸";color:var(--accent-2)}
|
||||
.tpl-course-module .main{min-width:0}
|
||||
.tpl-course-module .h1{font-family:'Playfair Display',serif;font-size:72px;line-height:1.02;font-weight:800;letter-spacing:-.02em;color:var(--text-1)}
|
||||
.tpl-course-module .h2{font-family:'Playfair Display',serif;font-size:48px;line-height:1.1;font-weight:700;letter-spacing:-.015em;color:var(--text-1)}
|
||||
.tpl-course-module h3,.tpl-course-module h4{color:var(--text-1)}
|
||||
.tpl-course-module .kicker{color:var(--accent-2);font-size:12px;font-weight:700;letter-spacing:.14em}
|
||||
.tpl-course-module .lede{font-size:20px;color:var(--text-2);line-height:1.7}
|
||||
.tpl-course-module .callout{border-left:4px solid var(--accent-2);background:var(--surface-2);padding:20px 24px;border-radius:0 var(--radius) var(--radius) 0;margin-top:24px}
|
||||
.tpl-course-module .callout b{color:var(--accent-2)}
|
||||
.tpl-course-module .concept-box{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px 26px;box-shadow:var(--shadow)}
|
||||
.tpl-course-module .concept-box h4{margin-top:0;color:var(--accent)}
|
||||
.tpl-course-module .exercise{background:#fff8ed;border:1.5px dashed var(--accent-2);border-radius:var(--radius);padding:24px 28px}
|
||||
.tpl-course-module .exercise::before{content:"✎ Exercise";display:block;font-size:12px;font-weight:700;letter-spacing:.12em;color:var(--accent-2);margin-bottom:10px;text-transform:uppercase}
|
||||
.tpl-course-module .code{background:#2a2418;color:#f4f1e8;border-radius:var(--radius);padding:20px 24px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.7;overflow:auto}
|
||||
.tpl-course-module .code .cmt{color:#8a7f68;font-style:italic}
|
||||
.tpl-course-module .code .kw{color:#e8a770}
|
||||
.tpl-course-module .code .str{color:#8ec6b2}
|
||||
.tpl-course-module .mcq{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 22px;margin-bottom:10px;display:flex;gap:14px;align-items:flex-start;cursor:pointer}
|
||||
.tpl-course-module .mcq .letter{flex:none;width:28px;height:28px;border-radius:50%;border:2px solid var(--text-3);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;color:var(--text-2)}
|
||||
.tpl-course-module .mcq.correct{border-color:var(--accent);background:rgba(45,125,110,.06)}
|
||||
.tpl-course-module .mcq.correct .letter{border-color:var(--accent);background:var(--accent);color:#fff}
|
||||
.tpl-course-module .pill-academic{display:inline-block;padding:4px 12px;border-radius:4px;background:var(--surface-2);border:1px solid var(--border);font-size:12px;color:var(--text-2);font-family:'JetBrains Mono',monospace}
|
||||
.tpl-course-module .slide.full .h1{font-size:88px}
|
||||
.tpl-course-module .deck-footer{color:var(--text-3)}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# dir-key-nav-minimal
|
||||
|
||||
8 张幻灯片,每张一个纯色/渐变 mono-background(indigo / cream / crimson / emerald / slate / violet / white / charcoal)。灵感直接来自 `20260405 演示幻灯片【方向键版】.html` —— 八个 `t-*` 主题类,每张幻灯一个背景,方向键切换,极简 editorial 气质。
|
||||
|
||||
**Visual traits:** 每张独立背景色 + 单一 accent、巨大 160px 标题无副图、4px 短粗 accent line divider、arrow-prefixed mono list、左下 `← →` 键盘提示 + 右下 page label、全屏 breathing negative space、JetBrains Mono 做数字 / 代码 / 键盘 hint、每个背景有自己的 `.dk-accent` 色。
|
||||
|
||||
**Use when:** 有话要说、没太多图、希望用排版节奏推进观众注意力;keynote 式的极简讲稿;每张幻灯只讲一件事;公开分享 / keynote / 演讲稿。
|
||||
|
||||
**Source inspiration:** `20260405-Karpathy-知识库/20260405 演示幻灯片【方向键版】.html`.
|
||||
|
||||
**Path:** `templates/full-decks/dir-key-nav-minimal/index.html`
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dir-Key Nav Minimal</title>
|
||||
<link rel="stylesheet" href="../../../assets/fonts.css">
|
||||
<link rel="stylesheet" href="../../../assets/base.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="tpl-dir-key-nav-minimal">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER · indigo -->
|
||||
<section class="slide t-indigo is-active">
|
||||
<div class="dk-snum">01 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">Karpathy LLM Wiki</div>
|
||||
<h1 class="dk-h0">为什么笔记<br>治不了 <span class="dk-accent">LLM</span></h1>
|
||||
<span class="dk-line"></span>
|
||||
<p class="dk-lede">8 种背景、8 张幻灯,一个关于如何把 AI 变成「长期记忆外挂」的最短陈述。<strong>按 → 继续。</strong></p>
|
||||
</div>
|
||||
<div class="dk-keyhint">nav · <kbd>←</kbd> <kbd>→</kbd> · <kbd>space</kbd></div>
|
||||
<div class="dk-page">cover</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION · cream -->
|
||||
<section class="slide t-cream">
|
||||
<div class="dk-snum">02 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">Chapter 01</div>
|
||||
<h1 class="dk-h0">The <span class="dk-accent">Problem</span>.</h1>
|
||||
<span class="dk-line"></span>
|
||||
<p class="dk-lede">Token 上限是一个物理事实。你每次和 LLM 说话,它都是一个失忆症患者。</p>
|
||||
</div>
|
||||
<div class="dk-keyhint">chapter · 01 / 04</div>
|
||||
<div class="dk-page">section</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT · crimson -->
|
||||
<section class="slide t-crimson">
|
||||
<div class="dk-snum">03 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">Symptoms</div>
|
||||
<h2 class="dk-h1">四种你已经<br>受够的<br><span class="dk-accent">遗忘</span>。</h2>
|
||||
<ul class="dk-list">
|
||||
<li>昨天聊过的项目,今天重新解释一遍</li>
|
||||
<li>上下文窗口一到,它开始「编造记忆」</li>
|
||||
<li>不同 session 之间毫无关联,就像第一次见</li>
|
||||
<li>你的真正偏好从未被记住,每次都要 re-prompt</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dk-keyhint">content · list</div>
|
||||
<div class="dk-page">03</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. CONTENT · emerald -->
|
||||
<section class="slide t-emerald">
|
||||
<div class="dk-snum">04 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">The Fix</div>
|
||||
<h2 class="dk-h1">答案不是<br><span class="dk-accent">更大</span> 的窗口。</h2>
|
||||
<p class="dk-lede" style="margin-top:10px">而是:把你的知识、偏好、历史都<strong>写进文件系统</strong>。<br>让 LLM 每次对话前,先去读那个系统。</p>
|
||||
<div class="dk-grid-2">
|
||||
<div class="dk-col"><h3>× 窗口 stuffing</h3><p>把所有东西塞 prompt,贵、慢、最终溢出。</p></div>
|
||||
<div class="dk-col"><h3>✓ 文件 + 检索</h3><p>按需加载,永远不溢出,结构化可 diff。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dk-keyhint">content · compare</div>
|
||||
<div class="dk-page">04</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. CODE · slate -->
|
||||
<section class="slide t-slate">
|
||||
<div class="dk-snum">05 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">Minimal Setup</div>
|
||||
<h2 class="dk-h2"><span class="dk-accent">4 行</span> YAML<br>就能开始。</h2>
|
||||
<pre class="dk-code">memory:
|
||||
root: ~/.llm-wiki
|
||||
format: markdown
|
||||
retrieval: hybrid # embedding + bm25</pre>
|
||||
<p class="dk-lede" style="margin-top:16px;font-size:20px">你现在拥有一个会随时间增长的 <strong>第二大脑</strong>。每次对话它都会被读、被更新。</p>
|
||||
</div>
|
||||
<div class="dk-keyhint">content · code</div>
|
||||
<div class="dk-page">05</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. CHART · violet — big number with bar -->
|
||||
<section class="slide t-violet">
|
||||
<div class="dk-snum">06 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">30-day result</div>
|
||||
<div class="dk-big dk-accent">87%</div>
|
||||
<p class="dk-lede" style="margin-top:14px;font-size:26px">的 re-explain 被消除。平均每次对话节省 <strong>4.2 分钟</strong> 的 re-context。</p>
|
||||
<svg viewBox="0 0 900 80" style="width:100%;max-width:900px;margin-top:30px">
|
||||
<rect x="0" y="30" width="900" height="22" rx="11" fill="rgba(255,255,255,.12)"/>
|
||||
<rect x="0" y="30" width="783" height="22" rx="11" fill="#c4b5fd"/>
|
||||
<text x="792" y="47" font-family="JetBrains Mono" font-size="16" fill="#c4b5fd" font-weight="700">87%</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dk-keyhint">chart · big-num</div>
|
||||
<div class="dk-page">06</div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA · white -->
|
||||
<section class="slide t-white">
|
||||
<div class="dk-snum">07 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">Start tonight</div>
|
||||
<h2 class="dk-h1">开始<br>你的 <span class="dk-accent">wiki</span>。</h2>
|
||||
<span class="dk-line"></span>
|
||||
<p class="dk-lede">不是装又一个插件。是决定:从今晚起,<strong>你的所有 AI 对话都要有一个共同的 vault</strong>。</p>
|
||||
<pre class="dk-code" style="font-size:18px">$ mkdir ~/llm-wiki && cd ~/llm-wiki
|
||||
$ git init
|
||||
$ echo "# my brain" > README.md</pre>
|
||||
</div>
|
||||
<div class="dk-keyhint">cta · three-commands</div>
|
||||
<div class="dk-page">07</div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS · charcoal -->
|
||||
<section class="slide t-charcoal">
|
||||
<div class="dk-snum">08 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">End · thanks for staying</div>
|
||||
<h1 class="dk-h0"><span class="dk-accent">謝謝</span>。</h1>
|
||||
<span class="dk-line"></span>
|
||||
<p class="dk-lede">Karpathy 的原始 thread + 我的 vault 结构都在 <strong>github.com/lewis/llm-wiki</strong>。欢迎按 ← 再看一遍。</p>
|
||||
</div>
|
||||
<div class="dk-keyhint">press <kbd>←</kbd> to rewind · <kbd>F</kbd> for fullscreen</div>
|
||||
<div class="dk-page">fin</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<script src="../../../assets/runtime.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/* dir-key-nav-minimal — 方向键极简 · 8 种 mono-background 切换 */
|
||||
.tpl-dir-key-nav-minimal{
|
||||
--dk-font:'Inter','Noto Sans SC','PingFang SC',-apple-system,sans-serif;
|
||||
--dk-mono:'JetBrains Mono',monospace;
|
||||
background:#000;
|
||||
color:#fff;
|
||||
font-family:var(--dk-font);
|
||||
}
|
||||
.tpl-dir-key-nav-minimal .slide{padding:80px 104px;overflow:hidden;position:absolute;inset:0}
|
||||
/* 8 background themes */
|
||||
.tpl-dir-key-nav-minimal .t-indigo{background:linear-gradient(135deg,#0f172a 0%,#1e1b4b 100%);color:#fff}
|
||||
.tpl-dir-key-nav-minimal .t-cream{background:#F5F0E8;color:#1a1a1a}
|
||||
.tpl-dir-key-nav-minimal .t-crimson{background:linear-gradient(135deg,#7f1d1d 0%,#991b1b 100%);color:#fff}
|
||||
.tpl-dir-key-nav-minimal .t-emerald{background:linear-gradient(135deg,#052e16 0%,#064e3b 100%);color:#ecfdf5}
|
||||
.tpl-dir-key-nav-minimal .t-slate{background:linear-gradient(135deg,#0f1923 0%,#1a2942 100%);color:#e6edf3}
|
||||
.tpl-dir-key-nav-minimal .t-violet{background:linear-gradient(135deg,#1e0a2e 0%,#2e1065 100%);color:#f5f3ff}
|
||||
.tpl-dir-key-nav-minimal .t-white{background:#ffffff;color:#111216}
|
||||
.tpl-dir-key-nav-minimal .t-charcoal{background:linear-gradient(135deg,#111827 0%,#1f2937 100%);color:#f3f4f6}
|
||||
|
||||
.tpl-dir-key-nav-minimal .dk-snum{position:absolute;top:30px;right:48px;font-size:11px;font-weight:700;letter-spacing:3px;text-transform:uppercase;font-family:var(--dk-mono)}
|
||||
.tpl-dir-key-nav-minimal .t-cream .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-white .dk-snum{color:#999}
|
||||
.tpl-dir-key-nav-minimal .t-indigo .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-crimson .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-emerald .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-slate .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-violet .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-charcoal .dk-snum{color:rgba(255,255,255,.38)}
|
||||
|
||||
.tpl-dir-key-nav-minimal .dk-eyebrow{font-size:12px;font-weight:700;letter-spacing:3.5px;text-transform:uppercase;opacity:.55;margin-bottom:22px;display:flex;align-items:center;gap:14px}
|
||||
.tpl-dir-key-nav-minimal .dk-eyebrow::after{content:'';flex:1;max-width:120px;height:1px;background:currentColor;opacity:.3}
|
||||
.tpl-dir-key-nav-minimal .dk-h0{font-size:160px;font-weight:900;line-height:.9;letter-spacing:-5px;margin:0 0 20px}
|
||||
.tpl-dir-key-nav-minimal .dk-h1{font-size:100px;font-weight:900;line-height:.98;letter-spacing:-3px;margin:0 0 18px}
|
||||
.tpl-dir-key-nav-minimal .dk-h2{font-size:72px;font-weight:800;line-height:1.05;letter-spacing:-2px;margin:0 0 16px}
|
||||
.tpl-dir-key-nav-minimal .dk-lede{font-size:26px;line-height:1.45;opacity:.72;max-width:900px;font-weight:300}
|
||||
.tpl-dir-key-nav-minimal .dk-lede strong{font-weight:700;opacity:1}
|
||||
.tpl-dir-key-nav-minimal .dk-big{font-family:var(--dk-mono);font-size:240px;font-weight:800;line-height:.9;letter-spacing:-10px}
|
||||
|
||||
.tpl-dir-key-nav-minimal .dk-line{display:block;width:90px;height:4px;background:currentColor;margin:30px 0;opacity:.85}
|
||||
.tpl-dir-key-nav-minimal .t-indigo .dk-accent{color:#a5b4fc}
|
||||
.tpl-dir-key-nav-minimal .t-cream .dk-accent{color:#B5392A}
|
||||
.tpl-dir-key-nav-minimal .t-crimson .dk-accent{color:#fecaca}
|
||||
.tpl-dir-key-nav-minimal .t-emerald .dk-accent{color:#6ee7b7}
|
||||
.tpl-dir-key-nav-minimal .t-slate .dk-accent{color:#7dd3fc}
|
||||
.tpl-dir-key-nav-minimal .t-violet .dk-accent{color:#c4b5fd}
|
||||
.tpl-dir-key-nav-minimal .t-white .dk-accent{color:#6366f1}
|
||||
.tpl-dir-key-nav-minimal .t-charcoal .dk-accent{color:#fbbf24}
|
||||
|
||||
.tpl-dir-key-nav-minimal .dk-list{list-style:none;padding:0;margin:28px 0 0;font-family:var(--dk-mono);font-size:22px;line-height:2}
|
||||
.tpl-dir-key-nav-minimal .dk-list li{padding-left:30px;position:relative;font-weight:400;opacity:.85}
|
||||
.tpl-dir-key-nav-minimal .dk-list li::before{content:'→';position:absolute;left:0;opacity:.5}
|
||||
.tpl-dir-key-nav-minimal .dk-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:56px;margin-top:36px}
|
||||
.tpl-dir-key-nav-minimal .dk-col h3{font-size:28px;font-weight:700;margin-bottom:10px}
|
||||
.tpl-dir-key-nav-minimal .dk-col p{font-size:19px;line-height:1.55;opacity:.72;font-weight:300}
|
||||
.tpl-dir-key-nav-minimal .dk-code{font-family:var(--dk-mono);font-size:16px;line-height:1.9;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:10px;padding:24px 28px;margin-top:24px;white-space:pre}
|
||||
.tpl-dir-key-nav-minimal .t-cream .dk-code,
|
||||
.tpl-dir-key-nav-minimal .t-white .dk-code{background:rgba(0,0,0,.05);border-color:rgba(0,0,0,.1)}
|
||||
.tpl-dir-key-nav-minimal .dk-keyhint{position:absolute;bottom:34px;left:104px;font-family:var(--dk-mono);font-size:12px;letter-spacing:2px;text-transform:uppercase;opacity:.45}
|
||||
.tpl-dir-key-nav-minimal .dk-keyhint kbd{display:inline-block;padding:2px 10px;margin:0 3px;border:1px solid currentColor;border-radius:4px;font-size:12px}
|
||||
.tpl-dir-key-nav-minimal .dk-page{position:absolute;bottom:34px;right:48px;font-family:var(--dk-mono);font-size:12px;letter-spacing:2px;opacity:.45}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# graphify-dark-graph
|
||||
|
||||
Deep-night 暗底 + 力导向知识图谱覆盖层 + 温暖玻璃拟态卡片。灵感来自 `20260413-graphify/ppt/graphify.html` 的 `#06060c` 渐变底、飘移 orb 光晕、glass 卡片(warm/blue/green/purple 五变体)和 rainbow-text 标题。
|
||||
|
||||
**Visual traits:** `#06060c → #0e1020` 斜向渐变、三颗 400-520px blur orb 慢飘动、cover SVG 力导向图谱作为背景、rainbow shift 渐变标题、JetBrains Mono 的 `.cmd-glow` 命令行、玻璃拟态卡片带顶部高光和微妙内阴影、温暖色系 accent (#e8a87c 琥珀 / #7ed3a4 薄荷 / #7eb8da 雾蓝 / #b8a4d6 丁香).
|
||||
|
||||
**Use when:** 介绍一个开发者工具、命令行产品、知识图谱 / 数据可视化相关项目;你希望现场演示时视觉有「AI native + 科技感 + 温度」。
|
||||
|
||||
**Source inspiration:** `20260413-graphify/ppt/graphify.html`.
|
||||
|
||||
**Path:** `templates/full-decks/graphify-dark-graph/index.html`
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Graphify Dark Graph</title>
|
||||
<link rel="stylesheet" href="../../../assets/fonts.css">
|
||||
<link rel="stylesheet" href="../../../assets/base.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="tpl-graphify-dark-graph">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER -->
|
||||
<section class="slide is-active">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-1"></div><div class="gd-orb gd-orb-2"></div><div class="gd-orb gd-orb-3"></div></div>
|
||||
<!-- live force-directed graph bg -->
|
||||
<svg viewBox="0 0 1600 900" style="position:absolute;inset:0;width:100%;height:100%;opacity:.38;z-index:1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke="#7eb8da" stroke-width="1" stroke-opacity=".5" fill="none">
|
||||
<line x1="300" y1="200" x2="520" y2="340"/>
|
||||
<line x1="520" y1="340" x2="780" y2="260"/>
|
||||
<line x1="780" y1="260" x2="1040" y2="420"/>
|
||||
<line x1="520" y1="340" x2="640" y2="560"/>
|
||||
<line x1="640" y1="560" x2="900" y2="620"/>
|
||||
<line x1="900" y1="620" x2="1040" y2="420"/>
|
||||
<line x1="1040" y1="420" x2="1260" y2="300"/>
|
||||
<line x1="1260" y1="300" x2="1380" y2="500"/>
|
||||
<line x1="900" y1="620" x2="1120" y2="720"/>
|
||||
<line x1="300" y1="200" x2="200" y2="420"/>
|
||||
<line x1="200" y1="420" x2="360" y2="640"/>
|
||||
<line x1="360" y1="640" x2="640" y2="560"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle cx="300" cy="200" r="10" fill="#e8a87c"/>
|
||||
<circle cx="520" cy="340" r="14" fill="#7eb8da"/>
|
||||
<circle cx="780" cy="260" r="9" fill="#7ed3a4"/>
|
||||
<circle cx="1040" cy="420" r="18" fill="#b8a4d6"/>
|
||||
<circle cx="640" cy="560" r="11" fill="#d4a0b9"/>
|
||||
<circle cx="900" cy="620" r="12" fill="#e8a87c"/>
|
||||
<circle cx="1260" cy="300" r="8" fill="#7ed3a4"/>
|
||||
<circle cx="1380" cy="500" r="10" fill="#7eb8da"/>
|
||||
<circle cx="1120" cy="720" r="9" fill="#d4a0b9"/>
|
||||
<circle cx="200" cy="420" r="8" fill="#b8a4d6"/>
|
||||
<circle cx="360" cy="640" r="11" fill="#7eb8da"/>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="gd-snum">01 / 08</div>
|
||||
<div style="margin-top:auto">
|
||||
<p class="gd-eyebrow">Tech Sharing · 纯干货</p>
|
||||
<h1 class="gd-h1" style="font-size:88px"><span class="gd-rainbow">手把手用 Graphify<br>搭建个人知识图谱</span></h1>
|
||||
<p class="gd-lede" style="margin-top:20px">一行命令 · 全多模态 · 诚实审计 —— <span class="gd-accent">把任何文件夹变成可导航的知识网络。</span></p>
|
||||
<p class="gd-eyebrow" style="margin-top:26px">↑ 背景就是 Graphify 真实跑出来的知识图谱</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION DIVIDER -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-1"></div><div class="gd-orb gd-orb-2"></div></div>
|
||||
<div class="gd-snum">02 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="gd-eyebrow">Part 01</div>
|
||||
<h1 class="gd-h1" style="font-size:120px">Why <span class="gd-grad">Graph</span>?</h1>
|
||||
<p class="gd-lede">folder → tree → graph,人类认知的下一步</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT — plugin grid -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-2"></div><div class="gd-orb gd-orb-3"></div></div>
|
||||
<div class="gd-snum">03 / 08</div>
|
||||
<p class="gd-eyebrow">Feature Map</p>
|
||||
<h2 class="gd-h2">一个工具,<span class="gd-grad">四件事</span></h2>
|
||||
<div class="gd-grid-4">
|
||||
<div class="gd-glass gd-glass-warm"><div style="font-size:30px">📂</div><h4 style="margin:10px 0 6px">Folder Ingest</h4><p class="gd-dim" style="font-size:13px;line-height:1.55">递归扫描任意路径,支持 md / pdf / 代码 / 图片</p></div>
|
||||
<div class="gd-glass gd-glass-blue"><div style="font-size:30px">🧠</div><h4 style="margin:10px 0 6px">Entity Extract</h4><p class="gd-dim" style="font-size:13px;line-height:1.55">用 LLM 抽概念、人物、事件、关系</p></div>
|
||||
<div class="gd-glass gd-glass-green"><div style="font-size:30px">🕸️</div><h4 style="margin:10px 0 6px">Force Graph</h4><p class="gd-dim" style="font-size:13px;line-height:1.55">D3 力导向,点击即跳转原文</p></div>
|
||||
<div class="gd-glass"><div style="font-size:30px">🔍</div><h4 style="margin:10px 0 6px">Audit Trail</h4><p class="gd-dim" style="font-size:13px;line-height:1.55">每条边都能追溯到 source span</p></div>
|
||||
</div>
|
||||
<div class="gd-glass gd-glass-warm" style="margin-top:24px"><p style="font-size:18px;line-height:1.6">它不是「又一个 RAG」—— 它是 <span class="gd-accent">把检索结果画出来,让你一眼就知道信息长什么样</span>。</p></div>
|
||||
</section>
|
||||
|
||||
<!-- 4. CODE -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-1"></div></div>
|
||||
<div class="gd-snum">04 / 08</div>
|
||||
<p class="gd-eyebrow">One command</p>
|
||||
<h2 class="gd-h2">从 0 到图谱,<span class="gd-grad">大概 90 秒</span></h2>
|
||||
<p class="gd-cmd" style="margin:16px 0 22px">$ graphify ~/notes --out ./graph</p>
|
||||
<pre class="gd-codebox"><span class="cm"># graphify.config.yaml</span>
|
||||
<span class="kw">ingest</span>:
|
||||
paths: [<span class="st">~/notes</span>, <span class="st">~/code/docs</span>]
|
||||
include: [<span class="st">"*.md"</span>, <span class="st">"*.pdf"</span>, <span class="st">"*.py"</span>]
|
||||
|
||||
<span class="kw">extract</span>:
|
||||
model: <span class="st">claude-opus-4-6</span>
|
||||
schema: [<span class="st">concept</span>, <span class="st">person</span>, <span class="st">event</span>, <span class="st">relation</span>]
|
||||
|
||||
<span class="kw">render</span>:
|
||||
engine: <span class="st">d3-force</span>
|
||||
audit: <span class="fn">true</span> <span class="cm"># 每条边带 source span</span></pre>
|
||||
</section>
|
||||
|
||||
<!-- 5. CHART — race diagram -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-3"></div></div>
|
||||
<div class="gd-snum">05 / 08</div>
|
||||
<p class="gd-eyebrow">Efficiency Race</p>
|
||||
<h2 class="gd-h2">没有知识库 vs 有知识库</h2>
|
||||
<div style="max-width:900px;margin-top:30px">
|
||||
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
|
||||
<div style="width:110px;text-align:right;font-weight:700;color:var(--gd-danger)">没有<br>知识库</div>
|
||||
<div style="flex:1;position:relative;height:70px;background:rgba(224,112,112,.06);border:1px solid rgba(224,112,112,.2);border-radius:16px">
|
||||
<div style="position:absolute;left:16px;top:50%;transform:translateY(-50%);font-size:32px">🛵</div>
|
||||
<div style="position:absolute;left:72px;top:50%;transform:translateY(-50%);color:var(--gd-danger);font-size:14px">反复喂信息…整理…又忘了…</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:16px">
|
||||
<div style="width:110px;text-align:right;font-weight:700;color:var(--gd-green)">有<br>知识库</div>
|
||||
<div style="flex:1;position:relative;height:70px;background:rgba(126,211,164,.06);border:1px solid rgba(126,211,164,.25);border-radius:16px">
|
||||
<div style="position:absolute;right:16px;top:50%;transform:translateY(-50%);font-size:32px">🏎️</div>
|
||||
<div style="position:absolute;right:72px;top:50%;transform:translateY(-50%);color:var(--gd-green);font-size:14px">AI 自己找 → 确认 → 干活!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gd-grid-3" style="margin-top:36px">
|
||||
<div class="gd-glass gd-glass-warm"><div class="gd-big gd-grad">5×</div><p class="gd-dim" style="margin-top:6px">速度提升</p></div>
|
||||
<div class="gd-glass gd-glass-green"><div class="gd-big gd-grad">-80%</div><p class="gd-dim" style="margin-top:6px">重复喂信息</p></div>
|
||||
<div class="gd-glass gd-glass-blue"><div class="gd-big gd-grad">∞</div><p class="gd-dim" style="margin-top:6px">记忆持久化</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. PIPELINE -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-2"></div></div>
|
||||
<div class="gd-snum">06 / 08</div>
|
||||
<p class="gd-eyebrow">Pipeline</p>
|
||||
<h2 class="gd-h2">端到端 <span class="gd-grad">4 步走</span></h2>
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-top:36px">
|
||||
<div class="gd-glass" style="flex:1;text-align:center"><div style="font-size:34px">📂</div><div style="font-weight:600;margin-top:8px">Scan</div><div class="gd-dim" style="font-size:13px">递归读文件</div></div>
|
||||
<div style="color:var(--gd-text3);font-size:24px">→</div>
|
||||
<div class="gd-glass gd-glass-blue" style="flex:1;text-align:center"><div style="font-size:34px">🔬</div><div style="font-weight:600;margin-top:8px">Extract</div><div class="gd-dim" style="font-size:13px">LLM 抽实体</div></div>
|
||||
<div style="color:var(--gd-text3);font-size:24px">→</div>
|
||||
<div class="gd-glass gd-glass-green" style="flex:1;text-align:center"><div style="font-size:34px">🕸️</div><div style="font-weight:600;margin-top:8px">Build</div><div class="gd-dim" style="font-size:13px">构图 + 去重</div></div>
|
||||
<div style="color:var(--gd-text3);font-size:24px">→</div>
|
||||
<div class="gd-glass gd-glass-warm" style="flex:1;text-align:center"><div style="font-size:34px">🎨</div><div style="font-weight:600;margin-top:8px">Render</div><div class="gd-dim" style="font-size:13px">D3 交互图</div></div>
|
||||
</div>
|
||||
<div class="gd-glass" style="margin-top:32px"><p style="font-size:16px;line-height:1.6;color:var(--gd-text2)">每一步都有 audit log:你永远知道某个节点为什么存在、它来自哪个文件的哪一行。</p></div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-1"></div><div class="gd-orb gd-orb-3"></div></div>
|
||||
<div class="gd-snum">07 / 08</div>
|
||||
<p class="gd-eyebrow">Try it tonight</p>
|
||||
<h2 class="gd-h1" style="font-size:80px">Graphify <span class="gd-grad">your folders</span></h2>
|
||||
<p class="gd-cmd" style="margin-top:22px">$ npm i -g @lewis/graphify</p>
|
||||
<p class="gd-cmd" style="margin-top:10px;color:var(--gd-warm);text-shadow:0 0 30px rgba(232,168,124,.45)">$ graphify ~/obsidian-vault</p>
|
||||
<div style="margin-top:32px">
|
||||
<span class="gd-tag">#knowledge-graph</span>
|
||||
<span class="gd-tag">#open-source</span>
|
||||
<span class="gd-tag">#claude-agent</span>
|
||||
<span class="gd-tag">#obsidian</span>
|
||||
<span class="gd-tag">#d3-force</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-2"></div></div>
|
||||
<div class="gd-snum">08 / 08</div>
|
||||
<div style="margin:auto 0;text-align:center">
|
||||
<div class="gd-big gd-rainbow" style="font-size:180px">Thanks.</div>
|
||||
<p class="gd-lede" style="margin:28px auto 0">github.com/lewis/graphify · 欢迎 star / issue / PR</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<script src="../../../assets/runtime.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/* graphify-dark-graph — 暗底玻璃 + 力导向知识图谱 */
|
||||
.tpl-graphify-dark-graph{
|
||||
--gd-bg:#06060c;
|
||||
--gd-bg2:#0e1020;
|
||||
--gd-text:#f0ece4;
|
||||
--gd-text2:#b0a99e;
|
||||
--gd-text3:#7a746c;
|
||||
--gd-warm:#e8a87c;
|
||||
--gd-blue:#7eb8da;
|
||||
--gd-green:#7ed3a4;
|
||||
--gd-rose:#d4a0b9;
|
||||
--gd-purple:#b8a4d6;
|
||||
--gd-danger:#e07070;
|
||||
background:var(--gd-bg);
|
||||
color:var(--gd-text);
|
||||
font-family:'Inter','Noto Sans SC',-apple-system,sans-serif;
|
||||
letter-spacing:-.01em;
|
||||
}
|
||||
.tpl-graphify-dark-graph .slide{background:linear-gradient(160deg,#08080f,#0e1020 50%,#08080f);color:var(--gd-text);padding:64px 88px;overflow:hidden}
|
||||
.tpl-graphify-dark-graph .gd-ambient{position:absolute;inset:0;pointer-events:none;z-index:0;overflow:hidden}
|
||||
.tpl-graphify-dark-graph .gd-orb{position:absolute;border-radius:50%;filter:blur(110px);opacity:.35;animation:gdDrift 22s ease-in-out infinite alternate}
|
||||
.tpl-graphify-dark-graph .gd-orb-1{width:520px;height:520px;background:radial-gradient(circle,rgba(126,184,218,.55),transparent 70%);top:-12%;left:-6%}
|
||||
.tpl-graphify-dark-graph .gd-orb-2{width:460px;height:460px;background:radial-gradient(circle,rgba(232,168,124,.45),transparent 70%);top:55%;right:-8%;animation-delay:-6s}
|
||||
.tpl-graphify-dark-graph .gd-orb-3{width:420px;height:420px;background:radial-gradient(circle,rgba(184,164,214,.4),transparent 70%);bottom:-8%;left:30%;animation-delay:-11s}
|
||||
@keyframes gdDrift{0%{transform:translate(0,0) scale(1)}100%{transform:translate(25px,-20px) scale(1.08)}}
|
||||
.tpl-graphify-dark-graph .slide > *{position:relative;z-index:2}
|
||||
.tpl-graphify-dark-graph .gd-snum{position:absolute;top:28px;right:40px;font-size:12px;letter-spacing:.25em;color:var(--gd-text3);z-index:3}
|
||||
.tpl-graphify-dark-graph .gd-eyebrow{font-size:13px;letter-spacing:.2em;text-transform:uppercase;color:var(--gd-text3);font-weight:500}
|
||||
.tpl-graphify-dark-graph .gd-h1{font-size:74px;font-weight:800;line-height:1.08;letter-spacing:-.02em;margin:16px 0 10px;color:var(--gd-text)}
|
||||
.tpl-graphify-dark-graph .gd-h2{font-size:52px;font-weight:700;line-height:1.12;margin:0 0 14px}
|
||||
.tpl-graphify-dark-graph .gd-lede{font-size:22px;line-height:1.65;font-weight:300;color:var(--gd-text2);max-width:850px}
|
||||
.tpl-graphify-dark-graph .gd-rainbow{background:linear-gradient(90deg,#ff0080,#ff4d00,#ff9900,#ffe600,#00c853,#0091ea,#6200ea,#ff0080);background-size:200% auto;-webkit-background-clip:text;-webkit-text-fill-color:transparent;animation:gdRainbow 4s linear infinite}
|
||||
@keyframes gdRainbow{0%{background-position:0% center}100%{background-position:200% center}}
|
||||
.tpl-graphify-dark-graph .gd-grad{background:linear-gradient(135deg,var(--gd-warm),var(--gd-rose),var(--gd-purple));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.tpl-graphify-dark-graph .gd-accent{color:var(--gd-warm);font-weight:500}
|
||||
.tpl-graphify-dark-graph .gd-green{color:var(--gd-green)}
|
||||
.tpl-graphify-dark-graph .gd-blue{color:var(--gd-blue)}
|
||||
.tpl-graphify-dark-graph .gd-dim{color:var(--gd-text2)}
|
||||
.tpl-graphify-dark-graph .gd-mono{font-family:'JetBrains Mono',monospace}
|
||||
.tpl-graphify-dark-graph .gd-glass{position:relative;overflow:hidden;border-radius:20px;padding:22px 26px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.1);backdrop-filter:blur(20px) saturate(160%);box-shadow:0 8px 32px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.08)}
|
||||
.tpl-graphify-dark-graph .gd-glass::before{content:'';position:absolute;top:0;left:0;right:0;height:50%;background:linear-gradient(180deg,rgba(255,255,255,.05),transparent);pointer-events:none}
|
||||
.tpl-graphify-dark-graph .gd-glass-warm{background:rgba(232,168,124,.06);border-color:rgba(232,168,124,.2)}
|
||||
.tpl-graphify-dark-graph .gd-glass-green{background:rgba(126,211,164,.06);border-color:rgba(126,211,164,.2)}
|
||||
.tpl-graphify-dark-graph .gd-glass-blue{background:rgba(126,184,218,.06);border-color:rgba(126,184,218,.2)}
|
||||
.tpl-graphify-dark-graph .gd-grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:24px}
|
||||
.tpl-graphify-dark-graph .gd-grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:24px}
|
||||
.tpl-graphify-dark-graph .gd-tag{display:inline-block;border-radius:999px;padding:5px 14px;font-size:12px;font-weight:500;margin:2px 4px 2px 0;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);color:var(--gd-text2)}
|
||||
.tpl-graphify-dark-graph .gd-cmd{font-family:'JetBrains Mono',monospace;font-size:32px;font-weight:700;color:var(--gd-green);text-shadow:0 0 30px rgba(126,211,164,.45),0 0 60px rgba(126,211,164,.15);letter-spacing:-.01em}
|
||||
.tpl-graphify-dark-graph .gd-big{font-size:120px;font-weight:900;letter-spacing:-.04em;line-height:1}
|
||||
.tpl-graphify-dark-graph .gd-codebox{background:rgba(0,0,0,.55);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:22px 26px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.8;color:#d8d4c8}
|
||||
.tpl-graphify-dark-graph .gd-codebox .cm{color:#6b6a62}
|
||||
.tpl-graphify-dark-graph .gd-codebox .kw{color:var(--gd-warm)}
|
||||
.tpl-graphify-dark-graph .gd-codebox .st{color:var(--gd-green)}
|
||||
.tpl-graphify-dark-graph .gd-codebox .fn{color:var(--gd-blue)}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# hermes-cyber-terminal
|
||||
|
||||
黑底 (`#0a0c10`) + 终端 chrome + 扫描线 + 薄荷绿 glow 大字 + JetBrains Mono 全文打字机感。灵感来自 `20260414-hermes-agent/ppt/hermes-record.html` 的 `codebox #15151b` 深色代码盒和 `hermes-vs-openclaw.html` 的实测对比气质 —— 把两者合成一份「honest cyber review」。
|
||||
|
||||
**Visual traits:** 56px cyber 网格 + CRT vignette + 半透明 scanlines 叠层、窗口 traffic-light chrome、`$ prompt` 开头的 command-line 标题、薄荷绿 text-shadow glow `#7ed3a4`、monospace 全局、虚拟 bar chart 用 stroke-only 呈现、blinking cursor、amber/green/red 分级标签。
|
||||
|
||||
**Use when:** 评测一个开发者工具 / CLI / agent,展示跑分数据、trace、diff;想要即刻给出「技术人 honest review」的视觉语气;适合长 trace / long code 的场景。
|
||||
|
||||
**Source inspiration:** `20260414-hermes-agent/ppt/hermes-record.html` + `hermes-vs-openclaw.html`.
|
||||
|
||||
**Path:** `templates/full-decks/hermes-cyber-terminal/index.html`
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Hermes Cyber Terminal</title>
|
||||
<link rel="stylesheet" href="../../../assets/fonts.css">
|
||||
<link rel="stylesheet" href="../../../assets/base.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="tpl-hermes-cyber-terminal">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER -->
|
||||
<section class="slide is-active">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-vignette"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>~/hermes · zsh · 118x42 · 01:37:04</div></div>
|
||||
<div style="margin:auto 0">
|
||||
<p class="hc-prompt">whoami --hermes</p>
|
||||
<h1 class="hc-h1">HERMES<br>AGENT / v0.9.2<span class="hc-cursor"></span></h1>
|
||||
<p class="hc-lede">一个号称能「自主跑完整软件工程任务」的命令行 agent。<br>真的好用?还是又一轮营销?—— 我连续跑了 72 小时,告诉你答案。</p>
|
||||
<div style="margin-top:26px">
|
||||
<span class="hc-tag">rust-core</span>
|
||||
<span class="hc-tag">mcp-native</span>
|
||||
<span class="hc-tag amber">72h-benchmark</span>
|
||||
<span class="hc-tag red">honest-review</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hc-footer"><span>hermes-review · lewis · 2026</span><span>01 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION DIVIDER -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>section · 01/04</div></div>
|
||||
<div style="margin:auto 0">
|
||||
<p class="hc-prompt">cat chapter_01.md</p>
|
||||
<h1 class="hc-h1" style="font-size:110px">// Setup</h1>
|
||||
<p class="hc-lede">从 <code style="color:var(--hc-amber)">brew install hermes</code> 到第一次 prompt —— 一共 4 分 22 秒。</p>
|
||||
</div>
|
||||
<div class="hc-footer"><span>section · setup</span><span>02 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT — spec cards -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>benchmark · cold-start</div></div>
|
||||
<h2 class="hc-h2">开箱数据</h2>
|
||||
<p class="hc-lede">cold start → first-successful-task 三次平均</p>
|
||||
<div class="hc-grid-3">
|
||||
<div class="hc-card"><div class="lbl">install time</div><div class="val">42s</div><div class="desc">单 binary,无 docker,无 python env。</div></div>
|
||||
<div class="hc-card"><div class="lbl">first token</div><div class="val">1.8s</div><div class="desc">接入 claude-opus-4-6,无预热。</div></div>
|
||||
<div class="hc-card"><div class="lbl">first PR merged</div><div class="val">4m22s</div><div class="desc">跑的是 fix-a-typo 级别的低难度任务。</div></div>
|
||||
</div>
|
||||
<div class="hc-grid-2">
|
||||
<div class="hc-card"><div class="lbl">// verdict +</div><div class="val" style="color:var(--hc-green);font-size:18px">冷启动是真的快</div><div class="desc">和 OpenClaw 的 docker + pip 流程比,快不止一个数量级。</div></div>
|
||||
<div class="hc-card"><div class="lbl">// verdict -</div><div class="val" style="color:var(--hc-red);font-size:18px">MCP 服务器配置不够友好</div><div class="desc">env 变量需要手动塞进 ~/.hermes/env,文档几乎没写。</div></div>
|
||||
</div>
|
||||
<div class="hc-footer"><span>data · verified 3 runs</span><span>03 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 4. CODE -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>trace · hermes run</div></div>
|
||||
<p class="hc-prompt">hermes run "refactor auth module to use pkce"</p>
|
||||
<h3 class="hc-h3" style="margin-top:12px">↓ 真实 trace (节选)</h3>
|
||||
<pre class="hc-codebox" style="margin-top:10px"><span class="cm"># hermes v0.9.2 · session 42a1</span>
|
||||
[<span class="fn">plan</span>] <span class="st">"分析 src/auth/*.ts → 找 oauth flow → 抽成 pkce"</span>
|
||||
[<span class="fn">read</span>] src/auth/oauth.ts <span class="cm">// 214 lines</span>
|
||||
[<span class="fn">read</span>] src/auth/token.ts <span class="cm">// 88 lines</span>
|
||||
[<span class="kw">think</span>] <span class="st">"发现 implicit flow,改为 code+pkce,需新 state param"</span>
|
||||
[<span class="fn">edit</span>] src/auth/oauth.ts <span class="hl">+43 -17</span>
|
||||
[<span class="fn">edit</span>] src/auth/token.ts <span class="hl">+12 -4</span>
|
||||
[<span class="fn">test</span>] pnpm vitest auth <span class="st">PASS 18/18</span>
|
||||
[<span class="fn">commit</span>] <span class="var">"feat(auth): migrate to oauth2 code+pkce"</span>
|
||||
[<span class="fn">push</span>] origin feat/pkce-auth <span class="st">ok</span>
|
||||
|
||||
<span class="cm"># 总耗时 3m 14s · 14k tokens · $0.21</span></pre>
|
||||
<div class="hc-footer"><span>trace · live</span><span>04 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 5. CHART -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>benchmark · hermes vs openclaw</div></div>
|
||||
<h2 class="hc-h2">72 小时对比</h2>
|
||||
<p class="hc-lede">同一组 48 个 GitHub issue,两个 agent 各跑一遍</p>
|
||||
<svg viewBox="0 0 1000 380" style="width:100%;max-width:1040px;margin-top:24px" xmlns="http://www.w3.org/2000/svg">
|
||||
<g font-family="JetBrains Mono, monospace" font-size="13" fill="#8a8892">
|
||||
<!-- axis -->
|
||||
<line x1="80" y1="40" x2="80" y2="320" stroke="rgba(126,211,164,.2)"/>
|
||||
<line x1="80" y1="320" x2="960" y2="320" stroke="rgba(126,211,164,.2)"/>
|
||||
<!-- y labels -->
|
||||
<text x="70" y="46" text-anchor="end">100%</text>
|
||||
<text x="70" y="116" text-anchor="end">75%</text>
|
||||
<text x="70" y="186" text-anchor="end">50%</text>
|
||||
<text x="70" y="256" text-anchor="end">25%</text>
|
||||
<text x="70" y="324" text-anchor="end">0</text>
|
||||
<!-- bars: hermes -->
|
||||
<g>
|
||||
<rect x="130" y="80" width="80" height="240" fill="rgba(126,211,164,.15)" stroke="#7ed3a4" stroke-width="1.5"/>
|
||||
<text x="170" y="76" text-anchor="middle" fill="#7ed3a4" font-weight="700">82%</text>
|
||||
<text x="170" y="345" text-anchor="middle">resolved</text>
|
||||
<rect x="240" y="146" width="80" height="174" fill="rgba(126,211,164,.15)" stroke="#7ed3a4" stroke-width="1.5"/>
|
||||
<text x="280" y="142" text-anchor="middle" fill="#7ed3a4" font-weight="700">58%</text>
|
||||
<text x="280" y="345" text-anchor="middle">one-shot</text>
|
||||
<rect x="350" y="60" width="80" height="260" fill="rgba(126,211,164,.15)" stroke="#7ed3a4" stroke-width="1.5"/>
|
||||
<text x="390" y="56" text-anchor="middle" fill="#7ed3a4" font-weight="700">89%</text>
|
||||
<text x="390" y="345" text-anchor="middle">test-pass</text>
|
||||
<rect x="460" y="110" width="80" height="210" fill="rgba(126,211,164,.15)" stroke="#7ed3a4" stroke-width="1.5"/>
|
||||
<text x="500" y="106" text-anchor="middle" fill="#7ed3a4" font-weight="700">71%</text>
|
||||
<text x="500" y="345" text-anchor="middle">pr-merged</text>
|
||||
</g>
|
||||
<!-- bars: openclaw -->
|
||||
<g>
|
||||
<rect x="570" y="150" width="80" height="170" fill="rgba(233,197,138,.12)" stroke="#e9c58a" stroke-width="1.5"/>
|
||||
<text x="610" y="146" text-anchor="middle" fill="#e9c58a" font-weight="700">60%</text>
|
||||
<text x="610" y="345" text-anchor="middle">resolved</text>
|
||||
<rect x="680" y="212" width="80" height="108" fill="rgba(233,197,138,.12)" stroke="#e9c58a" stroke-width="1.5"/>
|
||||
<text x="720" y="208" text-anchor="middle" fill="#e9c58a" font-weight="700">38%</text>
|
||||
<text x="720" y="345" text-anchor="middle">one-shot</text>
|
||||
<rect x="790" y="130" width="80" height="190" fill="rgba(233,197,138,.12)" stroke="#e9c58a" stroke-width="1.5"/>
|
||||
<text x="830" y="126" text-anchor="middle" fill="#e9c58a" font-weight="700">67%</text>
|
||||
<text x="830" y="345" text-anchor="middle">test-pass</text>
|
||||
</g>
|
||||
<!-- legend -->
|
||||
<g transform="translate(820,50)">
|
||||
<rect x="0" y="0" width="14" height="14" fill="rgba(126,211,164,.15)" stroke="#7ed3a4"/>
|
||||
<text x="22" y="12" fill="#7ed3a4">hermes 0.9.2</text>
|
||||
<rect x="0" y="22" width="14" height="14" fill="rgba(233,197,138,.12)" stroke="#e9c58a"/>
|
||||
<text x="22" y="34" fill="#e9c58a">openclaw 2.1</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="hc-footer"><span>benchmark · n=48</span><span>05 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 6. STATS -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>tldr</div></div>
|
||||
<p class="hc-prompt">echo $VERDICT</p>
|
||||
<div class="hc-big">7.8<span style="font-size:60px;color:var(--hc-ink2)">/ 10</span></div>
|
||||
<p class="hc-lede" style="margin-top:14px">值得装,还不值得完全依赖。</p>
|
||||
<div class="hc-grid-2" style="margin-top:24px">
|
||||
<div class="hc-card"><div class="lbl">+ strong points</div><div class="desc">• rust 本体冷启快<br>• trace 可读性极强<br>• diff 审核友好,commit message 也写得合格</div></div>
|
||||
<div class="hc-card"><div class="lbl">- weak points</div><div class="desc">• plan 阶段偶尔跳步<br>• 超 50k LoC 仓库会 OOM<br>• MCP 配置需要手动塞 env</div></div>
|
||||
</div>
|
||||
<div class="hc-footer"><span>verdict · honest</span><span>06 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>install</div></div>
|
||||
<h2 class="hc-h2">想自己跑一遍?</h2>
|
||||
<p class="hc-lede">三条命令,不到 5 分钟就能看见它干第一件事。</p>
|
||||
<pre class="hc-codebox" style="margin-top:22px"><span class="cm"># 1. install</span>
|
||||
<span class="kw">$</span> brew install hermes-agent/tap/hermes
|
||||
|
||||
<span class="cm"># 2. auth (先准备好 anthropic api key)</span>
|
||||
<span class="kw">$</span> hermes auth login
|
||||
|
||||
<span class="cm"># 3. first task</span>
|
||||
<span class="kw">$</span> cd ~/your-repo && hermes run <span class="st">"add a CHANGELOG.md from git log"</span></pre>
|
||||
<div style="margin-top:26px">
|
||||
<span class="hc-tag">brew-ready</span>
|
||||
<span class="hc-tag">opus-4.6</span>
|
||||
<span class="hc-tag amber">needs-api-key</span>
|
||||
</div>
|
||||
<div class="hc-footer"><span>try-it-now</span><span>07 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>EOF</div></div>
|
||||
<div style="margin:auto 0">
|
||||
<p class="hc-prompt">exit 0</p>
|
||||
<h1 class="hc-h1" style="font-size:120px">// thanks<span class="hc-cursor"></span></h1>
|
||||
<p class="hc-lede">完整 trace、48 个任务的 PR 列表、benchmark 脚本都在 <span style="color:var(--hc-amber)">github.com/lewis/hermes-review</span></p>
|
||||
</div>
|
||||
<div class="hc-footer"><span>session closed</span><span>08 / 08</span></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<script src="../../../assets/runtime.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/* hermes-cyber-terminal — 暗终端 + 霓虹绿青 + 扫描线 */
|
||||
.tpl-hermes-cyber-terminal{
|
||||
--hc-bg:#0a0c10;
|
||||
--hc-bg2:#15151b;
|
||||
--hc-surface:#12141a;
|
||||
--hc-border:rgba(126,211,164,.18);
|
||||
--hc-ink:#e4e2d8;
|
||||
--hc-ink2:#8a8892;
|
||||
--hc-green:#7ed3a4;
|
||||
--hc-cyan:#64dfdf;
|
||||
--hc-amber:#e9c58a;
|
||||
--hc-rose:#d4a0b9;
|
||||
--hc-red:#ff6b6b;
|
||||
background:var(--hc-bg);
|
||||
color:var(--hc-ink);
|
||||
font-family:'JetBrains Mono','SF Mono','Inter','Noto Sans SC',monospace;
|
||||
}
|
||||
.tpl-hermes-cyber-terminal .slide{background:var(--hc-bg);color:var(--hc-ink);padding:60px 84px;overflow:hidden}
|
||||
.tpl-hermes-cyber-terminal .hc-scanlines{position:absolute;inset:0;pointer-events:none;z-index:3;background:repeating-linear-gradient(180deg,transparent 0,transparent 3px,rgba(126,211,164,.025) 3px,rgba(126,211,164,.025) 4px);mix-blend-mode:screen}
|
||||
.tpl-hermes-cyber-terminal .hc-grid{position:absolute;inset:0;pointer-events:none;opacity:.35;background-image:linear-gradient(rgba(126,211,164,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(126,211,164,.08) 1px,transparent 1px);background-size:56px 56px;mask-image:radial-gradient(ellipse at 50% 50%,black 30%,transparent 80%)}
|
||||
.tpl-hermes-cyber-terminal .hc-vignette{position:absolute;inset:0;pointer-events:none;background:radial-gradient(ellipse at 50% 50%,transparent 50%,rgba(0,0,0,.6) 100%)}
|
||||
.tpl-hermes-cyber-terminal .slide > *{position:relative;z-index:2}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px;font-size:11px;color:var(--hc-ink2);letter-spacing:.18em;text-transform:uppercase}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome .dots{display:flex;gap:8px}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome .dots span{width:11px;height:11px;border-radius:50%;background:#2a2d33}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome .dots span:nth-child(1){background:#ff5f57}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome .dots span:nth-child(2){background:#febc2e}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome .dots span:nth-child(3){background:var(--hc-green)}
|
||||
.tpl-hermes-cyber-terminal .hc-prompt{color:var(--hc-green);font-weight:500}
|
||||
.tpl-hermes-cyber-terminal .hc-prompt::before{content:'$ ';color:var(--hc-cyan)}
|
||||
.tpl-hermes-cyber-terminal .hc-h1{font-family:'JetBrains Mono',monospace;font-size:72px;font-weight:700;line-height:1.05;letter-spacing:-.02em;color:var(--hc-green);text-shadow:0 0 30px rgba(126,211,164,.35),0 0 60px rgba(126,211,164,.1);margin:14px 0 12px}
|
||||
.tpl-hermes-cyber-terminal .hc-h2{font-size:46px;font-weight:600;color:var(--hc-ink);margin:0 0 10px;letter-spacing:-.015em}
|
||||
.tpl-hermes-cyber-terminal .hc-h3{font-size:22px;font-weight:600;color:var(--hc-amber);margin:0 0 10px}
|
||||
.tpl-hermes-cyber-terminal .hc-lede{font-size:18px;line-height:1.7;color:var(--hc-ink2);max-width:780px;font-family:'Inter','Noto Sans SC',sans-serif}
|
||||
.tpl-hermes-cyber-terminal .hc-cursor{display:inline-block;width:12px;height:1em;background:var(--hc-green);vertical-align:middle;margin-left:6px;animation:hcBlink 1s steps(2) infinite}
|
||||
@keyframes hcBlink{50%{opacity:0}}
|
||||
.tpl-hermes-cyber-terminal .hc-card{background:var(--hc-surface);border:1px solid var(--hc-border);border-radius:10px;padding:20px 24px;position:relative}
|
||||
.tpl-hermes-cyber-terminal .hc-card::before{content:'';position:absolute;top:-1px;left:12px;right:12px;height:2px;background:linear-gradient(90deg,transparent,var(--hc-green),transparent)}
|
||||
.tpl-hermes-cyber-terminal .hc-card .lbl{font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:var(--hc-ink2);margin-bottom:8px}
|
||||
.tpl-hermes-cyber-terminal .hc-card .val{font-size:22px;font-weight:700;color:var(--hc-green);font-family:'JetBrains Mono',monospace}
|
||||
.tpl-hermes-cyber-terminal .hc-card .desc{font-size:13px;color:var(--hc-ink2);margin-top:10px;line-height:1.55;font-family:'Inter',sans-serif}
|
||||
.tpl-hermes-cyber-terminal .hc-grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-top:24px}
|
||||
.tpl-hermes-cyber-terminal .hc-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:24px}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox{background:#0c0d12;border:1px solid var(--hc-border);border-radius:10px;padding:22px 26px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.85;color:#d8d4c8;box-shadow:inset 0 0 60px rgba(126,211,164,.04)}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .cm{color:#5a6068}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .kw{color:var(--hc-amber)}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .st{color:var(--hc-green)}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .fn{color:var(--hc-cyan)}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .var{color:var(--hc-rose)}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .hl{color:#fff;background:rgba(126,211,164,.15);padding:0 4px;border-radius:3px}
|
||||
.tpl-hermes-cyber-terminal .hc-tag{display:inline-block;font-family:'JetBrains Mono',monospace;font-size:11px;padding:3px 10px;border:1px solid var(--hc-border);border-radius:4px;color:var(--hc-green);background:rgba(126,211,164,.04);margin:2px 6px 2px 0;text-transform:uppercase;letter-spacing:.1em}
|
||||
.tpl-hermes-cyber-terminal .hc-tag.amber{color:var(--hc-amber);border-color:rgba(233,197,138,.2);background:rgba(233,197,138,.04)}
|
||||
.tpl-hermes-cyber-terminal .hc-tag.red{color:var(--hc-red);border-color:rgba(255,107,107,.25);background:rgba(255,107,107,.05)}
|
||||
.tpl-hermes-cyber-terminal .hc-big{font-family:'JetBrains Mono',monospace;font-size:140px;font-weight:700;line-height:1;color:var(--hc-green);text-shadow:0 0 40px rgba(126,211,164,.4),0 0 80px rgba(126,211,164,.15);letter-spacing:-.04em}
|
||||
.tpl-hermes-cyber-terminal .hc-footer{position:absolute;left:84px;right:84px;bottom:32px;display:flex;justify-content:space-between;font-size:10px;color:var(--hc-ink2);letter-spacing:.2em;text-transform:uppercase;border-top:1px solid rgba(126,211,164,.1);padding-top:14px}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# knowledge-arch-blueprint
|
||||
|
||||
奶油纸 (`#F0EAE0`) 底 + 锈红 (`#B5392A`) 单一 accent + 硬黑描边卡片 + 虚线反馈回路箭头。灵感来自 `20260405 架构图v2.html` —— 那是一张真正的「技术白皮书架构图」,像建筑蓝图。
|
||||
|
||||
**Visual traits:** 暖米色纸底、微弱 48px 网格做 blueprint 感、硬朗 2px 黑边卡片、pipeline step-box 一字排开配 hero box 凸起、右上 insight 红色 callout、大小写 kicker 2.5-4px 字距、SVG 反馈回路虚线 + 箭头、Playfair 大字号衬线数据、无渐变无阴影极度克制。
|
||||
|
||||
**Use when:** 讲系统架构、数据流向、流程拆解;你想让内容看起来像一份正经技术白皮书而不是营销贴;需要严肃感、印刷感、可直接截图塞进 README。
|
||||
|
||||
**Source inspiration:** `20260405-Karpathy-知识库/20260405 架构图v2.html`.
|
||||
|
||||
**Path:** `templates/full-decks/knowledge-arch-blueprint/index.html`
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Knowledge Arch Blueprint</title>
|
||||
<link rel="stylesheet" href="../../../assets/fonts.css">
|
||||
<link rel="stylesheet" href="../../../assets/base.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="tpl-knowledge-arch-blueprint">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER -->
|
||||
<section class="slide is-active">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:40px;margin-bottom:44px">
|
||||
<div>
|
||||
<div class="kb-kicker">Karpathy Stack · 架构图 v2</div>
|
||||
<h1 class="kb-h1">LLM <span class="rust">知识库</span> 的<br>工程化蓝图</h1>
|
||||
<p class="kb-sub">从「乱贴笔记」到「可审计、可纠错、可复用」的第二大脑 —— 这是我读完 Karpathy 的分享后画的一张系统图。</p>
|
||||
</div>
|
||||
<div class="kb-insight"><span class="kk">KEY INSIGHT</span>Karpathy 原版缺一块:<br>反馈闭环让错误能回流纠正。</div>
|
||||
</div>
|
||||
<div class="kb-section-label">Pipeline · End-to-end</div>
|
||||
<div class="kb-pipeline">
|
||||
<div class="kb-step"><div class="kb-step-num">STEP 01</div><div class="kb-step-title">采集</div><div class="kb-step-body">浏览器剪藏、PDF、Podcast 转写、聊天记录</div></div>
|
||||
<div class="kb-step"><div class="kb-step-num">STEP 02</div><div class="kb-step-title">去噪</div><div class="kb-step-body">清洗导航栏、广告、重复段落、低信噪素材</div></div>
|
||||
<div class="kb-step hero"><div class="kb-step-num">STEP 03 · CORE</div><div class="kb-step-title">Wiki 化</div><div class="kb-step-body">结构化成双链笔记,实体、关系、属性全在一起</div></div>
|
||||
<div class="kb-step"><div class="kb-step-num">STEP 04</div><div class="kb-step-title">使用</div><div class="kb-step-body">Agent 随时检索、回答、再写入</div></div>
|
||||
</div>
|
||||
<div class="kb-footer"><span>Blueprint · v2 · 2026.04</span><span>01 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION DIVIDER -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="kb-kicker">Chapter One</div>
|
||||
<h1 class="kb-h1" style="font-size:120px">为什么 <span class="rust">笔记</span><br>不够用了</h1>
|
||||
<p class="kb-sub" style="font-size:24px;margin-top:20px">当你的知识量超过记忆容量,<br>你需要的不是更多文件,而是一张<b>可导航的图</b>。</p>
|
||||
</div>
|
||||
<div class="kb-footer"><span>Section · Chapter 1</span><span>02 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT 2-col -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div class="kb-kicker">Problem · Solution</div>
|
||||
<h1 class="kb-h1" style="font-size:48px">原版 vs <span class="rust">升级版</span></h1>
|
||||
<div class="kb-grid-2">
|
||||
<div class="kb-card">
|
||||
<div class="kb-kicker" style="color:#888">原版 Karpathy</div>
|
||||
<h4>一次性写入</h4>
|
||||
<p>采集 → 转写 → 存档,错了就错了。没有回路,没有修正机制,笔记越多越混乱。</p>
|
||||
</div>
|
||||
<div class="kb-card" style="background:var(--kb-rust-soft);border-color:var(--kb-rust)">
|
||||
<div class="kb-kicker">升级 v2</div>
|
||||
<h4>反馈闭环</h4>
|
||||
<p>AI 使用知识库时记录每次 miss / 幻觉 / 过期事实,自动回灌到源文件,让笔记会自我修正。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kb-legend">
|
||||
<div class="d"><span class="b"></span>普通节点</div>
|
||||
<div class="d"><span class="b rust"></span>核心节点 · 反馈回路入口</div>
|
||||
<div class="d">—— 数据流 ┈┈ 反馈回路</div>
|
||||
</div>
|
||||
<div class="kb-footer"><span>Content · Compare</span><span>03 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 4. CODE -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div class="kb-kicker">Implementation · Skill Manifest</div>
|
||||
<h1 class="kb-h1" style="font-size:48px">反馈回路 <span class="rust">怎么实现</span></h1>
|
||||
<p class="kb-sub">一个 100 行的 Agent Skill,把「AI 用得顺不顺」回写成 vault 的一条条修订记录。</p>
|
||||
<pre class="kb-codebox"><span class="cm"># skills/wiki-feedback/SKILL.md</span>
|
||||
<span class="kw">name</span>: wiki-feedback
|
||||
<span class="kw">trigger</span>: <span class="st">"after every retrieval"</span>
|
||||
|
||||
<span class="kw">on_hit</span>: record(<span class="st">query, path, used=true</span>)
|
||||
<span class="kw">on_miss</span>: record(<span class="st">query, reason=</span><span class="hl">"not-in-vault"</span>)
|
||||
<span class="kw">on_wrong</span>: record(<span class="st">query, correction, path</span>)
|
||||
|
||||
<span class="kw">nightly</span>:
|
||||
- <span class="st">aggregate misses → suggest new notes</span>
|
||||
- <span class="st">aggregate wrongs → diff-patch old notes</span>
|
||||
- <span class="st">commit to git, open PR for human review</span></pre>
|
||||
<div class="kb-footer"><span>Content · Code</span><span>04 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 5. DIAGRAM - SVG feedback loop -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div class="kb-kicker">System Diagram</div>
|
||||
<h1 class="kb-h1" style="font-size:44px">反馈回路全貌</h1>
|
||||
<svg viewBox="0 0 1200 520" style="width:100%;max-width:1200px;margin-top:20px" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#1a1a1a"/>
|
||||
</marker>
|
||||
<marker id="arrow-r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#B5392A"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<g font-family="Inter, sans-serif" font-size="16" font-weight="700">
|
||||
<!-- boxes -->
|
||||
<rect x="40" y="180" width="200" height="120" rx="10" fill="#fff" stroke="#1a1a1a" stroke-width="2"/>
|
||||
<text x="140" y="220" text-anchor="middle">Sources</text>
|
||||
<text x="140" y="250" text-anchor="middle" font-size="12" font-weight="400" fill="#666">web · pdf · chat</text>
|
||||
|
||||
<rect x="300" y="180" width="200" height="120" rx="10" fill="#fff" stroke="#1a1a1a" stroke-width="2"/>
|
||||
<text x="400" y="220" text-anchor="middle">Clean + Split</text>
|
||||
<text x="400" y="250" text-anchor="middle" font-size="12" font-weight="400" fill="#666">defuddle / chunker</text>
|
||||
|
||||
<rect x="560" y="160" width="220" height="160" rx="10" fill="#B5392A" stroke="#B5392A" stroke-width="2"/>
|
||||
<text x="670" y="210" text-anchor="middle" fill="#fff" font-size="20">Vault (Wiki)</text>
|
||||
<text x="670" y="240" text-anchor="middle" font-size="12" font-weight="400" fill="rgba(255,255,255,.8)">markdown · links</text>
|
||||
<text x="670" y="262" text-anchor="middle" font-size="12" font-weight="400" fill="rgba(255,255,255,.8)">bases · canvas</text>
|
||||
|
||||
<rect x="840" y="180" width="200" height="120" rx="10" fill="#fff" stroke="#1a1a1a" stroke-width="2"/>
|
||||
<text x="940" y="220" text-anchor="middle">Agent Use</text>
|
||||
<text x="940" y="250" text-anchor="middle" font-size="12" font-weight="400" fill="#666">retrieve / answer</text>
|
||||
|
||||
<!-- forward arrows -->
|
||||
<line x1="245" y1="240" x2="295" y2="240" stroke="#1a1a1a" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<line x1="505" y1="240" x2="555" y2="240" stroke="#1a1a1a" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<line x1="785" y1="240" x2="835" y2="240" stroke="#1a1a1a" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- feedback dashed -->
|
||||
<path d="M 940 180 Q 940 80, 670 80 Q 400 80, 400 180" fill="none" stroke="#B5392A" stroke-width="2" stroke-dasharray="6 6" marker-end="url(#arrow-r)"/>
|
||||
<rect x="580" y="58" width="180" height="30" rx="6" fill="#F0EAE0" stroke="#B5392A" stroke-width="1"/>
|
||||
<text x="670" y="78" text-anchor="middle" fill="#B5392A" font-size="12">FEEDBACK · wrong / miss</text>
|
||||
|
||||
<!-- bottom feedback to sources -->
|
||||
<path d="M 940 300 Q 940 420, 670 420 Q 140 420, 140 300" fill="none" stroke="#B5392A" stroke-width="2" stroke-dasharray="6 6" marker-end="url(#arrow-r)"/>
|
||||
<rect x="560" y="400" width="220" height="30" rx="6" fill="#F0EAE0" stroke="#B5392A" stroke-width="1"/>
|
||||
<text x="670" y="420" text-anchor="middle" fill="#B5392A" font-size="12">NIGHTLY · suggest new sources</text>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="kb-footer"><span>Diagram · Feedback Loop</span><span>05 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 6. STATS -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div class="kb-kicker">After 6 Months</div>
|
||||
<h1 class="kb-h1" style="font-size:44px">升级版 <span class="rust">跑了半年</span> 的数据</h1>
|
||||
<div style="display:grid;grid-template-columns:1.3fr 1fr 1fr;gap:24px;margin-top:28px;align-items:center">
|
||||
<div style="text-align:center"><div class="kb-big-num">13</div><p style="font-size:14px;color:#666;margin-top:6px;letter-spacing:.1em;text-transform:uppercase">关键优化项 · 全部落地</p></div>
|
||||
<div class="kb-card"><h4 style="color:var(--kb-rust)">-62%</h4><p>幻觉率(相比无反馈回路版本)</p></div>
|
||||
<div class="kb-card"><h4 style="color:var(--kb-rust)">+4.1×</h4><p>单次检索命中率</p></div>
|
||||
</div>
|
||||
<div class="kb-grid-2" style="margin-top:18px">
|
||||
<div class="kb-card"><h4>自动修订 227 条</h4><p>其中 189 条被人工批准合并,38 条被拒绝(数据已归档)。</p></div>
|
||||
<div class="kb-card"><h4>新增笔记 412 篇</h4><p>从 miss 日志聚类而来,每篇都有来源追溯。</p></div>
|
||||
</div>
|
||||
<div class="kb-footer"><span>Content · Stats</span><span>06 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div class="kb-kicker">Next Step</div>
|
||||
<h1 class="kb-h1" style="font-size:60px">开始你的 <span class="rust">Wiki v2</span></h1>
|
||||
<p class="kb-sub">不用重写所有笔记。先接一条回路,让 AI 的每次使用都在「改好」你的 vault。</p>
|
||||
<div class="kb-pipeline">
|
||||
<div class="kb-step"><div class="kb-step-num">TONIGHT</div><div class="kb-step-title">装 Skill</div><div class="kb-step-body">pnpm i -g @lewis/wiki-feedback</div></div>
|
||||
<div class="kb-step"><div class="kb-step-num">DAY 2</div><div class="kb-step-title">跑 7 天</div><div class="kb-step-body">观察 miss log 自动累积</div></div>
|
||||
<div class="kb-step hero"><div class="kb-step-num">DAY 8 · CORE</div><div class="kb-step-title">第一次审 PR</div><div class="kb-step-body">花 15 分钟 review 自动生成的修订</div></div>
|
||||
<div class="kb-step"><div class="kb-step-num">MONTH 1</div><div class="kb-step-title">开始信它</div><div class="kb-step-body">你的 vault 会变成活的</div></div>
|
||||
</div>
|
||||
<div class="kb-footer"><span>CTA</span><span>07 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div style="margin:auto 0;text-align:center">
|
||||
<div class="kb-kicker">END · blueprint v2</div>
|
||||
<h1 class="kb-h1" style="font-size:140px;margin-top:24px">谢谢 <span class="rust">·</span> thanks</h1>
|
||||
<p class="kb-sub" style="margin:0 auto;font-size:22px">图纸、Skill、笔记模板都在 <b>github.com/lewis/karpathy-wiki-v2</b></p>
|
||||
</div>
|
||||
<div class="kb-footer"><span>End of deck</span><span>08 / 08</span></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<script src="../../../assets/runtime.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/* knowledge-arch-blueprint — 奶油纸 + 建筑蓝图风 */
|
||||
.tpl-knowledge-arch-blueprint{
|
||||
--kb-bg:#F0EAE0;
|
||||
--kb-ink:#1a1a1a;
|
||||
--kb-ink2:#555;
|
||||
--kb-ink3:#aaa;
|
||||
--kb-rust:#B5392A;
|
||||
--kb-rust-soft:rgba(181,57,42,.08);
|
||||
--kb-line:#cec8be;
|
||||
background:var(--kb-bg);
|
||||
color:var(--kb-ink);
|
||||
font-family:'Inter','Noto Sans SC',-apple-system,sans-serif;
|
||||
}
|
||||
.tpl-knowledge-arch-blueprint .slide{background:var(--kb-bg);color:var(--kb-ink);padding:64px 80px}
|
||||
.tpl-knowledge-arch-blueprint .kb-grid-bg{position:absolute;inset:0;pointer-events:none;opacity:.35;background-image:linear-gradient(rgba(26,26,26,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(26,26,26,.06) 1px,transparent 1px);background-size:48px 48px;mask-image:radial-gradient(ellipse at center,black 40%,transparent 85%)}
|
||||
.tpl-knowledge-arch-blueprint .slide > *{position:relative;z-index:2}
|
||||
.tpl-knowledge-arch-blueprint .kb-kicker{font-size:13px;font-weight:800;letter-spacing:4px;text-transform:uppercase;color:var(--kb-rust);margin-bottom:12px}
|
||||
.tpl-knowledge-arch-blueprint .kb-h1{font-size:66px;font-weight:900;line-height:1.08;color:#111;margin:0 0 14px;letter-spacing:-.02em}
|
||||
.tpl-knowledge-arch-blueprint .kb-h1 span.rust{color:var(--kb-rust)}
|
||||
.tpl-knowledge-arch-blueprint .kb-sub{font-size:20px;color:#666;line-height:1.55;max-width:780px}
|
||||
.tpl-knowledge-arch-blueprint .kb-insight{display:inline-block;background:var(--kb-rust);color:#fff;border-radius:10px;padding:16px 22px;font-size:14px;font-weight:700;line-height:1.5;max-width:340px;box-shadow:0 8px 24px rgba(181,57,42,.22)}
|
||||
.tpl-knowledge-arch-blueprint .kb-insight .kk{font-size:10px;letter-spacing:2px;opacity:.7;display:block;margin-bottom:6px;font-weight:800}
|
||||
.tpl-knowledge-arch-blueprint .kb-section-label{font-size:11px;font-weight:800;letter-spacing:3.5px;text-transform:uppercase;color:#aaa;margin:30px 0 12px;display:flex;align-items:center;gap:14px}
|
||||
.tpl-knowledge-arch-blueprint .kb-section-label::after{content:'';flex:1;height:1px;background:var(--kb-line)}
|
||||
.tpl-knowledge-arch-blueprint .kb-pipeline{display:flex;align-items:stretch;gap:14px;margin-top:24px}
|
||||
.tpl-knowledge-arch-blueprint .kb-step{flex:1;border:2px solid #1a1a1a;border-radius:12px;padding:22px 18px;background:#fff;position:relative;min-height:200px;display:flex;flex-direction:column}
|
||||
.tpl-knowledge-arch-blueprint .kb-step.hero{background:var(--kb-rust);border-color:var(--kb-rust);color:#fff;flex:1.25;box-shadow:0 10px 32px rgba(181,57,42,.28);transform:translateY(-10px)}
|
||||
.tpl-knowledge-arch-blueprint .kb-step-num{font-size:10px;font-weight:800;letter-spacing:2.5px;color:#bbb;margin-bottom:8px;text-transform:uppercase}
|
||||
.tpl-knowledge-arch-blueprint .kb-step.hero .kb-step-num{color:rgba(255,255,255,.6)}
|
||||
.tpl-knowledge-arch-blueprint .kb-step-title{font-size:22px;font-weight:900;line-height:1.15;color:#111;margin-bottom:8px}
|
||||
.tpl-knowledge-arch-blueprint .kb-step.hero .kb-step-title{color:#fff}
|
||||
.tpl-knowledge-arch-blueprint .kb-step-body{font-size:13px;line-height:1.55;color:#555;margin-top:auto}
|
||||
.tpl-knowledge-arch-blueprint .kb-step.hero .kb-step-body{color:rgba(255,255,255,.88)}
|
||||
.tpl-knowledge-arch-blueprint .kb-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:24px}
|
||||
.tpl-knowledge-arch-blueprint .kb-card{background:#fff;border:2px solid #1a1a1a;border-radius:12px;padding:22px 24px}
|
||||
.tpl-knowledge-arch-blueprint .kb-card h4{font-size:20px;font-weight:900;margin-bottom:6px}
|
||||
.tpl-knowledge-arch-blueprint .kb-card p{font-size:14px;color:#555;line-height:1.55}
|
||||
.tpl-knowledge-arch-blueprint .kb-legend{display:flex;gap:18px;flex-wrap:wrap;margin-top:22px;font-size:12px;color:#666}
|
||||
.tpl-knowledge-arch-blueprint .kb-legend .d{display:flex;align-items:center;gap:8px}
|
||||
.tpl-knowledge-arch-blueprint .kb-legend .b{width:14px;height:14px;border:2px solid #1a1a1a;border-radius:3px}
|
||||
.tpl-knowledge-arch-blueprint .kb-legend .b.rust{background:var(--kb-rust);border-color:var(--kb-rust)}
|
||||
.tpl-knowledge-arch-blueprint .kb-footer{position:absolute;left:80px;right:80px;bottom:36px;display:flex;justify-content:space-between;font-size:11px;color:#999;letter-spacing:.15em;text-transform:uppercase;border-top:1px solid var(--kb-line);padding-top:16px}
|
||||
.tpl-knowledge-arch-blueprint .kb-mono{font-family:'JetBrains Mono',monospace}
|
||||
.tpl-knowledge-arch-blueprint .kb-codebox{background:#1a1a1a;color:#f0eae0;border-radius:12px;padding:22px 26px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.8;margin-top:20px;border:2px solid #1a1a1a}
|
||||
.tpl-knowledge-arch-blueprint .kb-codebox .cm{color:#7a766c}
|
||||
.tpl-knowledge-arch-blueprint .kb-codebox .kw{color:#e8a87c}
|
||||
.tpl-knowledge-arch-blueprint .kb-codebox .st{color:#b3d1bc}
|
||||
.tpl-knowledge-arch-blueprint .kb-codebox .hl{color:var(--kb-rust);background:rgba(255,255,255,.08);padding:0 4px;border-radius:3px}
|
||||
.tpl-knowledge-arch-blueprint .kb-big-num{font-family:'Playfair Display',Georgia,serif;font-size:200px;font-weight:900;line-height:.9;color:var(--kb-rust);letter-spacing:-.04em}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# obsidian-claude-gradient
|
||||
|
||||
GitHub-dark (`#0d1117`) + 紫色 ambient radial + 60px 遮罩网格 + 紫→蓝→绿渐变文字。灵感来自 `20260406-obsidian-claude/slides.html` 的 `--accent #7c3aed`、`.cbg` 双 radial cover、`.cgrid` 60px 遮罩网格以及 `.g` 三停渐变。
|
||||
|
||||
**Visual traits:** 深灰蓝底 + 紫蓝 radial 晕染 + 暗网格遮罩、居中对齐的标题/正文、圆角紫色 pill tag、linear `#a855f7→#60a5fa→#34d399` 渐变字、GitHub-ish 代码色 (`#010409` 背景 + 紫/蓝/橙/绿 token)、紫色左边框 highlight 块、简洁 step 列表。
|
||||
|
||||
**Use when:** 讲一个开发者友好的工作流、MCP / Agent / Dev tool 教程;你希望气质接近 GitHub Blog / Linear Changelog;内容以配置文件 + 步骤为主。
|
||||
|
||||
**Source inspiration:** `20260406-obsidian-claude/slides.html`.
|
||||
|
||||
**Path:** `templates/full-decks/obsidian-claude-gradient/index.html`
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user