Files
guizang-ppt-skill/assets/template.html
T

1372 lines
89 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[必填] 替换为 PPT 标题 · Deck Title</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&family=Source+Serif+4:ital,opsz,wght@0,8..60,300;0,8..60,400;0,8..60,500;0,8..60,600;1,8..60,400&family=IBM+Plex+Mono:wght@300;400;500;600&family=Noto+Serif+SC:wght@300;400;500;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap" rel="stylesheet">
<style>
:root{
/* ============ 主题色(默认:🖋 墨水经典) ============
切换主题:从 references/themes.md 复制对应的 :root 块
整体替换这几行(--ink / --ink-rgb / --paper / --paper-rgb)
其他地方散落的 rgba() 都走 var(--ink-rgb) / var(--paper-rgb),无需逐处改 */
--ink:#0a0a0b;
--ink-rgb:10,10,11;
--paper:#f1efea;
--paper-rgb:241,239,234;
--paper-tint:#e8e5de;
--ink-tint:#18181a;
/* ============ 字体(跨主题固定) ============ */
--mono:"IBM Plex Mono",ui-monospace,monospace;
--serif-en:"Playfair Display","Source Serif 4",Georgia,serif;
--serif-body-en:"Source Serif 4",Georgia,serif;
--serif-zh:"Noto Serif SC",source-han-serif-sc,serif;
--sans-zh:"Noto Sans SC",source-han-sans-sc,sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:100%;height:100%;overflow:hidden;background:var(--ink);color:var(--paper);font-family:var(--sans-zh);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
/* ============ WebGL 双背景 ============ */
canvas.bg{position:fixed;inset:0;width:100vw;height:100vh;z-index:0;display:block;transition:opacity 1.2s ease}
canvas#bg-light{opacity:0}
canvas#bg-dark{opacity:1}
body.light-bg canvas#bg-light{opacity:1}
body.light-bg canvas#bg-dark{opacity:0}
body.low-power canvas.bg{display:none!important}
/* ============ Deck 容器 + 翻页 ============ */
/* width: NSLIDES * 100vw,会在 JS 里动态矫正 */
#deck{position:fixed;inset:0;width:10000vw;height:100vh;display:flex;flex-wrap:nowrap;transition:transform .9s cubic-bezier(.77,0,.175,1);z-index:10;will-change:transform}
.slide{width:100vw;height:100vh;flex:0 0 100vw;position:relative;padding:6vh 6vw 10vh 6vw;display:flex;flex-direction:column;overflow:hidden}
.slide.light{color:var(--ink);background:var(--paper)}
.slide.dark{color:var(--paper);background:var(--ink)}
/* 默认页:遮罩较厚,保证文字可读 */
.slide::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;transition:background .7s ease}
.slide.light::before{background:rgba(var(--paper-rgb),.78);backdrop-filter:blur(3px)}
.slide.dark::before{background:rgba(var(--ink-rgb),.78);backdrop-filter:blur(3px)}
/* Hero 页:遮罩大幅降低,让 WebGL 背景明显透出 */
.slide.hero.light::before{background:rgba(var(--paper-rgb),.16);backdrop-filter:none}
.slide.hero.dark::before{background:rgba(var(--ink-rgb),.12);backdrop-filter:none}
/* Hero 页顶底微弱渐隐,保证 chrome/foot 区域可读 */
.slide.hero::after{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none}
.slide.hero.light::after{background:linear-gradient(180deg,rgba(var(--paper-rgb),.28) 0%,rgba(var(--paper-rgb),0) 14%,rgba(var(--paper-rgb),0) 86%,rgba(var(--paper-rgb),.28) 100%)}
.slide.hero.dark::after{background:linear-gradient(180deg,rgba(var(--ink-rgb),.32) 0%,rgba(var(--ink-rgb),0) 14%,rgba(var(--ink-rgb),0) 86%,rgba(var(--ink-rgb),.32) 100%)}
/* ============ Magazine chrome:顶部 meta + 底部 foot ============
(v1 布局 + v2 字号在此合并为唯一定义,不要在别处二次定义) */
.chrome{display:flex;justify-content:space-between;align-items:flex-start;font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.2em;text-transform:uppercase;opacity:.62}
.chrome .left,.chrome .right{display:flex;gap:2.4em;align-items:center}
.chrome .sep{width:40px;height:1px;background:currentColor;opacity:.4}
.foot{margin-top:auto;display:flex;justify-content:space-between;align-items:flex-end;font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.5}
.foot .title{font-family:var(--serif-zh);font-weight:400;letter-spacing:.05em;text-transform:none;opacity:.75;font-size:13px}
.tag{display:inline-block;font-family:var(--mono);font-size:11px;letter-spacing:.24em;text-transform:uppercase;padding:6px 14px;border:1px solid currentColor;opacity:.85}
.rule{width:100%;height:1px;background:currentColor;opacity:.25;margin:3vh 0}
.rule.v{width:1px;height:100%;margin:0}
/* ============ 字体规则 ============
· 衬线(Noto Serif SC / Playfair):大标题、重点金句、数字
· 非衬线(Noto Sans SC):正文描述、body、补充说明
· 等宽(IBM Plex Mono):kicker、meta 小标签、foot 右侧
*/
.kicker{font-family:var(--mono);font-size:12px;letter-spacing:.3em;text-transform:uppercase;opacity:.6;margin-bottom:2.6vh}
.display{font-family:var(--serif-en);font-weight:700;font-size:11vw;line-height:.92;letter-spacing:-.025em}
.display-zh{font-family:var(--serif-zh);font-weight:700;font-size:7.8vw;line-height:1.04;letter-spacing:-.005em}
.h1-zh{font-family:var(--serif-zh);font-weight:700;font-size:4.6vw;line-height:1.12;letter-spacing:-.005em}
.h2-zh{font-family:var(--serif-zh);font-weight:600;font-size:3.2vw;line-height:1.2;letter-spacing:0}
.h3-zh{font-family:var(--serif-zh);font-weight:500;font-size:1.9vw;line-height:1.35}
.body-zh{font-family:var(--sans-zh);font-weight:400;font-size:max(15px,1.22vw);line-height:1.75;opacity:.82;letter-spacing:.01em}
.body-serif{font-family:var(--serif-zh);font-weight:400;font-size:max(15px,1.3vw);line-height:1.65;opacity:.88}
.lead{font-family:var(--serif-zh);font-weight:400;font-size:1.75vw;line-height:1.5;opacity:.86}
.meta{font-family:var(--mono);font-size:max(11px,.88vw);letter-spacing:.16em;text-transform:uppercase;opacity:.6}
.big-num{font-family:var(--serif-en);font-weight:800;font-size:10vw;line-height:.85;letter-spacing:-.03em;font-feature-settings:"tnum"}
.mid-num{font-family:var(--serif-en);font-weight:700;font-size:5.5vw;line-height:.88;letter-spacing:-.02em;font-feature-settings:"tnum"}
.ghost{font-family:var(--serif-en);font-weight:900;font-size:34vw;line-height:.8;opacity:.06;letter-spacing:-.04em;position:absolute;font-feature-settings:"tnum"}
em{font-style:italic;font-family:var(--serif-en)}
.en{font-family:var(--serif-en);font-style:italic;font-weight:500}
/* ============ 布局工具 ============ */
.col{display:flex;flex-direction:column;gap:2.4vh}
.row{display:flex;align-items:center;gap:3vw}
.grid-6{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr);gap:4vw 6vw;flex:1;align-content:center;padding:2vh 0}
.grid-9{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);gap:3vh 4vw;flex:1;align-content:center}
.grid-4{display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr);gap:4vh 6vw;flex:1;align-content:center}
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:4vw;flex:1;align-content:center}
.split{display:grid;grid-template-columns:1fr 1fr;gap:4vw;flex:1;align-items:center}
.split-55{display:grid;grid-template-columns:55fr 45fr;gap:5vw;flex:1;align-items:stretch}
.fill{flex:1}
.center{align-items:center;justify-content:center;text-align:center}
.bottom-left{position:absolute;left:6vw;bottom:9vh;max-width:50vw}
.bottom-right{position:absolute;right:6vw;bottom:9vh;max-width:50vw;text-align:right}
.top-right{position:absolute;right:6vw;top:6vh;text-align:right}
/* ============ Stat(数字矩阵) ============ */
.stat{display:flex;flex-direction:column;gap:1vh;align-items:flex-start}
.stat .n{font-family:var(--serif-en);font-weight:800;font-size:8vw;line-height:.88;letter-spacing:-.03em;font-feature-settings:"tnum"}
.stat .l{font-family:var(--sans-zh);font-size:max(13px,1.05vw);opacity:.7;margin-top:1vh;font-weight:400;line-height:1.5}
.stat .m{font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;opacity:.5;margin-bottom:.2vh}
/* ============ Callout(引用框) ============ */
.callout{padding:3vh 2.4vw;border-left:3px solid currentColor;position:relative;font-family:var(--serif-zh);font-size:max(15px,1.2vw);line-height:1.55;opacity:.92}
.slide.light .callout{background:rgba(var(--ink-rgb),.05)}
.slide.dark .callout{background:rgba(var(--paper-rgb),.06)}
.callout .cite{display:block;margin-top:1.6vh;font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;opacity:.6}
.callout .q-big{font-family:var(--serif-zh);font-weight:600;font-size:max(17px,1.6vw);line-height:1.42}
/* ============ Platform(平台卡) ============ */
.plat{display:flex;flex-direction:column;justify-content:flex-end;padding:2vh 0;border-top:1px solid currentColor;border-color:rgba(127,127,127,.35)}
.plat .name{font-family:var(--serif-zh);font-weight:700;font-size:1.8vw;margin-bottom:.6vh}
.plat .nb{font-family:var(--serif-en);font-weight:700;font-size:3.2vw;letter-spacing:-.02em;line-height:1;font-feature-settings:"tnum"}
.plat .sub{font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;opacity:.55;margin-top:.6vh}
.plat .fill{font-family:var(--sans-zh);font-weight:300;font-size:2.4vw;opacity:.28;letter-spacing:-.01em;line-height:1}
/* ============ Rowline(表格行) ============ */
.rowline{display:grid;grid-template-columns:1fr 2fr 1fr;gap:2vw;padding:2.2vh 0;border-top:1px solid currentColor;align-items:center;border-color:rgba(127,127,127,.25)}
.rowline:last-child{border-bottom:1px solid currentColor;border-color:rgba(127,127,127,.25)}
.rowline .k{font-family:var(--serif-zh);font-weight:700;font-size:1.7vw}
.rowline .v{font-family:var(--sans-zh);font-weight:400;font-size:max(14px,1.2vw);opacity:.85;line-height:1.55}
.rowline .m{font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;opacity:.6;justify-self:end}
/* ============ Pillar(支柱卡片) ============ */
.pillar{display:flex;flex-direction:column;gap:1.8vh}
.pillar .ic{font-family:var(--serif-en);font-style:italic;font-size:2.6vw;opacity:.45;font-weight:400}
.pillar .ic svg{width:2.8vw;height:2.8vw;stroke-width:1.2;opacity:.7}
.pillar .t{font-family:var(--serif-zh);font-weight:700;font-size:2.4vw;line-height:1.1}
.pillar .d{font-family:var(--sans-zh);font-weight:400;font-size:max(14px,1.1vw);opacity:.76;line-height:1.6}
/* ============ Signature / Highlight ============ */
.sign{font-family:var(--serif-en);font-style:italic;font-weight:500;font-size:2vw;opacity:.7}
.hi{position:relative;display:inline}
.slide.dark .hi::after{content:"";position:absolute;left:-.1em;right:-.1em;bottom:-.05em;height:.28em;background:rgba(var(--paper-rgb),.15);z-index:-1}
.slide.light .hi::after{content:"";position:absolute;left:-.1em;right:-.1em;bottom:-.05em;height:.28em;background:rgba(var(--ink-rgb),.08);z-index:-1}
/* ============ Icons(Lucide via CDN) ============ */
.ico{width:1em;height:1em;display:inline-block;vertical-align:-.12em;stroke:currentColor;fill:none;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}
.ico-lg,.ico-md,.ico-sm{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round}
.ico-lg{width:2.6vw;height:2.6vw;stroke-width:1.2;display:inline-block}
.ico-md{width:1.8vw;height:1.8vw;stroke-width:1.3;display:inline-block;vertical-align:-.4em}
.ico-sm{width:1.1vw;height:1.1vw;stroke-width:1.4;display:inline-block;vertical-align:-.15em;opacity:.7}
/* ============ 图片占位(虚线框,提示设计师位置) ============ */
.img-slot{border:1.5px dashed rgba(127,127,127,.4);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:1vh;padding:2vh 2vw;font-family:var(--mono);font-size:10px;letter-spacing:.28em;text-transform:uppercase;opacity:.55;position:relative;aspect-ratio:16/9;width:100%;max-height:56vh;margin-inline:auto;box-sizing:border-box}
.img-slot::before{content:"";position:absolute;inset:8px;border:1px solid currentColor;opacity:.2}
.img-slot .plus{font-size:2vw;font-weight:300;opacity:.5;letter-spacing:0}
.img-slot .label{position:relative;z-index:2;text-align:center}
.img-slot.r-4x3{aspect-ratio:4/3}
.img-slot.r-3x2{aspect-ratio:3/2}
.img-slot.r-1x1{aspect-ratio:1/1}
/* ============ 图片实填框(关键:固定高度 + 只裁底部) ============
重要约束:高度用内联 height:Nvh 精确控制,不要用 aspect-ratio(会撑破布局)
object-position:top center 保证严禁裁剪顶部和左右,只裁剪底部
*/
.frame-img{overflow:hidden;position:relative;background:rgba(0,0,0,.04);box-sizing:border-box;width:100%;border-radius:4px}
.slide.dark .frame-img{background:rgba(255,255,255,.04);border-color:rgba(255,255,255,.12)}
.frame-img > img{width:100%;height:100%;object-fit:cover;object-position:top center;display:block}
.frame-img.fit-contain > img{object-fit:contain;object-position:center center}
.frame-img.r-16x9{aspect-ratio:16/9;max-height:64vh}
.frame-img.r-16x10{aspect-ratio:16/10;max-height:56vh}
.frame-img.r-4x3{aspect-ratio:4/3;max-height:56vh}
.frame-img.r-3x2{aspect-ratio:3/2;max-height:46vh}
.frame-img.r-3x4{aspect-ratio:3/4;max-height:60vh}
.frame-img.r-1x1{aspect-ratio:1/1;max-height:50vh}
.frame-img.h-16{height:16vh}
.frame-img.h-18{height:18vh}
.frame-img.h-22{height:22vh}
.frame-img.h-26{height:26vh}
.frame-img.h-28{height:28vh}
.frame-cap{display:flex;justify-content:space-between;align-items:baseline;gap:1vw;margin-top:.8vh;font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;opacity:.72}
.frame-cap .pf{font-family:var(--serif-zh);font-weight:600;font-size:max(13px,1vw);letter-spacing:.04em;text-transform:none;opacity:.94}
.frame-cap .nb{font-family:var(--serif-en);font-style:italic;font-size:max(15px,1.2vw);letter-spacing:.02em;text-transform:none;opacity:.88}
.frame-cap .idx{font-family:var(--mono);opacity:.5}
figure.tile{display:flex;flex-direction:column;margin:0;min-width:0}
figure.tile > .frame-img{flex:0 0 auto}
/* ============ 导航 ============ */
#nav{position:fixed;left:50%;bottom:2.6vh;transform:translateX(-50%);z-index:30;display:flex;gap:10px;padding:8px 14px;border-radius:999px;background:rgba(0,0,0,.18);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
#nav .dot{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,.3);cursor:pointer;transition:all .3s ease;border:0;padding:0}
#nav .dot:hover{background:rgba(255,255,255,.5);transform:scale(1.15)}
#nav .dot.active{background:rgba(255,255,255,.95);width:22px;border-radius:999px}
body.light-bg #nav{background:rgba(255,255,255,.25)}
body.light-bg #nav .dot{background:rgba(var(--ink-rgb),.25)}
body.light-bg #nav .dot.active{background:rgba(var(--ink-rgb),.9)}
#hint{position:fixed;bottom:3vh;right:3vw;z-index:30;display:flex;align-items:center;gap:.65em;font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;opacity:.4;mix-blend-mode:difference;color:#aaa}
#hint button{all:unset;display:inline-flex;align-items:center;gap:.45em;cursor:pointer;color:inherit;white-space:nowrap}
#hint button:hover,#hint button:focus-visible{opacity:1;text-decoration:underline;text-underline-offset:.35em}
#hint .ppt-key{padding:.16em .38em;border:1px solid currentColor;border-radius:3px;letter-spacing:.08em}
#hint .ppt-sep{opacity:.55}
body.low-power #hint{opacity:.72;color:var(--paper);mix-blend-mode:normal}
body.light-bg.low-power #hint{color:var(--ink)}
/* ============ 演讲者模式 ============ */
body.ppt-audience #hint,body.ppt-audience #nav,
body.ppt-preview #hint,body.ppt-preview #nav{display:none!important}
body.ppt-audience{cursor:none}
body.ppt-preview{pointer-events:none}
body.ppt-presenter>#deck,body.ppt-presenter>#nav,body.ppt-presenter>#hint,
body.ppt-presenter>.bg{visibility:hidden}
#ppt-presenter{position:fixed;inset:0;z-index:90;display:none;min-width:0;min-height:0;overflow:hidden;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif}
body.ppt-presenter #ppt-presenter{display:grid;grid-template-rows:auto 1fr auto}
.ppt-presenter-top{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:18px 24px;border-bottom:1px solid #2b2e35;background:#15181d}
.ppt-presenter-brand{font-weight:650;letter-spacing:.02em}.ppt-presenter-meta{display:flex;align-items:center;gap:12px;color:#aeb4be;font-size:14px}
.ppt-sync{display:inline-flex;align-items:center;gap:8px;padding:7px 11px;border:1px solid #363a43;border-radius:999px;color:#d2d6dc;background:#1b1e24}
.ppt-sync::before{content:"";width:8px;height:8px;border-radius:50%;background:#818894}
.ppt-sync[data-state="synced"]::before{background:#72df99;box-shadow:0 0 0 4px rgba(114,223,153,.12)}
.ppt-sync[data-state="connecting"]::before,.ppt-sync[data-state="frozen"]::before{background:#f0c766}.ppt-sync[data-state="unsynced"]::before,.ppt-sync[data-state="blocked"]::before{background:#ff9e64}.ppt-sync[data-state="disconnected"]::before{background:#ff6b72}
.ppt-ui-btn{appearance:none;border:1px solid #3a3e48;background:#20242a;color:#f6f7f8;border-radius:8px;padding:9px 13px;font:600 14px/1 Inter,"Noto Sans SC",sans-serif;cursor:pointer}
.ppt-ui-btn:hover{background:#2a2f37;border-color:#525864}.ppt-ui-btn:focus-visible{outline:2px solid #84a8ff;outline-offset:2px}.ppt-ui-btn:disabled{opacity:.28;cursor:not-allowed}
.ppt-presenter-main{display:grid;grid-template-columns:minmax(0,1.75fr) minmax(300px,.75fr);gap:18px;min-height:0;padding:18px 24px}
.ppt-preview-stack{display:grid;grid-template-rows:minmax(0,1fr) clamp(132px,22vh,210px);gap:16px;min-width:0;min-height:0}
.ppt-stage-card,.ppt-notes-card{background:#171a20;border:1px solid #2b2f37;border-radius:12px;overflow:hidden}
.ppt-stage-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0}
.ppt-stage-label{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:11px 14px;color:#aeb4be;font-size:13px;border-bottom:1px solid #292d34}
.ppt-annotation-tools{display:flex;align-items:center;gap:5px;margin-left:auto}.ppt-annotation-tools .ppt-ui-btn{padding:6px 9px;font-size:12px}.ppt-annotation-tools .ppt-ui-btn.is-active{color:#111318;background:#ff5b63;border-color:#ff5b63}.ppt-annotation-tools .ppt-clear-btn{min-width:0}
.ppt-frame-viewport{position:relative;display:grid;place-items:center;min-width:0;min-height:0;overflow:hidden;background:#08090b}
.ppt-frame{display:block;width:0;height:0;max-width:100%;max-height:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86}
.ppt-annotation-layer{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:4;width:0;height:0;pointer-events:none;touch-action:none}.ppt-annotation-layer.is-active{pointer-events:auto;cursor:crosshair}
#ppt-audience-ink{position:fixed;inset:0;z-index:70;width:100vw;height:100vh;pointer-events:none}#ppt-audience-cover{position:fixed;inset:0;z-index:80;display:none;pointer-events:none}
.ppt-notes-card{display:grid;grid-template-rows:auto 1fr;min-height:0}.ppt-notes-head{padding:16px 18px;border-bottom:1px solid #292d34}.ppt-notes-title{font-size:20px;font-weight:650;margin-bottom:7px}.ppt-notes-sub{display:flex;align-items:center;justify-content:space-between;gap:10px;color:#9fa6b1;font-size:13px}
.ppt-notes-tools{display:flex;align-items:center;gap:6px}.ppt-note-editor{width:100%;height:100%;min-height:0;resize:none;border:0;outline:0;background:#171a20;color:#edf0f3;padding:18px;font:400 var(--ppt-note-size,17px)/1.72 Inter,"Noto Sans SC",sans-serif}
.ppt-saved{color:#72df99}.ppt-presenter-bottom{display:grid;grid-template-columns:minmax(280px,1fr) auto minmax(260px,1fr);align-items:center;gap:20px;padding:14px 24px;border-top:1px solid #2b2e35;background:#15181d}
.ppt-timer{display:flex;align-items:center;gap:9px;min-width:0}.ppt-time-copy{min-width:142px}.ppt-clock{font:600 24px/1 var(--mono,monospace);font-variant-numeric:tabular-nums}.ppt-time-detail{margin-top:5px;color:#9fa6b1;font-size:12px;white-space:nowrap}.ppt-time-detail.is-over{color:#ff8a90}.ppt-rehearsal-on{color:#ff8a90}.ppt-controls{display:flex;align-items:center;gap:8px}.ppt-controls .ppt-ui-btn{min-width:72px}.ppt-controls .ppt-primary{background:#f2f4f6;color:#111318;border-color:#f2f4f6}.ppt-controls .ppt-primary:hover{background:#fff}.ppt-page-state{text-align:right;color:#aeb4be;font-size:14px}.ppt-page-state strong{display:block;color:#fff;font-size:18px;margin-bottom:3px}.ppt-auto-state{display:inline-flex;align-items:center;gap:6px;margin-top:7px}.ppt-auto-state.is-on{color:#72df99}.ppt-auto-state.is-paused{color:#f0c766}
#ppt-presenter[data-layout="slides"] .ppt-presenter-main{grid-template-columns:minmax(0,2.15fr) minmax(260px,.58fr)}#ppt-presenter[data-layout="notes"] .ppt-presenter-main{grid-template-columns:minmax(0,1.15fr) minmax(380px,1fr)}
.ppt-dialog{position:fixed;inset:0;z-index:130;display:grid;place-items:center;padding:24px;background:rgba(4,6,9,.72);backdrop-filter:blur(10px)}.ppt-dialog[hidden]{display:none}.ppt-dialog-panel{width:min(720px,calc(100vw - 48px));max-height:min(760px,calc(100vh - 48px));display:grid;grid-template-rows:auto minmax(0,1fr);overflow:hidden;border:1px solid #343943;border-radius:14px;background:#171a20;box-shadow:0 24px 80px rgba(0,0,0,.45)}.ppt-dialog-head{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 18px;border-bottom:1px solid #2b2f37}.ppt-dialog-title{font-size:20px;font-weight:700}.ppt-dialog-body{overflow:auto;padding:18px;color:#d8dce2;font-size:14px;line-height:1.6}.ppt-dialog-section+.ppt-dialog-section{margin-top:22px}.ppt-dialog-section h3{margin:0 0 10px;color:#fff;font-size:15px}.ppt-dialog-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.ppt-dialog-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid #282c33}.ppt-dialog-row:last-child{border-bottom:0}.ppt-field{display:grid;gap:6px}.ppt-field label{color:#aeb4be;font-size:12px}.ppt-field input{width:100%;border:1px solid #3a3e48;border-radius:8px;background:#101216;color:#fff;padding:9px 10px;font:600 14px Inter,sans-serif}.ppt-check{display:flex;align-items:flex-start;gap:10px}.ppt-check-dot{width:9px;height:9px;margin-top:7px;border-radius:50%;background:#ff6b72}.ppt-check.is-ok .ppt-check-dot{background:#72df99}.ppt-check.is-warn .ppt-check-dot{background:#f0c766}.ppt-summary{width:100%;border-collapse:collapse;font-size:13px}.ppt-summary th,.ppt-summary td{padding:9px 8px;border-bottom:1px solid #2b2f37;text-align:left}.ppt-summary th{color:#9fa6b1;font-weight:600}.ppt-summary td:last-child,.ppt-summary th:last-child{text-align:right}.ppt-muted{color:#8f96a1}.ppt-shortcuts{display:grid;grid-template-columns:auto 1fr;gap:8px 14px}.ppt-shortcuts kbd{min-width:44px;padding:4px 7px;border:1px solid #3a3e48;border-radius:6px;background:#101216;color:#fff;text-align:center;font:600 12px var(--mono,monospace)}
#overview{z-index:110!important}
@media(max-width:1200px){.ppt-presenter-main{grid-template-columns:minmax(0,1.55fr) minmax(280px,.72fr)}}
@media(max-width:900px){.ppt-presenter-top{padding:12px 16px;flex-wrap:wrap}.ppt-presenter-top .ppt-presenter-meta{gap:6px}.ppt-presenter-top .ppt-ui-btn{padding:8px 9px}.ppt-presenter-main,#ppt-presenter[data-layout="slides"] .ppt-presenter-main,#ppt-presenter[data-layout="notes"] .ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(250px,.72fr);gap:12px;padding:12px 16px}.ppt-preview-stack{gap:12px;grid-template-rows:minmax(0,1fr) clamp(104px,20vh,150px)}.ppt-presenter-bottom{grid-template-columns:1fr auto;gap:8px;padding:10px 16px}.ppt-time-copy{min-width:112px}.ppt-time-detail{white-space:normal}.ppt-timer .ppt-ui-btn:nth-of-type(2){display:none}.ppt-controls{gap:5px}.ppt-controls .ppt-ui-btn{min-width:58px;padding:8px 10px}.ppt-page-state{display:none}.ppt-dialog-grid{grid-template-columns:1fr}}
@media(max-height:700px){.ppt-presenter-top{padding-top:11px;padding-bottom:11px}.ppt-presenter-main{padding-top:12px;padding-bottom:12px}.ppt-preview-stack{grid-template-rows:minmax(0,1fr) clamp(96px,18vh,126px);gap:12px}.ppt-stage-label{padding:8px 11px}.ppt-notes-head{padding:12px 14px}.ppt-presenter-bottom{padding-top:10px;padding-bottom:10px}}
/* ============================================================
============ LAYOUTS API · 面向 agent 的类(v2)============
所有 layouts.md 中的骨架都基于下面这套命名。
如果你在 layouts.md 里看到某个类,它必须在下面有定义。
============================================================ */
/* ---------- .frame:每页主内容容器 ---------- */
.frame{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}
/* 当 .frame 同时加了 grid 类时,grid 的 display:grid 覆盖 flex */
.frame.grid-2-7-5,
.frame.grid-2-6-6,
.frame.grid-2-8-4,
.frame.grid-3-3,
.frame.grid-6{display:grid}
/* ---------- 标题层级(API 名称,衬线为主) ---------- */
.h-hero{
font-family:var(--serif-zh);
font-weight:900;
font-size:10vw;
line-height:.96;
letter-spacing:-.02em;
}
.h-xl{
font-family:var(--serif-zh);
font-weight:700;
font-size:6.2vw;
line-height:1.08;
letter-spacing:-.01em;
}
.h-sub{
font-family:var(--serif-zh);
font-weight:500;
font-size:3.1vw;
line-height:1.25;
letter-spacing:0;
opacity:.7;
}
.h-md{
font-family:var(--serif-zh);
font-weight:600;
font-size:2.3vw;
line-height:1.3;
}
/* 英文标题专用(Playfair 衬线) */
.h-hero-en,.h-xl-en{font-family:var(--serif-en);letter-spacing:-.025em}
/* ---------- lead 引语:定义见上方“字体规则”区块(已合并,勿重复定义) ---------- */
/* ---------- meta-row 底部元数据 ---------- */
.meta-row{
display:flex;
gap:1.2em;
align-items:baseline;
flex-wrap:wrap;
font-family:var(--mono);
font-size:max(12px,.92vw);
letter-spacing:.16em;
text-transform:uppercase;
opacity:.6;
}
/* ---------- stat-card(数据大字报用) ---------- */
.stat-card{
display:flex;
flex-direction:column;
gap:.8vh;
align-items:flex-start;
padding-top:1.6vh;
border-top:1px solid currentColor;
border-color:rgba(127,127,127,.3);
}
.stat-card .stat-label{
font-family:var(--mono);
font-size:max(10px,.78vw);
letter-spacing:.24em;
text-transform:uppercase;
opacity:.55;
}
.stat-card .stat-nb{
font-family:var(--serif-en);
font-weight:800;
font-size:5.8vw;
line-height:.9;
letter-spacing:-.03em;
font-feature-settings:"tnum";
margin-top:.4vh;
}
.stat-card .stat-nb .stat-unit{
font-family:var(--serif-zh);
font-weight:500;
font-size:.38em;
letter-spacing:0;
opacity:.72;
margin-left:.14em;
}
.stat-card .stat-note{
font-family:var(--sans-zh);
font-weight:400;
font-size:max(13px,1.05vw);
line-height:1.5;
opacity:.72;
margin-top:.6vh;
}
/* 当 stat-card 用于 grid-4(2x2),数字适度放大;若页面有标题+引文在上方,可内联 style 覆盖为更小值 */
.grid-4 .stat-card .stat-nb{font-size:5vw}
/* 当只有 3 个,字也可以稍大 */
.grid-3 .stat-card .stat-nb{font-size:6.8vw}
/* ---------- pipeline(流水线) ---------- */
.pipeline-section{
margin-top:4.4vh;
padding-top:2.8vh;
border-top:1px dashed rgba(127,127,127,.32);
}
.pipeline-section:first-of-type{
border-top:0;
padding-top:0;
margin-top:3vh;
}
.pipeline-label{
font-family:var(--mono);
font-size:max(11px,.85vw);
letter-spacing:.24em;
text-transform:uppercase;
opacity:.62;
margin-bottom:2.2vh;
}
.pipeline{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:1.2vw;
}
.pipeline[data-cols="3"]{grid-template-columns:repeat(3,1fr)}
.pipeline[data-cols="4"]{grid-template-columns:repeat(4,1fr)}
.pipeline[data-cols="6"]{grid-template-columns:repeat(6,1fr)}
.step{
display:flex;
flex-direction:column;
gap:.8vh;
padding-top:1.4vh;
border-top:1px solid currentColor;
border-color:rgba(127,127,127,.35);
}
.step-nb{
font-family:var(--serif-en);
font-style:italic;
font-weight:500;
font-size:1.15vw;
opacity:.45;
}
.step-title{
font-family:var(--sans-zh);
font-weight:700;
font-size:1.55vw;
letter-spacing:.01em;
line-height:1.2;
}
.step-desc{
font-family:var(--sans-zh);
font-weight:400;
font-size:max(12px,.95vw);
line-height:1.45;
opacity:.72;
}
/* ---------- 网格(layouts.md 所用) ---------- */
/* 这些类独立挂到任何容器上都能生效,不依赖 .frame 复合选择器 */
.grid-2-7-5{display:grid;grid-template-columns:7fr 5fr;gap:3vw 4vh;align-items:start}
.grid-2-6-6{display:grid;grid-template-columns:1fr 1fr;gap:3vw 4vh;align-items:start}
.grid-2-8-4{display:grid;grid-template-columns:8fr 4fr;gap:3vw 4vh;align-items:start}
.grid-3-3{
display:grid;
grid-template-columns:repeat(3,1fr);
grid-auto-rows:minmax(0,1fr);
gap:2.4vh 2vw;
}
/* grid-6 已在旧样式里定义为 3x2,这里仅补 align */
/* ---------- 图片 frame-img(layouts.md 主命名) ---------- */
/* 在旧样式里已定义,这里补 img-cap 命名别名与增强 */
figure.frame-img{margin:0;display:flex;flex-direction:column;min-width:0}
/* 有图注时:图片弹性收缩,图注收进固定高度/比例框内部,
否则 img 的 height:100% 会把 figcaption 挤出 overflow:hidden 裁掉 */
figure.frame-img:has(> .img-cap) > img{flex:1 1 auto;min-height:0;height:auto}
.img-cap{
display:block;
flex:0 0 auto;
padding:.7vh .9em .9vh;
font-family:var(--mono);
font-size:max(10px,.8vw);
letter-spacing:.22em;
text-transform:uppercase;
opacity:.6;
}
/* callout src 命名别名 */
.callout-src{
display:block;
margin-top:1.6vh;
font-family:var(--mono);
font-size:11px;
letter-spacing:.2em;
text-transform:uppercase;
opacity:.6;
}
/* ---------- chrome & foot:定义见上方“Magazine chrome”区块(已合并,勿重复定义) ---------- */
/* ============ 动效系统(Motion One 驱动) ============
所有 [data-anim] 元素默认隐藏,进入页面时由 JS 逐个揭示。
- cascade(默认):按 DOM 顺序 stagger 120ms 淡入 + y:16→0
- hero(hero 页自动):慢一点的 stagger 180ms
- quote(含 [data-anim="line"]):逐行 600ms 淡入
- directional(含 [data-anim="left|right"]):左→分隔线→右
- pipeline(data-animate="pipeline"):Space/→ 手动推进
Motion One 没加载成功时(CDN 断 + 本地丢),所有 [data-anim] 会兜底显示。
*/
[data-anim]{opacity:1}
body.motion-ready [data-anim]{opacity:0}
body.motion-ready [data-anim="left"]{transform:translateX(-24px)}
body.motion-ready [data-anim="right"]{transform:translateX(24px)}
body.motion-ready [data-anim="line"]{opacity:0;transform:translateY(10px)}
body.motion-ready [data-animate="pipeline"] [data-anim]{opacity:.15}
body.low-power #deck{transition:none!important}
body.low-power *,
body.low-power *::before,
body.low-power *::after{animation:none!important;transition:none!important}
body.low-power.motion-ready [data-anim],
body.low-power [data-anim]{opacity:1!important;transform:none!important}
/* ESC 索引缩略图:克隆的 slide 不经过动效播放,强制显示所有待动画元素 */
#overview [data-anim]{opacity:1!important;transform:none!important}
/* ---------- 响应式降级 ---------- */
@media (max-width:900px){
.display{font-size:16vw}
.display-zh{font-size:12vw}
.h1-zh{font-size:7vw}
.h-hero{font-size:14vw}
.h-xl{font-size:9vw}
.pipeline{grid-template-columns:repeat(2,1fr)}
.grid-2-7-5,.grid-2-6-6,.grid-2-8-4{grid-template-columns:1fr}
}
</style>
</head>
<body>
<script>
(function(){
const KEY = 'guizang-ppt-low-power';
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
const stored = localStorage.getItem(KEY);
window.__lowPowerMode = stored === '1' || (stored === null && reduced);
function updateHint(){
const label = document.querySelector('.ppt-low-power-label');
if(label) label.textContent = window.__lowPowerMode ? '动态' : '静态';
}
window.__setLowPowerMode = function(on, opts={}){
window.__lowPowerMode = !!on;
document.body.classList.toggle('low-power', window.__lowPowerMode);
if(opts.persist !== false) localStorage.setItem(KEY, window.__lowPowerMode ? '1' : '0');
if(window.__lowPowerMode && document.getAnimations){
document.getAnimations().forEach(a=>a.cancel());
}
updateHint();
dispatchEvent(new CustomEvent('ppt-low-power-change', {detail:{on:window.__lowPowerMode}}));
if(window.__playSlide) window.__playSlide(window.__currentSlideIndex || 0);
};
document.body.classList.toggle('low-power', window.__lowPowerMode);
addEventListener('DOMContentLoaded', updateHint, {once:true});
})();
</script>
<canvas id="bg-dark" class="bg"></canvas>
<canvas id="bg-light" class="bg"></canvas>
<div id="hint" role="toolbar" aria-label="快捷控制">
<span>← → 翻页</span><span class="ppt-sep">·</span>
<button id="ppt-low-power-btn" type="button"><span class="ppt-key">B</span><span class="ppt-low-power-label">静态</span></button><span class="ppt-sep">·</span>
<button id="ppt-overview-btn" type="button"><span class="ppt-key">ESC</span>总览</button><span class="ppt-sep">·</span>
<button id="ppt-presenter-btn" type="button"><span class="ppt-key">P</span>演讲模式</button>
</div>
<div id="deck">
<!-- ============================================================
SLIDES 插入区 · 在此处填充所有 <section class="slide ..."> 页面
每页模板参考 references/page-patterns.md
页面组件参考 references/components.md
============================================================ -->
<!-- SLIDES_HERE -->
</div>
<div id="nav"></div>
<!-- 演讲备注:每页一条,id 必须与 section[data-slide-id] 一致。见 references/presenter-mode.md -->
<script>
const SPEAKER_NOTES = [
/* {
id: 'stable-slide-id', title: '页面标题', section: '主体', minutes: 1,
purpose: '这一页要让观众理解什么',
talk: ['只在台上说的补充信息', '例子、证据或语气提示', '不复述页面可见文字'],
transition: '为什么下一页紧接着出现', cue: '可选:停顿 / Demo / 视频',
interaction: '可选:现场提问', delivery: '可选:语气 / 重音',
advance: '可选:翻页时机', fallback: '可选:Demo 失败时的备用说法'
} */
];
window.__SPEAKER_NOTES__ = SPEAKER_NOTES;
</script>
<script>
/* =============== WebGL 双背景 ===============
深色页:Holographic Dispersion(全息色散 · 钛金暗流)—— 彩虹微扰、鼠标径向涟漪
浅色页:Spiral Vortex(旋转涡流 · 银色珍珠)—— domain-warp 流动、无中心
修改风格请参考 references/webgl-backgrounds.md
*/
const VS = `attribute vec2 position;void main(){gl_Position=vec4(position,0.0,1.0);}`;
const FS_DARK = `precision highp float;
uniform vec2 u_resolution;uniform float u_time;uniform vec2 u_mouse;
vec3 palette(float t,vec3 a,vec3 b,vec3 c,vec3 d){return a+b*cos(6.28318*(c*t+d));}
void main(){
vec2 uv=gl_FragCoord.xy/u_resolution.xy;
vec2 p=uv*2.0-1.0;p.x*=u_resolution.x/u_resolution.y;
vec2 m=u_mouse*2.0-1.0;m.x*=u_resolution.x/u_resolution.y;
float md=length(p-m);
float mr=sin(md*15.0-u_time*4.0)*exp(-md*3.0);p+=mr*0.08;
vec2 p0=p;
for(float i=1.0;i<4.0;i++){
p.x+=0.1/i*sin(i*3.0*p.y+u_time*0.4)+0.05;
p.y+=0.1/i*cos(i*2.0*p.x+u_time*0.3)-0.05;
}
float r=length(p);float ang=atan(p.y,p.x);
vec3 a=vec3(0.12,0.12,0.13);
vec3 b=vec3(0.03,0.04,0.05);
vec3 c=vec3(1.0,1.0,1.0);
vec3 d=vec3(0.1,0.2,0.4);
vec3 col=palette(r*1.5+p0.x*0.5+u_time*0.1,a,b,c,d);
float disp=sin(r*25.0-u_time*1.5+ang*2.0)*0.5+0.5;
col+=vec3(disp*0.015,disp*0.01,disp*0.02);
float hi=pow(sin(p.x*4.0+p.y*3.0+u_time)*0.5+0.5,8.0);
col+=hi*0.08;
vec3 base=vec3(0.05,0.05,0.06);
col=mix(base,col,0.85);
gl_FragColor=vec4(col,1.0);
}`;
const FS_LIGHT = `precision highp float;
uniform vec2 u_resolution;uniform float u_time;uniform vec2 u_mouse;
float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
float noise(vec2 p){
vec2 i=floor(p),f=fract(p);
float a=hash(i),b=hash(i+vec2(1,0));
float c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1));
vec2 u=f*f*(3.0-2.0*f);
return mix(a,b,u.x)+(c-a)*u.y*(1.0-u.x)+(d-b)*u.x*u.y;
}
float fbm(vec2 p){
float v=0.0,a=0.5;
mat2 m=mat2(0.80,0.60,-0.60,0.80);
for(int i=0;i<5;i++){v+=a*noise(p);p=m*p*2.02;a*=0.5;}
return v;
}
void main(){
vec2 uv=gl_FragCoord.xy/u_resolution.xy;
vec2 p=uv;p.x*=u_resolution.x/u_resolution.y;
vec2 m=u_mouse;m.x*=u_resolution.x/u_resolution.y;
vec2 md=p-m;float dl=length(md);
p+=normalize(md+vec2(0.0001))*exp(-dl*5.0)*0.03;
vec2 q=vec2(fbm(p*1.8+u_time*0.07),fbm(p*1.8+vec2(5.2,1.3)+u_time*0.06));
vec2 r=vec2(fbm(p*2.0+q*1.3+vec2(1.7,9.2)+u_time*0.05),
fbm(p*2.0+q*1.3+vec2(8.3,2.8)+u_time*0.04));
float f=fbm(p*2.2+r*1.5);
vec3 silverDark=vec3(0.86,0.85,0.84);
vec3 paper=vec3(0.955,0.945,0.925);
vec3 col=mix(silverDark,paper,f);
float ph=r.x*2.2+u_time*0.35;
col+=vec3(0.78,0.62,0.92)*sin(ph)*0.055;
col+=vec3(0.55,0.72,0.95)*sin(ph*0.8+2.0)*0.05;
float hl=smoothstep(0.48,0.92,f);
col+=hl*0.06;
gl_FragColor=vec4(col,1.0);
}`;
const mouse={x:0.5,y:0.5};
addEventListener('mousemove',e=>{mouse.x=e.clientX/innerWidth;mouse.y=e.clientY/innerHeight});
function bootGL(canvasId, fsSrc){
const canvas=document.getElementById(canvasId);
const gl=canvas.getContext('webgl',{alpha:false,antialias:true});
if(!gl) return ()=>false;
const mk=(t,s)=>{const sh=gl.createShader(t);gl.shaderSource(sh,s);gl.compileShader(sh);return sh};
const prog=gl.createProgram();
gl.attachShader(prog,mk(gl.VERTEX_SHADER,VS));
gl.attachShader(prog,mk(gl.FRAGMENT_SHADER,fsSrc));
gl.linkProgram(prog);gl.useProgram(prog);
const buf=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buf);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW);
const pos=gl.getAttribLocation(prog,'position');
gl.enableVertexAttribArray(pos);gl.vertexAttribPointer(pos,2,gl.FLOAT,false,0,0);
const lRes=gl.getUniformLocation(prog,'u_resolution');
const lT=gl.getUniformLocation(prog,'u_time');
const lM=gl.getUniformLocation(prog,'u_mouse');
const resize=()=>{
const d=Math.min(window.devicePixelRatio||1,2);
canvas.width=innerWidth*d;canvas.height=innerHeight*d;
gl.viewport(0,0,canvas.width,canvas.height);
};
addEventListener('resize',resize);resize();
return (tSec)=>{
gl.uniform2f(lRes,canvas.width,canvas.height);
gl.uniform1f(lT,tSec);
gl.uniform2f(lM,mouse.x,1-mouse.y);
gl.drawArrays(gl.TRIANGLES,0,6);
return true;
};
}
let drawDark=null, drawLight=null, glRAF=0, glT0=Date.now();
function startGL(){
if(window.__lowPowerMode || glRAF) return;
if(!drawDark) drawDark=bootGL('bg-dark',FS_DARK);
if(!drawLight) drawLight=bootGL('bg-light',FS_LIGHT);
glT0=Date.now();
function loop(){
if(window.__lowPowerMode){glRAF=0;return;}
const t=(Date.now()-glT0)/1000;
drawDark(t);drawLight(t);
glRAF=requestAnimationFrame(loop);
}
glRAF=requestAnimationFrame(loop);
}
function stopGL(){
if(glRAF) cancelAnimationFrame(glRAF);
glRAF=0;
}
startGL();
addEventListener('ppt-low-power-change', e=>{e.detail.on ? stopGL() : startGL();});
// =============== 导航(翻页 / 圆点 / 键盘 / 滚轮 / 触屏) ===============
const deck=document.getElementById('deck');
const slides=deck.querySelectorAll('.slide');
const nav=document.getElementById('nav');
let idx=0,total=slides.length,lock=false;
// 关键:矫正 deck 宽度为 total * 100vw,否则翻页会错位
deck.style.width=(total*100)+'vw';
slides.forEach((s,i)=>{
const b=document.createElement('button');
b.className='dot';b.dataset.i=i;b.setAttribute('aria-label','Page '+(i+1));
b.onclick=()=>go(i);
nav.appendChild(b);
});
function go(n,opts={}){
if(lock&&!opts.force)return;
idx=Math.max(0,Math.min(total-1,n));
window.__currentSlideIndex = idx;
deck.style.transform=`translateX(${-idx*100}vw)`;
nav.querySelectorAll('.dot').forEach((d,i)=>d.classList.toggle('active',i===idx));
/* 主题切换:优先读 data-theme,其次从 class(light/dark)推断 */
const el=slides[idx];
const th=el.dataset.theme || (el.classList.contains('light')?'light':(el.classList.contains('dark')?'dark':'dark'));
document.body.classList.toggle('light-bg',th==='light');
/* 动效:翻页过渡中段触发当前页的入场动画(由 motion-boot 注册) */
if(window.__playSlide) setTimeout(()=>window.__playSlide(idx), 450);
window.__pptPresenter?.onSlideChange(idx);
if(!opts.force){lock=true;setTimeout(()=>lock=false,700);}
}
/* =============== ESC 索引视图 =============== */
let overviewOn=false;
const ov=document.createElement('div');
ov.id='overview';
ov.style.cssText='position:fixed;inset:0;z-index:100;background:rgba(var(--ink-rgb),.92);backdrop-filter:blur(12px);display:none;overflow-y:auto;padding:4vh 4vw';
document.body.appendChild(ov);
function buildOverview(){
ov.innerHTML='';
const grid=document.createElement('div');
grid.style.cssText='display:grid;grid-template-columns:repeat(4,1fr);gap:2vh 1.6vw;max-width:90vw;margin:0 auto';
slides.forEach((s,i)=>{
const card=document.createElement('div');
card.style.cssText='cursor:pointer;border-radius:6px;overflow:hidden;border:2px solid '+(i===idx?'rgba(var(--paper-rgb),.8)':'rgba(var(--paper-rgb),.15)')+';transition:border-color .2s';
card.onmouseenter=()=>card.style.borderColor='rgba(var(--paper-rgb),.6)';
card.onmouseleave=()=>card.style.borderColor=i===idx?'rgba(var(--paper-rgb),.8)':'rgba(var(--paper-rgb),.15)';
const wrap=document.createElement('div');
wrap.style.cssText='width:100%;aspect-ratio:16/9;overflow:hidden;position:relative;pointer-events:none;background:'+(s.classList.contains('light')?'var(--paper)':'var(--ink)');
const clone=s.cloneNode(true);
clone.style.cssText='width:100vw;height:100vh;transform:scale('+(1/4.5)+');transform-origin:top left;position:absolute;top:0;left:0;pointer-events:none';
wrap.appendChild(clone);
const label=document.createElement('div');
label.style.cssText='padding:6px 10px;font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--paper);opacity:.7';
label.textContent=window.__pptPresenter?.slideLabel(i) || ((i+1)+' / '+total);
card.appendChild(wrap);
card.appendChild(label);
card.onclick=()=>{toggleOverview();go(i)};
grid.appendChild(card);
});
ov.appendChild(grid);
}
function toggleOverview(){
overviewOn=!overviewOn;
if(overviewOn){buildOverview();ov.style.display='block';}
else{ov.style.display='none';}
}
/* =============== 演讲者模式 / 观众屏同步 =============== */
window.__pptPresenter=(()=>{
const query=new URLSearchParams(location.search);
const requestedMode=query.get('mode')||'deck';
const originalTitle=document.title;
const notes=()=>Array.isArray(window.__SPEAKER_NOTES__)?window.__SPEAKER_NOTES__:[];
const slideId=i=>slides[i]?.dataset.slideId||`slide-${String(i+1).padStart(2,'0')}`;
const storageKey=name=>`guizang-ppt-${name}:${location.pathname}`;
let mode='deck',session=query.get('session')||Math.random().toString(36).slice(2),audienceWindow=null;
let channel=null,transportReady=false,popupBlocked=false,audienceOpenedAt=0,lastAckAt=0,lastAckIndex=-1,lastAckSeq=-1,seq=0,lastPingAt=0;
let renderedNoteId='',saveTimer=0,noteSize=Number(localStorage.getItem('guizang-ppt-note-size')||17),presenterFrameObserver=null;
let elapsed=0,timerRunning=false,timerStartedAt=0,timedSlideIndex=idx,slideTimerStartedAt=0,slideElapsed=Array(total).fill(0);
let rehearsalActive=false,lowPowerBeforePresenter=window.__lowPowerMode;
let layoutMode=localStorage.getItem(storageKey('layout'))||'balanced';
let autoEnabled=localStorage.getItem(storageKey('auto-enabled'))==='1';
let autoGlobalSeconds=Math.max(0,Number(localStorage.getItem(storageKey('auto-seconds'))||0));
let autoDeadline=0,autoRemainingMs=0;
let screenMode='normal',audienceFrozen=false;
let annotationTool='none',annotationPointerDown=false,annotationStart=null,draftCircle=null,circles=[],audienceCircles=[];
let laserPoint=null,audienceLaser=null,laserHideAt=0,audienceLaserHideAt=0,lastLaserSent=0;
const seenNonces=[],seenNonceSet=new Set();
const syncKey=()=>`guizang-ppt-sync:${location.pathname}:${session}`;
const channelName=()=>`guizang-ppt:${location.pathname}:${session}`;
function noteAt(i){
const id=slideId(i);
return notes().find(n=>n.id===id)||{id,title:slides[i]?.dataset.title||`第 ${i+1} 页`,purpose:'待补充本页目的',talk:['待补充讲述要点'],transition:'待补充转场'};
}
function slideLabel(i){return `${i+1} / ${total} · ${noteAt(i).title||slideId(i)}`;}
function escapeHTML(value){return String(value??'').replace(/[&<>"']/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));}
function valueLines(value){
if(Array.isArray(value))return value.filter(Boolean).map(x=>`• ${x}`).join('\n');
return value?String(value):'';
}
function generatedNote(n){
const parts=[];
if(n.purpose)parts.push(`【本页目的】\n${n.purpose}`);
if(Array.isArray(n.talk)&&n.talk.length)parts.push(`【讲述要点】\n${valueLines(n.talk)}`);
if(n.transition)parts.push(`【转场】\n${n.transition}`);
const optional=[['舞台提示',n.cue],['互动',n.interaction],['表达提示',n.delivery],['翻页时机',n.advance],['备用方案',n.fallback],['读音提醒',n.pronunciation]];
optional.forEach(([label,value])=>{const text=valueLines(value);if(text)parts.push(`【${label}】\n${text}`);});
return parts.join('\n\n');
}
function noteStorageKey(id){return `guizang-ppt-note:${location.pathname}:${id}`;}
function loadNoteValue(id,i,n){
const saved=localStorage.getItem(noteStorageKey(id));
if(saved!==null)return saved;
const prefix=window.__PPT_LEGACY_NOTE_KEY__;
if(prefix){
const legacy=localStorage.getItem(`${prefix}:${i}`);
if(legacy!==null){localStorage.setItem(noteStorageKey(id),legacy);return legacy;}
}
return generatedNote(n);
}
function frameUrl(targetMode,i){
const url=new URL(location.href);url.hash='';
url.searchParams.set('mode',targetMode);url.searchParams.set('session',session);url.searchParams.set('slide',String(i+1));
return url.href;
}
function plannedMsAt(i){
const minutes=Number(noteAt(i).minutes);
return Number.isFinite(minutes)&&minutes>0?minutes*60000:null;
}
function totalPlannedMs(){
const values=Array.from({length:total},(_,i)=>plannedMsAt(i));
return values.length&&values.every(Number.isFinite)?values.reduce((sum,x)=>sum+x,0):null;
}
function sectionInfo(i){
const name=String(noteAt(i).section||'').trim();
if(!name)return null;
let first=i,last=i;
while(first>0&&String(noteAt(first-1).section||'').trim()===name)first--;
while(last<total-1&&String(noteAt(last+1).section||'').trim()===name)last++;
const targets=[];for(let x=first;x<=last;x++)targets.push(plannedMsAt(x));
const planned=targets.every(Number.isFinite)?targets.reduce((sum,x)=>sum+x,0):null;
const used=slideElapsed.slice(first,last+1).reduce((sum,x)=>sum+x,0)+(timerRunning&&timedSlideIndex>=first&&timedSlideIndex<=last?Date.now()-slideTimerStartedAt:0);
return{name,position:i-first+1,count:last-first+1,planned,used};
}
function durationText(ms){
const value=Math.max(0,Math.round(Number(ms)||0)),seconds=Math.floor(value/1000);
const hours=Math.floor(seconds/3600),minutes=Math.floor((seconds%3600)/60),secs=seconds%60;
return hours?`${String(hours).padStart(2,'0')}:${String(minutes).padStart(2,'0')}:${String(secs).padStart(2,'0')}`:`${String(minutes).padStart(2,'0')}:${String(secs).padStart(2,'0')}`;
}
function signedDuration(ms){return `${ms>=0?'+':'−'}${durationText(Math.abs(ms))}`;}
function clockTime(timestamp){return new Date(timestamp).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});}
function fitFrame(frame){
const viewport=frame?.closest('.ppt-frame-viewport');
if(!viewport)return;
const availableWidth=viewport.clientWidth,availableHeight=viewport.clientHeight;
if(!availableWidth||!availableHeight)return;
const width=Math.min(availableWidth,availableHeight*(16/9)),height=width*(9/16);
frame.style.width=`${width}px`;frame.style.height=`${height}px`;
if(frame.id==='ppt-current-frame')sizeInk(document.getElementById('ppt-presenter-ink'),width,height);
}
function fitPresenterFrames(){
fitFrame(document.getElementById('ppt-current-frame'));
fitFrame(document.getElementById('ppt-next-frame'));
}
function sizeInk(canvas,width,height){
if(!canvas||!width||!height)return;
canvas.style.width=`${width}px`;canvas.style.height=`${height}px`;
const dpr=Math.min(devicePixelRatio||1,2),pixelWidth=Math.round(width*dpr),pixelHeight=Math.round(height*dpr);
if(canvas.width!==pixelWidth||canvas.height!==pixelHeight){canvas.width=pixelWidth;canvas.height=pixelHeight;}
}
function drawEllipse(ctx,circle,width,height,alpha=1){
const x1=circle.x1*width,y1=circle.y1*height,x2=circle.x2*width,y2=circle.y2*height;
ctx.save();ctx.globalAlpha=alpha;ctx.strokeStyle='#ff4f59';ctx.lineWidth=Math.max(3,width*.004);ctx.lineCap='round';
ctx.beginPath();ctx.ellipse((x1+x2)/2,(y1+y2)/2,Math.abs(x2-x1)/2,Math.abs(y2-y1)/2,0,0,Math.PI*2);ctx.stroke();ctx.restore();
}
function drawInk(canvas,list,laser,draft){
if(!canvas)return;const width=canvas.clientWidth,height=canvas.clientHeight;if(!width||!height)return;
sizeInk(canvas,width,height);const dpr=canvas.width/width,ctx=canvas.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,width,height);
(list||[]).forEach(circle=>drawEllipse(ctx,circle,width,height));
if(draft)drawEllipse(ctx,draft,width,height,.72);
if(laser){
const x=laser.x*width,y=laser.y*height,r=Math.max(6,width*.007);
const glow=ctx.createRadialGradient(x,y,0,x,y,r*2.8);glow.addColorStop(0,'rgba(255,255,255,.98)');glow.addColorStop(.2,'rgba(255,64,72,.98)');glow.addColorStop(1,'rgba(255,64,72,0)');
ctx.fillStyle=glow;ctx.beginPath();ctx.arc(x,y,r*2.8,0,Math.PI*2);ctx.fill();
}
}
function renderInks(){
const now=Date.now();if(now>laserHideAt)laserPoint=null;if(now>audienceLaserHideAt)audienceLaser=null;
drawInk(document.getElementById('ppt-presenter-ink'),circles,laserPoint,draftCircle);
drawInk(document.getElementById('ppt-audience-ink'),audienceCircles,audienceLaser,null);
}
function setAnnotationTool(tool){
annotationTool=annotationTool===tool?'none':tool;annotationPointerDown=false;draftCircle=null;
const canvas=document.getElementById('ppt-presenter-ink');canvas?.classList.toggle('is-active',annotationTool!=='none');
document.querySelectorAll('[data-ppt-tool]').forEach(button=>button.classList.toggle('is-active',button.dataset.pptTool===annotationTool));
renderInks();
}
function annotationPoint(event){
const canvas=event.currentTarget,rect=canvas.getBoundingClientRect();
return{x:Math.max(0,Math.min(1,(event.clientX-rect.left)/rect.width)),y:Math.max(0,Math.min(1,(event.clientY-rect.top)/rect.height))};
}
function onAnnotationPointerDown(event){
if(annotationTool!=='circle')return;event.preventDefault();annotationPointerDown=true;annotationStart=annotationPoint(event);draftCircle={x1:annotationStart.x,y1:annotationStart.y,x2:annotationStart.x,y2:annotationStart.y};event.currentTarget.setPointerCapture?.(event.pointerId);renderInks();
}
function onAnnotationPointerMove(event){
if(annotationTool==='laser'){
const point=annotationPoint(event);laserPoint=point;laserHideAt=Date.now()+650;
if(Date.now()-lastLaserSent>32){lastLaserSent=Date.now();emit({type:'laser',index:idx,point},{persist:false});}renderInks();return;
}
if(annotationTool==='circle'&&annotationPointerDown&&draftCircle){const point=annotationPoint(event);draftCircle.x2=point.x;draftCircle.y2=point.y;renderInks();}
}
function onAnnotationPointerUp(event){
if(annotationTool!=='circle'||!annotationPointerDown||!draftCircle)return;event.preventDefault();annotationPointerDown=false;
const circle={...draftCircle};draftCircle=null;
if(Math.abs(circle.x2-circle.x1)>.012&&Math.abs(circle.y2-circle.y1)>.012){circles.push(circle);emit({type:'circle',index:idx,circle});}
renderInks();
}
function onAnnotationPointerLeave(){
if(annotationTool==='laser'){laserPoint=null;emit({type:'laser',index:idx,point:null},{persist:false});renderInks();}
}
function clearAnnotations(broadcast=true){circles=[];draftCircle=null;laserPoint=null;if(broadcast)emit({type:'clear',index:idx});renderInks();}
function mountShell(){
if(document.getElementById('ppt-presenter'))return;
document.body.insertAdjacentHTML('beforeend',`<div id="ppt-presenter" data-layout="${layoutMode}" aria-label="演讲者视图">
<div class="ppt-presenter-top"><div><div class="ppt-presenter-brand">演讲者视图</div><div class="ppt-presenter-meta"><span id="ppt-plan"></span><span id="ppt-section" hidden></span></div></div><div class="ppt-presenter-meta"><span id="ppt-sync" class="ppt-sync" data-state="disconnected" title="表示观众页面的软件同步状态,不代表 HDMI 等物理线缆状态">观众屏 · 未连接</span><button id="ppt-preflight" class="ppt-ui-btn" type="button">检查</button><button id="ppt-settings" class="ppt-ui-btn" type="button">设置</button><button id="ppt-reopen" class="ppt-ui-btn" type="button">重新打开观众屏</button><button id="ppt-exit" class="ppt-ui-btn" type="button">退出</button></div></div>
<div class="ppt-presenter-main"><div class="ppt-preview-stack"><div class="ppt-stage-card"><div class="ppt-stage-label"><span>当前页</span><span class="ppt-annotation-tools"><button class="ppt-ui-btn" data-ppt-tool="laser" type="button" title="激光笔(L)">激光</button><button class="ppt-ui-btn" data-ppt-tool="circle" type="button" title="圈选(C)">圈选</button><button id="ppt-ink-clear" class="ppt-ui-btn ppt-clear-btn" type="button" title="清除标记(X)">清除</button></span><span id="ppt-current-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-current-frame" class="ppt-frame" title="当前页预览"></iframe><canvas id="ppt-presenter-ink" class="ppt-annotation-layer" aria-label="演讲标注层"></canvas></div></div><div class="ppt-stage-card" id="ppt-next-card"><div class="ppt-stage-label"><span>下一页</span><span id="ppt-next-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-next-frame" class="ppt-frame ppt-next-frame" title="下一页预览"></iframe></div></div></div>
<div class="ppt-notes-card"><div class="ppt-notes-head"><div id="ppt-notes-title" class="ppt-notes-title"></div><div class="ppt-notes-sub"><span id="ppt-note-meta"></span><span class="ppt-notes-tools"><span id="ppt-saved" class="ppt-saved">已保存</span><button id="ppt-note-smaller" class="ppt-ui-btn" type="button" title="缩小备注字号">A−</button><button id="ppt-note-larger" class="ppt-ui-btn" type="button" title="放大备注字号">A+</button></span></div></div><textarea id="ppt-note-editor" class="ppt-note-editor" spellcheck="false" aria-label="本页演讲备注"></textarea></div></div>
<div class="ppt-presenter-bottom"><div class="ppt-timer"><div class="ppt-time-copy"><div id="ppt-clock" class="ppt-clock">00:00</div><div id="ppt-time-detail" class="ppt-time-detail">本页 00:00 / —</div></div><button id="ppt-timer-toggle" class="ppt-ui-btn" type="button">开始</button><button id="ppt-timer-reset" class="ppt-ui-btn" type="button">重置</button><button id="ppt-rehearsal" class="ppt-ui-btn" type="button">排练</button></div><div class="ppt-controls"><button id="ppt-first" class="ppt-ui-btn" type="button">首页</button><button id="ppt-prev" class="ppt-ui-btn" type="button">上一页</button><button id="ppt-grid" class="ppt-ui-btn ppt-primary" type="button">总览</button><button id="ppt-next" class="ppt-ui-btn" type="button">下一页</button><button id="ppt-last" class="ppt-ui-btn" type="button">尾页</button></div><div class="ppt-page-state"><strong id="ppt-page-title"></strong><span id="ppt-page-count"></span><br><button id="ppt-auto-toggle" class="ppt-ui-btn ppt-auto-state" type="button">自动翻页 · 关闭</button></div></div>
<div id="ppt-dialog" class="ppt-dialog" hidden><div class="ppt-dialog-panel" role="dialog" aria-modal="true"><div class="ppt-dialog-head"><div id="ppt-dialog-title" class="ppt-dialog-title"></div><button id="ppt-dialog-close" class="ppt-ui-btn" type="button">关闭</button></div><div id="ppt-dialog-body" class="ppt-dialog-body"></div></div></div>
</div>`);
document.getElementById('ppt-reopen').onclick=reopenAudience;
document.getElementById('ppt-exit').onclick=exitPresenter;
document.getElementById('ppt-preflight').onclick=openPreflight;
document.getElementById('ppt-settings').onclick=openSettings;
document.getElementById('ppt-first').onclick=()=>go(0,{force:true});
document.getElementById('ppt-prev').onclick=()=>go(idx-1,{force:true});
document.getElementById('ppt-grid').onclick=toggleOverview;
document.getElementById('ppt-next').onclick=()=>go(idx+1,{force:true});
document.getElementById('ppt-last').onclick=()=>go(idx===total-1?0:total-1,{force:true});
document.getElementById('ppt-timer-toggle').onclick=toggleTimer;
document.getElementById('ppt-timer-reset').onclick=resetTimer;
document.getElementById('ppt-rehearsal').onclick=toggleRehearsal;
document.getElementById('ppt-auto-toggle').onclick=toggleAutoAdvance;
document.getElementById('ppt-note-smaller').onclick=()=>setNoteSize(noteSize-1);
document.getElementById('ppt-note-larger').onclick=()=>setNoteSize(noteSize+1);
document.getElementById('ppt-note-editor').addEventListener('input',saveNote);
document.querySelectorAll('[data-ppt-tool]').forEach(button=>button.onclick=()=>setAnnotationTool(button.dataset.pptTool));
document.getElementById('ppt-ink-clear').onclick=()=>clearAnnotations(true);
const ink=document.getElementById('ppt-presenter-ink');
ink.addEventListener('pointerdown',onAnnotationPointerDown);ink.addEventListener('pointermove',onAnnotationPointerMove);ink.addEventListener('pointerup',onAnnotationPointerUp);ink.addEventListener('pointercancel',onAnnotationPointerUp);ink.addEventListener('pointerleave',onAnnotationPointerLeave);
document.getElementById('ppt-dialog-close').onclick=closeDialog;
document.getElementById('ppt-dialog').onclick=event=>{if(event.target.id==='ppt-dialog')closeDialog();};
if('ResizeObserver'in window){presenterFrameObserver=new ResizeObserver(()=>requestAnimationFrame(fitPresenterFrames));presenterFrameObserver.observe(document.querySelector('.ppt-preview-stack'));}
else addEventListener('resize',fitPresenterFrames);
setNoteSize(noteSize,false);applyLayout(layoutMode,false);requestAnimationFrame(fitPresenterFrames);
}
function updateShell(){
if(mode!=='presenter')return;mountShell();
const n=noteAt(idx),next=Math.min(idx+1,total-1),id=slideId(idx),editor=document.getElementById('ppt-note-editor');
const currentFrame=document.getElementById('ppt-current-frame'),nextFrame=document.getElementById('ppt-next-frame');
if(currentFrame.dataset.slide!==String(idx)){currentFrame.src=frameUrl('preview',idx);currentFrame.dataset.slide=String(idx);}
if(nextFrame.dataset.slide!==String(next)){nextFrame.src=frameUrl('preview',next);nextFrame.dataset.slide=String(next);}
document.getElementById('ppt-current-label').textContent=`${idx+1} / ${total}`;
document.getElementById('ppt-next-label').textContent=idx===total-1?'已到尾页':`${next+1} / ${total}`;
document.getElementById('ppt-notes-title').textContent=n.title||`第 ${idx+1} 页`;
const target=plannedMsAt(idx);document.getElementById('ppt-note-meta').textContent=`建议 ${target?(target/60000).toFixed(1):'—'} 分钟 · ${id}`;
document.getElementById('ppt-page-title').textContent=n.title||`第 ${idx+1} 页`;
document.getElementById('ppt-page-count').textContent=`第 ${idx+1} / ${total} 页 · ${Math.round(((idx+1)/total)*100)}%`;
document.getElementById('ppt-first').disabled=idx===0;document.getElementById('ppt-prev').disabled=idx===0;document.getElementById('ppt-next').disabled=idx===total-1;
document.getElementById('ppt-last').textContent=idx===total-1?'重新开始':'尾页';
if(renderedNoteId!==id){renderedNoteId=id;editor.value=loadNoteValue(id,idx,n);editor.scrollTop=0;document.getElementById('ppt-saved').textContent='已保存';}
requestAnimationFrame(fitPresenterFrames);renderTimer();updateSyncStatus();
}
function saveNote(){
const saved=document.getElementById('ppt-saved');saved.textContent='保存中…';saved.classList.remove('ppt-saved');clearTimeout(saveTimer);
saveTimer=setTimeout(()=>{localStorage.setItem(noteStorageKey(renderedNoteId),document.getElementById('ppt-note-editor').value);saved.textContent='已保存';saved.classList.add('ppt-saved');},300);
}
function setNoteSize(value,persist=true){noteSize=Math.max(14,Math.min(26,value));document.documentElement.style.setProperty('--ppt-note-size',`${noteSize}px`);if(persist)localStorage.setItem('guizang-ppt-note-size',String(noteSize));}
function currentElapsed(){return elapsed+(timerRunning?Date.now()-timerStartedAt:0);}
function currentSlideElapsed(i=idx){return slideElapsed[i]+(timerRunning&&timedSlideIndex===i?Date.now()-slideTimerStartedAt:0);}
function commitRunningTime(now=Date.now()){
if(!timerRunning)return;elapsed+=now-timerStartedAt;slideElapsed[timedSlideIndex]+=now-slideTimerStartedAt;timerStartedAt=now;slideTimerStartedAt=now;
}
function rollSlideTimer(nextIndex){if(timerRunning)commitRunningTime();timedSlideIndex=nextIndex;slideTimerStartedAt=Date.now();}
function renderTimer(){
const totalUsed=currentElapsed(),pageUsed=currentSlideElapsed(),target=plannedMsAt(idx),clock=document.getElementById('ppt-clock');
if(clock)clock.textContent=durationText(totalUsed);
const detail=document.getElementById('ppt-time-detail');
if(detail){
detail.classList.toggle('is-over',Boolean(target&&pageUsed>target));
detail.textContent=target?(pageUsed<=target?`本页 ${durationText(pageUsed)} / ${durationText(target)} · 剩余 ${durationText(target-pageUsed)}`:`本页 ${durationText(pageUsed)} / ${durationText(target)} · 超时 ${durationText(pageUsed-target)}`):`本页 ${durationText(pageUsed)} / —`;
}
const totalPlan=totalPlannedMs(),plan=document.getElementById('ppt-plan');
if(plan)plan.textContent=totalPlan?`计划 ${Number(totalPlan/60000).toFixed(1)} 分钟 · 预计 ${clockTime(Date.now()+Math.max(0,totalPlan-totalUsed))}`:'计划 —';
const section=sectionInfo(idx),sectionEl=document.getElementById('ppt-section');
if(sectionEl){sectionEl.hidden=!section;sectionEl.textContent=section?`${section.name} · ${section.position}/${section.count}${section.planned?` · 剩余 ${durationText(Math.max(0,section.planned-section.used))}`:''}`:'';}
const timerButton=document.getElementById('ppt-timer-toggle');if(timerButton)timerButton.textContent=timerRunning?'暂停':(totalUsed?'继续':'开始');
const rehearsalButton=document.getElementById('ppt-rehearsal');if(rehearsalButton){rehearsalButton.textContent=rehearsalActive?'结束排练':'排练';rehearsalButton.classList.toggle('ppt-rehearsal-on',rehearsalActive);}
renderAutoState();
}
function toggleTimer(){const now=Date.now();if(timerRunning){commitRunningTime(now);timerRunning=false;}else{timerStartedAt=now;slideTimerStartedAt=now;timedSlideIndex=idx;timerRunning=true;}renderTimer();}
function resetTimer(){
elapsed=0;slideElapsed=Array(total).fill(0);timedSlideIndex=idx;
if(timerRunning){timerStartedAt=Date.now();slideTimerStartedAt=timerStartedAt;}else{timerStartedAt=0;slideTimerStartedAt=0;}
renderTimer();
}
function rehearsalHistory(){try{return JSON.parse(localStorage.getItem(storageKey('rehearsals'))||'[]');}catch(e){return[];}}
function toggleRehearsal(){
if(rehearsalActive){finishRehearsal(true);return;}
rehearsalActive=true;if(timerRunning){commitRunningTime();timerRunning=false;}resetTimer();toggleTimer();renderTimer();
}
function finishRehearsal(showSummary){
if(!rehearsalActive)return;if(timerRunning){commitRunningTime();timerRunning=false;}
const result={endedAt:Date.now(),totalMs:elapsed,slides:slideElapsed.map((ms,i)=>({index:i,id:slideId(i),title:noteAt(i).title||slideId(i),ms,targetMs:plannedMsAt(i)}))};
const history=rehearsalHistory();history.unshift(result);try{localStorage.setItem(storageKey('rehearsals'),JSON.stringify(history.slice(0,5)));}catch(e){}
rehearsalActive=false;renderTimer();if(showSummary)openRehearsalSummary(result,history.slice(0,5));
}
function openRehearsalSummary(result,history){
const rows=result.slides.filter(item=>item.ms>0).map(item=>`<tr><td>${item.index+1}</td><td>${escapeHTML(item.title)}</td><td>${durationText(item.ms)}</td><td>${item.targetMs?signedDuration(item.ms-item.targetMs):'—'}</td></tr>`).join('');
const recent=history.map(item=>`<div class="ppt-dialog-row"><span>${new Date(item.endedAt).toLocaleString()}</span><strong>${durationText(item.totalMs)}</strong></div>`).join('');
openDialog('排练记录',`<div class="ppt-dialog-section"><div class="ppt-dialog-row"><span>本次总时长</span><strong>${durationText(result.totalMs)}</strong></div><table class="ppt-summary"><thead><tr><th>#</th><th>页面</th><th>实际</th><th>与计划相比</th></tr></thead><tbody>${rows||'<tr><td colspan="4">没有记录到页面时间</td></tr>'}</tbody></table></div><div class="ppt-dialog-section"><h3>最近排练</h3>${recent}</div>`);
}
function autoDurationMs(i=idx){const explicit=Number(noteAt(i).autoAdvanceSeconds);const seconds=Number.isFinite(explicit)&&explicit>0?explicit:autoGlobalSeconds;return seconds>0?seconds*1000:null;}
function armAutoAdvance(){autoDeadline=0;autoRemainingMs=autoDurationMs()||0;}
function autoBlockedReason(){
if(overviewOn)return'总览中';if(annotationTool!=='none')return'标注中';if(screenMode!=='normal')return'观众屏已暂停';if(document.hidden)return'窗口不可见';
const dialog=document.getElementById('ppt-dialog');if(dialog&&!dialog.hidden)return'设置中';
if(audienceOpenedAt&&syncState()!=='synced')return'等待观众屏同步';return'';
}
function updateAutoAdvance(){
if(!autoEnabled)return;
const duration=autoDurationMs();if(!duration||idx===total-1)return;
const blocked=autoBlockedReason(),now=Date.now();
if(blocked){if(autoDeadline){autoRemainingMs=Math.max(0,autoDeadline-now);autoDeadline=0;}return;}
if(!autoDeadline)autoDeadline=now+(autoRemainingMs||duration);
autoRemainingMs=Math.max(0,autoDeadline-now);
if(now>=autoDeadline){autoDeadline=0;autoRemainingMs=0;go(idx+1,{force:true});}
}
function renderAutoState(){
const button=document.getElementById('ppt-auto-toggle');if(!button)return;
const duration=autoDurationMs(),blocked=autoEnabled?autoBlockedReason():'';
button.classList.toggle('is-on',autoEnabled&&!blocked);button.classList.toggle('is-paused',autoEnabled&&Boolean(blocked));
if(!autoEnabled)button.textContent='自动翻页 · 关闭';
else if(!duration)button.textContent='自动翻页 · 等待时长';
else if(idx===total-1)button.textContent='自动翻页 · 已结束';
else if(blocked)button.textContent=`自动翻页 · ${blocked}`;
else button.textContent=`自动翻页 · ${durationText(autoRemainingMs||duration)}`;
}
function toggleAutoAdvance(){
if(!autoEnabled&&!autoDurationMs()){openSettings();return;}
autoEnabled=!autoEnabled;localStorage.setItem(storageKey('auto-enabled'),autoEnabled?'1':'0');armAutoAdvance();renderAutoState();
}
function openDialog(title,html){
const dialog=document.getElementById('ppt-dialog');if(!dialog)return;
document.getElementById('ppt-dialog-title').textContent=title;document.getElementById('ppt-dialog-body').innerHTML=html;dialog.hidden=false;renderAutoState();
}
function closeDialog(){const dialog=document.getElementById('ppt-dialog');if(dialog)dialog.hidden=true;renderAutoState();}
function applyLayout(value,persist=true){
layoutMode=['balanced','slides','notes'].includes(value)?value:'balanced';
document.getElementById('ppt-presenter')?.setAttribute('data-layout',layoutMode);
if(persist)localStorage.setItem(storageKey('layout'),layoutMode);requestAnimationFrame(fitPresenterFrames);
}
function applyAudienceScreen(value){
const cover=document.getElementById('ppt-audience-cover');if(!cover)return;
if(value==='black'||value==='white'){cover.style.display='block';cover.style.background=value;}else cover.style.display='none';
}
function setScreenMode(value){
const next=['normal','black','white','frozen'].includes(value)?value:'normal',wasFrozen=audienceFrozen;
screenMode=next;audienceFrozen=next==='frozen';emit({type:'screen',mode:next});
if(wasFrozen&&!audienceFrozen)sendGoto(true);updateSyncStatus();armAutoAdvance();renderAutoState();
}
function openSettings(){
openDialog('演示设置',`<div class="ppt-dialog-section"><h3>自动翻页</h3><div class="ppt-dialog-grid"><label class="ppt-dialog-row"><span>启用</span><input id="ppt-auto-enabled-setting" type="checkbox" ${autoEnabled?'checked':''}></label><div class="ppt-field"><label for="ppt-auto-seconds-setting">全局间隔(秒)</label><input id="ppt-auto-seconds-setting" type="number" min="1" step="1" value="${autoGlobalSeconds||''}" placeholder="未设置"></div><button id="ppt-settings-save" class="ppt-ui-btn" type="button">保存自动翻页</button></div><p class="ppt-muted">页面里的 autoAdvanceSeconds 优先于全局间隔;建议讲述时间 minutes 不会自动触发翻页。</p></div><div class="ppt-dialog-section"><h3>演讲者布局</h3><div class="ppt-dialog-grid"><button class="ppt-ui-btn" data-layout-choice="balanced" type="button">均衡</button><button class="ppt-ui-btn" data-layout-choice="slides" type="button">页面优先</button><button class="ppt-ui-btn" data-layout-choice="notes" type="button">备注优先</button></div></div><div class="ppt-dialog-section"><h3>观众屏</h3><div class="ppt-dialog-grid"><button class="ppt-ui-btn" data-screen-choice="normal" type="button">正常显示</button><button class="ppt-ui-btn" data-screen-choice="black" type="button">黑屏 B</button><button class="ppt-ui-btn" data-screen-choice="white" type="button">白屏 W</button><button class="ppt-ui-btn" data-screen-choice="frozen" type="button">冻结 F</button></div></div><div class="ppt-dialog-section"><button id="ppt-open-shortcuts" class="ppt-ui-btn" type="button">查看快捷键</button></div>`);
document.querySelectorAll('[data-layout-choice]').forEach(button=>{button.classList.toggle('ppt-primary',button.dataset.layoutChoice===layoutMode);button.onclick=()=>{applyLayout(button.dataset.layoutChoice);openSettings();};});
document.querySelectorAll('[data-screen-choice]').forEach(button=>{button.classList.toggle('ppt-primary',button.dataset.screenChoice===screenMode);button.onclick=()=>{setScreenMode(button.dataset.screenChoice);openSettings();};});
document.getElementById('ppt-settings-save').onclick=()=>{autoEnabled=document.getElementById('ppt-auto-enabled-setting').checked;autoGlobalSeconds=Math.max(0,Number(document.getElementById('ppt-auto-seconds-setting').value||0));localStorage.setItem(storageKey('auto-enabled'),autoEnabled?'1':'0');localStorage.setItem(storageKey('auto-seconds'),String(autoGlobalSeconds));armAutoAdvance();closeDialog();};
document.getElementById('ppt-open-shortcuts').onclick=openShortcuts;
}
function openShortcuts(){
const items=[['← / →','上一页 / 下一页'],['Home / End','首页 / 尾页'],['Esc','总览;标注工具激活时先退出工具'],['L','激光笔'],['C','圈选'],['X','清除标记'],['B / W','观众屏黑屏 / 白屏'],['F','冻结 / 恢复观众屏'],['A','开启 / 关闭自动翻页'],['R','开始 / 结束排练'],['?','快捷键说明']];
openDialog('快捷键',`<div class="ppt-shortcuts">${items.map(([key,label])=>`<kbd>${key}</kbd><span>${label}</span>`).join('')}</div>`);
}
function preflightChecks(){
const images=[...document.images],videos=[...document.querySelectorAll('video')],frame=document.getElementById('ppt-current-frame'),rect=frame?.getBoundingClientRect();
return[
{label:'观众屏连接',ok:['synced','frozen'].includes(syncState()),detail:`当前状态:${document.getElementById('ppt-sync')?.textContent||'未连接'}`},
{label:'弹窗权限',ok:!popupBlocked,detail:popupBlocked?'浏览器阻止了观众窗口':'未检测到弹窗拦截'},
{label:'全屏状态',ok:Boolean(document.fullscreenElement),warn:true,detail:document.fullscreenElement?'已进入全屏':'尚未全屏,可在演讲前进入'},
{label:'字体加载',ok:!document.fonts||document.fonts.status==='loaded',detail:document.fonts?`状态:${document.fonts.status}`:'浏览器不提供字体状态'},
{label:'图片资源',ok:images.every(image=>image.complete&&image.naturalWidth>0),detail:`${images.filter(image=>image.complete&&image.naturalWidth>0).length} / ${images.length} 已加载`},
{label:'视频资源',ok:videos.every(video=>video.readyState>=2),detail:videos.length?`${videos.filter(video=>video.readyState>=2).length} / ${videos.length} 可播放`:'没有视频'},
{label:'当前页 16:9',ok:Boolean(rect&&Math.abs(rect.width/rect.height-16/9)<.003),detail:rect?`${Math.round(rect.width)} × ${Math.round(rect.height)}`:'尚未渲染'}
];
}
function openPreflight(){
const checks=preflightChecks(),html=checks.map(item=>`<div class="ppt-dialog-row"><div class="ppt-check ${item.ok?'is-ok':(item.warn?'is-warn':'')}"><span class="ppt-check-dot"></span><span><strong>${item.label}</strong><br><span class="ppt-muted">${item.detail}</span></span></div><strong>${item.ok?'正常':(item.warn?'建议':'异常')}</strong></div>`).join('');
openDialog('演前检查',`<div class="ppt-dialog-section">${html}</div><div class="ppt-dialog-section"><button id="ppt-preflight-refresh" class="ppt-ui-btn" type="button">重新检查</button> <button id="ppt-enter-fullscreen" class="ppt-ui-btn" type="button">进入全屏</button></div><p class="ppt-muted">这里只能检查浏览器和观众页面的软件状态,不能确认 HDMI、转接器或投影仪线缆是否物理连接。</p>`);
document.getElementById('ppt-preflight-refresh').onclick=openPreflight;
document.getElementById('ppt-enter-fullscreen').onclick=async()=>{try{await document.documentElement.requestFullscreen();}catch(e){}openPreflight();};
}
function initTransport(){
if(transportReady)return;transportReady=true;
if('BroadcastChannel'in window){try{channel=new BroadcastChannel(channelName());channel.onmessage=e=>receive(e.data);}catch(e){}}
addEventListener('message',e=>receive(e.data));
addEventListener('storage',e=>{if(e.key===syncKey()&&e.newValue){try{receive(JSON.parse(e.newValue));}catch(err){}}});
}
function emit(payload,{persist=true}={}){
const message={__guizangPptSync:1,session,role:mode,...payload,at:Date.now(),nonce:Math.random().toString(36).slice(2)};
try{channel?.postMessage(message);}catch(e){}
if(persist)try{localStorage.setItem(syncKey(),JSON.stringify(message));}catch(e){}
try{if(mode==='presenter'&&audienceWindow&&!audienceWindow.closed)audienceWindow.postMessage(message,'*');}catch(e){}
try{if(mode==='audience'&&window.opener&&!window.opener.closed)window.opener.postMessage(message,'*');}catch(e){}
}
function receive(message){
if(!message||message.__guizangPptSync!==1||message.session!==session||message.role===mode)return;
if(message.nonce){if(seenNonceSet.has(message.nonce))return;seenNonceSet.add(message.nonce);seenNonces.push(message.nonce);if(seenNonces.length>256)seenNonceSet.delete(seenNonces.shift());}
if(mode==='presenter'){
if(message.type==='ready'){popupBlocked=false;sendGoto(true);}
if(message.type==='ack'){lastAckAt=Date.now();lastAckIndex=Number(message.index);lastAckSeq=Number(message.seq);updateSyncStatus();}
return;
}
if(mode!=='audience')return;
if(message.type==='screen'){applyAudienceScreen(message.mode);return;}
if(message.type==='laser'&&Number(message.index)===idx){audienceLaser=message.point||null;audienceLaserHideAt=Date.now()+650;renderInks();return;}
if(message.type==='circle'&&Number(message.index)===idx&&message.circle){audienceCircles.push(message.circle);renderInks();return;}
if(message.type==='clear'&&Number(message.index)===idx){audienceCircles=[];renderInks();return;}
if(message.type==='goto'||message.type==='ping'){
if(message.type==='goto'){audienceCircles=Array.isArray(message.circles)?message.circles:[];applyAudienceScreen(message.screenMode);go(Number(message.index),{force:true});}
emit({type:'ack',index:idx,seq:Number(message.seq)});
}
}
function sendGoto(force=false){
if(mode!=='presenter'||audienceFrozen)return;if(force||lastAckIndex!==idx)seq++;
emit({type:'goto',index:idx,seq,screenMode,circles});updateSyncStatus();
}
function syncState(){
if(popupBlocked)return'blocked';if(audienceWindow&&audienceWindow.closed)return'disconnected';if(audienceFrozen)return'frozen';
if(!lastAckAt)return audienceOpenedAt&&Date.now()-audienceOpenedAt<2600?'connecting':(audienceOpenedAt?'unsynced':'disconnected');
if(Date.now()-lastAckAt>2600||lastAckIndex!==idx||lastAckSeq<seq)return'unsynced';return'synced';
}
function updateSyncStatus(){
const el=document.getElementById('ppt-sync');if(!el)return;const state=syncState();
const labels={blocked:'弹窗被拦截',disconnected:'未连接',connecting:'连接中',unsynced:'未同步',frozen:'已冻结',synced:`已同步 · 第 ${idx+1} 页`};
el.dataset.state=state;el.textContent=`观众屏 · ${labels[state]}`;
}
function reopenAudience(){
initTransport();popupBlocked=false;audienceOpenedAt=Date.now();lastAckAt=0;lastAckIndex=-1;lastAckSeq=-1;
if(audienceFrozen){audienceFrozen=false;screenMode='normal';}
audienceWindow=window.open(frameUrl('audience',idx),`guizang-ppt-audience-${session}`,'popup=yes,width=1440,height=900');
if(!audienceWindow)popupBlocked=true;updateSyncStatus();
}
function enterPresenter(openAudience=true){
if(mode==='presenter')return;lowPowerBeforePresenter=window.__lowPowerMode;mode='presenter';initTransport();mountShell();
document.body.classList.remove('ppt-audience','ppt-preview');document.body.classList.add('ppt-presenter');document.title=`演讲者视图 · ${originalTitle}`;
window.__setLowPowerMode?.(true,{persist:false});updateShell();armAutoAdvance();if(openAudience)reopenAudience();
}
function exitPresenter(){
if(rehearsalActive)finishRehearsal(false);else if(timerRunning){commitRunningTime();timerRunning=false;}emit({type:'bye'});mode='deck';document.body.classList.remove('ppt-presenter');document.title=originalTitle;window.__setLowPowerMode?.(lowPowerBeforePresenter,{persist:false});
}
function initAudience(){
mode='audience';document.body.classList.add('ppt-audience');document.title=`观众屏 · ${originalTitle}`;
document.body.insertAdjacentHTML('beforeend','<canvas id="ppt-audience-ink" aria-hidden="true"></canvas><div id="ppt-audience-cover" aria-hidden="true"></div>');
const resize=()=>{sizeInk(document.getElementById('ppt-audience-ink'),innerWidth,innerHeight);renderInks();};addEventListener('resize',resize);resize();
initTransport();emit({type:'ready',index:idx});
}
function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});}
function onSlideChange(nextIndex){
if(mode!=='presenter')return;rollSlideTimer(nextIndex);circles=[];draftCircle=null;laserPoint=null;annotationPointerDown=false;armAutoAdvance();updateShell();sendGoto();
}
function handleKey(e){
const editable=e.target?.matches?.('input,textarea,[contenteditable="true"]');
if(editable){if(e.key==='Escape')e.target.blur();return true;}
if(mode==='audience'||mode==='preview'){e.preventDefault();return true;}
if(mode==='deck'&&e.key?.toLowerCase()==='p'&&!e.metaKey&&!e.ctrlKey&&!e.altKey){e.preventDefault();enterPresenter(true);return true;}
if(mode!=='presenter')return false;
const key=e.key?.toLowerCase();
if(e.key==='?'||key==='/'){e.preventDefault();openShortcuts();return true;}
const dialog=document.getElementById('ppt-dialog');
if(!dialog?.hidden){if(e.key==='Escape'){e.preventDefault();closeDialog();}return true;}
if(e.key==='Escape'){e.preventDefault();if(annotationTool!=='none')setAnnotationTool('none');else toggleOverview();return true;}
if(overviewOn)return true;
if(key==='l'){e.preventDefault();setAnnotationTool('laser');return true;}
if(key==='c'){e.preventDefault();setAnnotationTool('circle');return true;}
if(key==='x'){e.preventDefault();clearAnnotations(true);return true;}
if(key==='b'){e.preventDefault();setScreenMode(screenMode==='black'?'normal':'black');return true;}
if(key==='w'){e.preventDefault();setScreenMode(screenMode==='white'?'normal':'white');return true;}
if(key==='f'){e.preventDefault();setScreenMode(audienceFrozen?'normal':'frozen');return true;}
if(key==='a'){e.preventDefault();toggleAutoAdvance();return true;}
if(key==='r'){e.preventDefault();toggleRehearsal();return true;}
if(['ArrowRight','PageDown',' ','ArrowDown'].includes(e.key)){e.preventDefault();go(idx+1,{force:true});return true;}
if(['ArrowLeft','PageUp','ArrowUp'].includes(e.key)){e.preventDefault();go(idx-1,{force:true});return true;}
if(e.key==='Home'){e.preventDefault();go(0,{force:true});return true;}
if(e.key==='End'){e.preventDefault();go(total-1,{force:true});return true;}
return false;
}
function initMode(){
document.getElementById('ppt-low-power-btn')?.addEventListener('click',()=>window.__setLowPowerMode(!window.__lowPowerMode));
document.getElementById('ppt-overview-btn')?.addEventListener('click',toggleOverview);
document.getElementById('ppt-presenter-btn')?.addEventListener('click',()=>enterPresenter(true));
if(requestedMode==='audience')initAudience();else if(requestedMode==='preview')initPreview();else if(requestedMode==='presenter')enterPresenter(false);
}
setInterval(()=>{
renderTimer();renderInks();updateAutoAdvance();
if(mode==='presenter'&&Date.now()-lastPingAt>900){lastPingAt=Date.now();if(audienceWindow?.closed)updateSyncStatus();emit({type:'ping',index:idx,seq});updateSyncStatus();}
},250);
return{initMode,onSlideChange,handleKey,slideLabel,enterPresenter,reopenAudience,debugState:()=>({mode,idx,session,syncState:syncState(),timerRunning,rehearsalActive,autoEnabled,autoGlobalSeconds,autoRemainingMs,screenMode,annotationTool,circles:[...circles],audienceCircles:[...audienceCircles],layoutMode})};
})();
addEventListener('keydown',e=>{
if(window.__pptPresenter?.handleKey(e))return;
if(e.key==='Escape'){e.preventDefault();toggleOverview();return;}
if(e.key && e.key.toLowerCase()==='b' && !e.metaKey && !e.ctrlKey && !e.altKey){
e.preventDefault();
window.__setLowPowerMode(!window.__lowPowerMode);
return;
}
if(overviewOn)return;
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown'){
if(window.__pipeAdvance && window.__pipeAdvance()) return;
go(idx+1);
return;
}
if(e.key==='ArrowLeft'||e.key==='PageUp'||e.key==='ArrowUp')go(idx-1);
if(e.key==='Home')go(0);
if(e.key==='End')go(total-1);
});
let wheelTO=null,wheelAcc=0;
addEventListener('wheel',e=>{
wheelAcc+=e.deltaY+e.deltaX;
if(Math.abs(wheelAcc)>50){
if(wheelAcc>0 && window.__pipeAdvance && window.__pipeAdvance()){
wheelAcc=0;
}else{
go(idx+(wheelAcc>0?1:-1));wheelAcc=0;
}
}
clearTimeout(wheelTO);wheelTO=setTimeout(()=>wheelAcc=0,150);
},{passive:true});
let tx=0,ty=0;
addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});
addEventListener('touchend',e=>{
const dx=(e.changedTouches[0].clientX-tx);
const dy=(e.changedTouches[0].clientY-ty);
if(Math.abs(dx)>50&&Math.abs(dx)>Math.abs(dy)){
if(dx<0 && window.__pipeAdvance && window.__pipeAdvance()) return;
go(idx+(dx<0?1:-1));
}
},{passive:true});
/* ?slide=N 直达(1-based),与 Swiss 模板行为一致 */
const initialSlideParam = new URLSearchParams(location.search).get('slide');
const initialSlide = initialSlideParam ? Number(initialSlideParam) - 1 : 0;
go(Number.isFinite(initialSlide) ? initialSlide : 0);
window.__pptPresenter?.initMode();
</script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script>lucide.createIcons();</script>
<!-- ============ Motion One 动效引擎(本地优先,CDN 兜底) ============
加载策略:先 ./assets/motion.min.js(本地,离线可用),失败 fallback 到 jsDelivr CDN
加载完成后挂 window.__playSlide / window.__pipeAdvance,
导航脚本会在翻页中段调用 __playSlide(idx),键盘/滚轮/触屏会在翻页前调用 __pipeAdvance()
双双失败时会兜底把所有 [data-anim] 设为可见,不让动效破坏阅读
-->
<script type="module">
let motion;
try {
motion = await import('./assets/motion.min.js');
} catch(e1) {
try {
motion = await import('https://cdn.jsdelivr.net/npm/motion@11.11.17/+esm');
} catch(e2) {
console.warn('[motion] local + CDN both failed, disabling animations', e1, e2);
document.querySelectorAll('[data-anim]').forEach(el=>{el.style.opacity='1';el.style.transform='none'});
document.querySelectorAll('[data-animate="pipeline"] [data-anim]').forEach(el=>el.style.opacity='1');
}
}
if(motion){
const { animate, stagger } = motion;
document.body.classList.add('motion-ready');
const EASE = [.22, 1, .36, 1];
const slides = [...document.querySelectorAll('.slide')];
let pipeStep = -1;
let lastIdx = -1;
function resetAnims(slide){
slide.querySelectorAll('[data-anim]').forEach(el=>{
el.style.opacity='';
el.style.transform='';
});
}
function revealStatic(slide){
resetAnims(slide);
document.getAnimations?.().forEach(a=>a.cancel());
slide.querySelectorAll('[data-anim]').forEach(el=>{
el.style.opacity='1';
el.style.transform='none';
});
}
function playSlide(i){
const slide = slides[i];
if(!slide) return;
lastIdx = i;
const recipe = slide.dataset.animate || (slide.classList.contains('hero') ? 'hero' : 'cascade');
if(window.__lowPowerMode){
revealStatic(slide);
return;
}
if(recipe === 'pipeline'){
pipeStep = -1;
slide.querySelectorAll('[data-anim]').forEach(el=>{
el.style.opacity='0.15';
el.style.transform='none';
});
return;
}
resetAnims(slide);
const all = [...slide.querySelectorAll('[data-anim]')];
if(!all.length) return;
if(recipe === 'directional'){
const lefts = all.filter(el=>el.dataset.anim==='left');
const divs = all.filter(el=>el.dataset.anim==='divider');
const rights = all.filter(el=>el.dataset.anim==='right');
const others = all.filter(el=>!['left','right','divider'].includes(el.dataset.anim));
if(others.length) animate(others, {opacity:[0,1], y:[12,0]}, {duration:.6, delay:stagger(.1, {start:.15}), easing:EASE});
if(lefts.length) animate(lefts, {opacity:[0,1], x:[-24,0]}, {duration:.8, delay:.35, easing:EASE});
if(divs.length) animate(divs, {opacity:[0,.25]}, {duration:.5, delay:.9});
if(rights.length) animate(rights, {opacity:[0,1], x:[24,0]}, {duration:.8, delay:1.0, easing:EASE});
return;
}
if(recipe === 'quote'){
const lines = all.filter(el=>el.dataset.anim==='line');
const others = all.filter(el=>el.dataset.anim!=='line');
if(others.length) animate(others, {opacity:[0,1], y:[8,0]}, {duration:.6, delay:stagger(.12, {start:.2}), easing:EASE});
if(lines.length) animate(lines, {opacity:[.35,1], y:[10,0]}, {duration:.8, delay:stagger(.55, {start:.5}), easing:EASE});
return;
}
if(recipe === 'hero'){
animate(all, {opacity:[0,1], y:[14,0]}, {duration:.9, delay:stagger(.16, {start:.2}), easing:EASE});
return;
}
// default: cascade
animate(all, {opacity:[0,1], y:[16,0]}, {duration:.75, delay:stagger(.1, {start:.15}), easing:EASE});
}
function pipeAdvance(){
if(window.__lowPowerMode) return false;
const slide = slides[lastIdx];
if(!slide || slide.dataset.animate !== 'pipeline') return false;
const steps = [...slide.querySelectorAll('[data-anim="step"]')];
const arrows = [...slide.querySelectorAll('[data-anim="arrow"]')];
if(pipeStep >= steps.length - 1) return false;
pipeStep++;
animate(steps[pipeStep], {opacity:[0.15,1], y:[8,0]}, {duration:.5, easing:EASE});
if(pipeStep > 0 && arrows[pipeStep-1]){
animate(arrows[pipeStep-1], {opacity:[0.15,.7]}, {duration:.3, delay:.15});
}
return true;
}
window.__playSlide = playSlide;
window.__pipeAdvance = pipeAdvance;
// 首屏:go() 已跑过(同步),没来得及触发动效 → 这里补一下(尊重 ?slide=N)
playSlide(window.__currentSlideIndex || 0);
}
</script>
</body>
</html>