Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Deploy preview site to GitHub Pages

on:
push:
branches: [main]
workflow_dispatch:

# Allow GITHUB_TOKEN to deploy to Pages.
permissions:
contents: read
pages: write
id-token: write

# Let a running deploy finish; queue at most one more.
concurrency:
group: pages
cancel-in-progress: false

jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# The site is pure static HTML/CSS/JS — no build step. The whole repo is
# published so that preview/ can reference ../assets/ and ../examples/
# exactly as it does on a local file server.
- name: Guard against Jekyll eating underscore paths
# assets/animations/fx/_util.js would be silently dropped by Jekyll,
# which breaks all 20 canvas FX with no error. .nojekyll must exist.
run: test -f .nojekyll || { echo "::error::.nojekyll is missing"; exit 1; }

- uses: actions/configure-pages@v5

- uses: actions/upload-pages-artifact@v3
with:
path: '.'

- id: deployment
uses: actions/deploy-pages@v4
Empty file added .nojekyll
Empty file.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
**License:** MIT
**中文文档:** [README.zh-CN.md](README.zh-CN.md)

### 🔍 [Live preview → lewislulu.github.io/html-ppt-skill](https://lewislulu.github.io/html-ppt-skill/preview/)

Browse every theme and animation on a real slide, in the browser, with no
install. Pick any of the **36 themes** and the slide re-skins instantly — no
reload — because a theme is nothing but a set of `:root` variables. Switch the
same slide between **中文 / English / Tiếng Việt** to see how a theme handles
ideographic versus Latin text before you commit to it.

![html-ppt — cover with live previews](docs/readme/hero.gif)

> One command installs **36 themes × 20 canvas FX × 31 layouts × 15 full decks + presenter mode**. Every preview above is a live iframe of a real template file rendering inside the deck — no screenshots, no mock-ups.
Expand Down
7 changes: 7 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
**协议:** MIT
**English docs:** [README.md](README.md)

### 🔍 [在线预览 → lewislulu.github.io/html-ppt-skill](https://lewislulu.github.io/html-ppt-skill/preview/)

不用安装,直接在浏览器里用真实幻灯片浏览全部主题和动效。**36 套主题**任意点选,
幻灯片**当场换肤、不刷新**——因为一套主题无非就是一组 `:root` 变量。同一页还能在
**中文 / English / Tiếng Việt** 之间切换,在你决定用哪套主题之前,先看清它在
表意文字和拉丁文字下分别是什么样。

![html-ppt 封面 · 实时预览](docs/readme/hero.gif)

> 一行命令装好 **36 主题 × 20 Canvas FX × 31 布局 × 15 完整 deck + 演讲者模式**。
Expand Down
46 changes: 44 additions & 2 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ One command, no build. Pure static HTML/CSS/JS with only CDN webfonts.
- **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
- **Live preview site** (`preview/`) — every theme and animation on one real
slide, switchable without a reload, in 中文 / English / Tiếng Việt. Published
at <https://lewislulu.github.io/html-ppt-skill/preview/>. Point users here when
they ask "which theme should I use?" instead of describing themes in prose.
- **Optional i18n layer** (`assets/i18n.js`) — mark text with `data-i18n="key"`,
keep the default language inline, and ship other languages as JSON. See
`examples/demo-deck/` for a working three-language deck.
- **Headless Chrome render script** for PNG export

## When to use
Expand Down Expand Up @@ -69,7 +76,11 @@ 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:
candidates based on tone — and link the user to the live preview so they can
judge for themselves rather than take your word for it:
<https://lewislulu.github.io/html-ppt-skill/preview/>. A direct link can carry
the exact theme, slide and language, e.g.
`preview/#stage=theme&theme=tokyo-night&lang=en&slide=1`.
- Business / investor pitch → `pitch-deck-vc`, `corporate-clean`, `swiss-grid`
- Tech sharing / engineering → `tokyo-night`, `dracula`, `catppuccin-mocha`,
`terminal-green`, `blueprint`
Expand Down Expand Up @@ -167,11 +178,20 @@ Chinese + English deck, and how to export.
```
html-ppt/
├── SKILL.md (this file)
├── index.html (GitHub Pages landing hub)
├── .nojekyll (REQUIRED — Jekyll would drop fx/_util.js)
├── references/ (detailed catalogs, load as needed)
├── preview/ (live theme + animation + language preview site)
│ ├── index.html (control panel)
│ ├── app.js / app.css (state, URL hash, postMessage bridge)
│ ├── anim-stage.html (single-slide rig for all 47 animations)
│ ├── themes.json (36-theme catalog + swatch colors)
│ └── i18n/*.json (en / vi strings for the anim stage)
├── assets/
│ ├── base.css (tokens + primitives — do not edit per deck)
│ ├── fonts.css (webfont imports)
│ ├── runtime.js (keyboard + presenter + overview + theme cycle)
│ ├── i18n.js (optional data-i18n multilingual layer)
│ ├── themes/*.css (36 token overrides, one per theme)
│ └── animations/
│ ├── animations.css (27 named CSS entry animations)
Expand All @@ -188,9 +208,30 @@ html-ppt/
├── scripts/
│ ├── new-deck.sh (scaffold a deck from deck.html)
│ └── render.sh (headless Chrome → PNG)
└── examples/demo-deck/ (complete working deck)
└── examples/demo-deck/ (complete working deck, zh + en + vi)
├── index.html (zh-CN inline = the default language)
└── i18n/{en,vi}.json (translations, fetched on demand)
```

