
Html Ppt
- 17.7k installs
- 7.6k repo stars
- Updated April 26, 2026
- lewislulu/html-ppt-skill
HTML PPT is a static HTML presentation framework with 36 themes, 31 layouts, animations, and presenter mode for authoring professional slide decks.
About
HTML PPT Studio is a static HTML presentation framework with 36 themes, 31 layout types, and 27 CSS animations plus 20 canvas FX effects for professional slide decks. It includes presenter mode with speaker notes, keyboard navigation (arrows, T for themes, S for presenter view), and 14 full-deck templates for common presentation types. Use this skill when creating pitch decks, technical presentations, or content-rich slideshows. No build step required - output is pure HTML/CSS/JS with optional headless Chrome rendering to PNG.
- 36 themes and 31 layout types with token-based design system
- Presenter mode with speaker notes, live preview sync, and timer
- 27 CSS animations and 20 canvas FX effects (particles, graphs, fireworks)
Html Ppt by the numbers
- 17,692 all-time installs (skills.sh)
- +483 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #48 of 688 Office & Documents skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
html-ppt capabilities & compatibility
- Use cases
- presentations
- Platforms
- macOS · Windows · Linux
- Runs
- Runs locally
- Pricing
- Free
npx skills add https://github.com/lewislulu/html-ppt-skill --skill html-pptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17.7k |
|---|---|
| repo stars | ★ 7.6k |
| Security audit | 3 / 3 scanners passed |
| Last updated | April 26, 2026 |
| Repository | lewislulu/html-ppt-skill ↗ |
How do you turn HTML Tailwind markup into slides?
HTML PPT Studio is a static HTML presentation framework with 36 themes, 31 layout types, and 27 CSS animations plus 20 canvas FX effects for professional slide decks. It includes presenter mode with
Who is it for?
Developers or tech leads who author slides as HTML plus Tailwind and need fast deck output for reports or demos.
Skip if: Native PowerPoint macro automation, chart-heavy financial modeling decks, or teams requiring collaborative real-time slide editing in Google Slides.
When should I use this skill?
User wants HTML or Tailwind content turned into a presentation deck, weekly report slides, or template-based slide output.
What you get
PowerPoint-style slide deck from HTML templates with shared assets and full-deck layouts ready to present.
- html presentation
- styled slide deck
- optional png exports
By the numbers
- Includes 2 named full-deck templates: weekly-report and testing-safety-alert
Files
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
npx skills add https://github.com/lewislulu/html-ppt-skillOne 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) viadata-anim - 20 canvas FX animations (
assets/animations/fx/*.js) viadata-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 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:
./scripts/new-deck.sh my-talk
open examples/my-talk/index.html2. Pick a theme. Open the deck and press T to cycle. Or hard-code it:
<link rel="stylesheet" id="theme-link" href="../assets/themes/aurora.css">Catalog in 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. 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. 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 and gallery at templates/full-decks-index.html. 6. Render to PNG.
./scripts/render.sh templates/theme-showcase.html # one shot
./scripts/render.sh examples/my-talk/index.html 12 # 12 slidesAuthoring 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.jsmakes.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 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 — all 36 themes with when-to-use.
- references/layouts.md — all 31 layout types.
- references/animations.md — 27 CSS + 20 canvas FX animations.
- references/full-decks.md — all 15 full-deck templates.
- references/presenter-mode.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.
./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 dirKeyboard 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 overlaysLicense & author
MIT. Copyright (c) 2026 lewis <sudolewis@gmail.com>.
# 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
/* 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}
}
/* 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();
}
})();
/* 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);
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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); } };
};
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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); } };
};
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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(); } };
};
})();
(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();
}};
};
})();
(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(); } };
};
})();
(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); } };
};
})();
(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(); } };
};
})();
/* 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}
}
/* 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');
/* 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);
});
})();
/* 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}
/* 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;
}
/* 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%)}
/* 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}
/* 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)}
/* 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;
}
/* 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;
}
/* 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)}
/* 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)}
/* 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;
}
/* 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}
/* 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}
/* 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)}
/* 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;
}
/* 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}
/* 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}
/* 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}
/* 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)}
/* 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;
}
/* 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}
/* 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}
/* 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;
}
/* 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}
/* 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}}
/* 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)}
Bud1% @� @� @� @E%DSDB`� @� @� @Related skills
How it compares
Pick html-ppt when slides already exist as HTML/Tailwind; use diagram skills when architecture visuals are the primary output.
FAQ
How many themes are available?
36 themes including minimal-white, tokyo-night, cyberpunk-neon, corporate-clean, xiaohongshu-white, and many more covering different styles and audiences.
What's the presenter mode?
Press S to open a popup with 4 draggable cards: current slide preview, next slide preview, speaker script (逐字稿), and timer with navigation controls.
Is Html Ppt safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.