## Multilingual decks

`assets/i18n.js` is optional and off unless a deck opts in. The default language
stays as inline HTML — so a deck still reads correctly when opened straight off
the filesystem, where `fetch()` is blocked — and other languages are JSON files
loaded only when selected.

```html
<body data-i18n-default="zh" data-i18n-src="i18n/{lang}.json">
<h1 data-i18n="cover.h1">默认语言写在这里</h1>
<script src="../../assets/i18n.js"></script>
```

Then `?lang=en` in the URL, or `HPXI18n.setLang('en')` at runtime. Values are
inserted as HTML, so a translation may carry `<br>` or `<span>`. Any key a
dictionary omits falls back to the inline text. Listen for the `hpx:i18n` event
on `document` to redraw anything JS-painted (a Chart.js canvas won't re-label
itself — see slide 6 of `examples/demo-deck/`).

## Rendering to PNG

`scripts/render.sh` wraps headless Chrome at
Expand All @@ -212,6 +253,7 @@ 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
?lang=xx URL param — pick a language (needs i18n.js)
T cycle themes (reads data-themes attr)
A cycle demo animation on current slide
#/N in URL deep-link to slide N
Expand Down
46 changes: 42 additions & 4 deletions assets/animations/fx-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,33 @@
const name = el.getAttribute('data-fx');
const fn = window.HPX[name];
if (typeof fn !== 'function') return;
/* Snapshot the host's existing children before the module runs. Modules
* append their own nodes (a canvas, a number overlay, a text wrapper) and
* several never remove them in stop(); on a re-init that debris would
* stack up. The host may equally hold the deck author's own markup, which
* must survive. Recording what was there first is the only way to tell
* the two apart. */
const own = new Set(Array.from(el.childNodes));
try {
const handle = fn(el, {}) || { stop(){} };
window.__hpxActive.set(el, handle);
window.__hpxActive.set(el, { handle: handle, own: own });
} 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){}
const rec = window.__hpxActive.get(el);
if (!rec) return;
if (rec.handle && typeof rec.handle.stop === 'function'){
try{ rec.handle.stop(); }catch(e){}
}
/* Remove only what the module added, never what was already there. */
if (rec.own){
Array.from(el.childNodes).forEach((n) => {
if (!rec.own.has(n)) el.removeChild(n);
});
}
window.__hpxActive.delete(el);
});
Expand All @@ -68,9 +82,33 @@
stopFxIn(root);
initFxIn(root);
}
/* fx-runtime is the single owner of every [data-fx] lifecycle. Anything that
* wants to swap an effect should change the attribute and call these, rather
* than invoking HPX[name] itself — two creators on one host means two
* canvases, of which only the last is tracked and stoppable. */
window.__hpxReinit = reinitFxIn;
window.__hpxStop = stopFxIn;
window.__hpxInit = initFxIn;

/* Canvas FX read their colors out of CSS variables exactly once, at init —
* a canvas cannot re-skin itself the way a CSS rule does. So on a theme swap
* every running effect keeps painting the previous theme's palette, which is
* only ever noticed by whoever presses T on a slide with a [data-fx].
*
* A <link> fires 'load' every time its href changes, and that is precisely
* the moment the new variables become readable — reinitialising any earlier
* would just re-sample the outgoing theme. */
function watchThemeSwaps(){
const link = document.getElementById('theme-link');
if (!link) return;
link.addEventListener('load', () => {
const active = document.querySelector('.slide.is-active') || document.querySelector('.slide');
if (active) reinitFxIn(active);
});
}

function boot(){
watchThemeSwaps();
ready.then(() => {
const active = document.querySelector('.slide.is-active') || document.querySelector('.slide');
if (active) initFxIn(active);
Expand Down
63 changes: 60 additions & 3 deletions assets/animations/fx/_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,73 @@

U.accent = (el, fb) => U.css(el, '--accent', fb || '#7c5cff');
U.accent2 = (el, fb) => U.css(el, '--accent-2', fb || '#22d3ee');
U.accent3 = (el, fb) => U.css(el, '--accent-3', fb || '#f0abfc');
U.text = (el, fb) => U.css(el, '--text-1', fb || '#eaeaf2');
U.bg = (el, fb) => U.css(el, '--bg', fb || '#0b0c10');

/* The per-frame wash that turns moving particles into trails: instead of
* clearing the canvas, paint the page's own background over it at low alpha
* so older frames sink into the slide.
*
* It has to be the *theme's* background. A hard-coded black wash is invisible
* on a dark deck and therefore looks correct — but on minimal-white it fogs a
* white slide to solid black within a second or two, taking the headline with
* it. Same bug, opposite sign. */
U.fade = (el, a) => U.alpha(U.bg(el, '#0b0c10'), a);

/* Decorative color pool for particles, nodes, sparks and rings.
*
* These are the theme's three accents, and nothing else. Two reasons:
*
* 1. They are what a theme actually designs as its signature triad — most
* themes build --grad out of exactly these three, so they are known to sit
* together.
* 2. The obvious-looking alternative, --good/--warn/--bad, is a *semantic*
* status triad meant for charts, diffs and pros/cons. Green/amber/red mean
* something. Firing them out of a confetti cannon means nothing, and on a
* restrained theme it actively fights the design: minimal-white's accents
* are three greys, so borrowing its --bad painted red dots across a deck
* whose entire point is greyscale.
*
* Callers must not assume a length — index with pal[i % pal.length]. This
* list has been 5 long and is now 3; treat it as "the theme's decorative
* colors", however many that turns out to be.
*/
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.accent3(el, '#f0abfc'),
];

/* Re-alpha a CSS color so an fx can draw a translucent glow/trail in a theme
* color instead of a hard-coded one. Handles the two forms theme tokens
* actually use — hex (#rgb / #rrggbb, with or without an alpha pair) and
* rgb()/rgba(). Anything else is returned untouched: better to draw the color
* at full strength than to throw inside an animation frame. */
U.alpha = (color, a) => {
if (typeof color !== 'string') return color;
const c = color.trim();

let m = /^#([0-9a-f]{3,8})$/i.exec(c);
if (m) {
let h = m[1];
if (h.length === 3 || h.length === 4) h = h.split('').map(x => x + x).join('');
if (h.length !== 6 && h.length !== 8) return c;
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
return `rgba(${r},${g},${b},${a})`;
}

m = /^rgba?\(([^)]+)\)$/i.exec(c);
if (m) {
const p = m[1].split(/[,\s/]+/).filter(Boolean);
if (p.length >= 3) return `rgba(${p[0]},${p[1]},${p[2]},${a})`;
}

return c;
};

U.canvas = (el) => {
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const c = document.createElement('canvas');
Expand Down
7 changes: 4 additions & 3 deletions assets/animations/fx/chain-react.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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 tx = U.text(el,'#e7e7ef');
const N = 8;
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
Expand All @@ -20,18 +21,18 @@
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(0, U.alpha(ac, 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.strokeStyle=U.alpha(tx, 0.4); ctx.lineWidth=2;
ctx.stroke();
// connectors
if (i<N-1){
ctx.strokeStyle='rgba(200,200,230,0.3)'; ctx.lineWidth=2;
ctx.strokeStyle=U.alpha(tx, 0.3); ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(x+r,cy); ctx.lineTo(x+dx-r,cy); ctx.stroke();
}
}
Expand Down
4 changes: 3 additions & 1 deletion assets/animations/fx/counter-explosion.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
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.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 ' + U.alpha(U.accent(el, '#7c5cff'), 0.5) + ';';
num.textContent = '0';
el.appendChild(num);
let parts = [];
Expand Down
2 changes: 1 addition & 1 deletion assets/animations/fx/data-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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.fillStyle = U.fade(el, 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++){
Expand Down
2 changes: 1 addition & 1 deletion assets/animations/fx/firework.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
};
let last = -1;
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(0,0,0,0.18)';
ctx.fillStyle = U.fade(el, 0.18);
ctx.fillRect(0,0,k.w,k.h);
if (t - last > 0.7) { launch(); last = t; }
const dt = 1/60;
Expand Down
2 changes: 1 addition & 1 deletion assets/animations/fx/galaxy-swirl.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
s: U.rand(0.8, 2.2) };
});
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillStyle = U.fade(el, 0.15);
ctx.fillRect(0,0,k.w,k.h);
const cx=k.w/2, cy=k.h/2;
for (const p of parts){
Expand Down
2 changes: 1 addition & 1 deletion assets/animations/fx/gradient-blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
return m.map(x=>parseInt(x,16));
};
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(10,12,22,0.2)';
ctx.fillStyle = U.fade(el, 0.2);
ctx.fillRect(0,0,k.w,k.h);
ctx.globalCompositeOperation = 'lighter';
for (const b of blobs){
Expand Down
2 changes: 1 addition & 1 deletion assets/animations/fx/knowledge-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
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;
ctx.strokeStyle = U.alpha(tx, 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();
Expand Down
Loading