1398 lines
99 KiB
HTML
1398 lines
99 KiB
HTML
<!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{position:relative;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,.ppt-presenter-overview{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-grid-btn.is-active{color:#111318;background:#edf1f5;border-color:#edf1f5}.ppt-annotation-tools .ppt-clear-btn{min-width:0}.ppt-grid-btn{display:inline-flex;align-items:center;gap:6px}.ppt-grid-icon{display:grid;grid-template-columns:repeat(2,4px);gap:2px}.ppt-grid-icon i{width:4px;height:4px;background:currentColor}
|
||
.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-preview-stack.is-overview>.ppt-stage-card{display:none}.ppt-presenter-overview{grid-row:1/-1;display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0}.ppt-presenter-overview[hidden]{display:none}.ppt-overview-head{display:flex;align-items:center;justify-content:space-between;padding:13px 15px;border-bottom:1px solid #292d34}.ppt-overview-head strong{font-size:15px}.ppt-overview-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;align-content:start;overflow:auto;padding:14px}.ppt-overview-page{appearance:none;min-height:112px;display:grid;grid-template-rows:auto 1fr auto;gap:12px;padding:13px;border:1px solid #343943;border-radius:10px;background:#111318;color:#eef1f4;text-align:left;cursor:pointer}.ppt-overview-page:hover{background:#1d2128;border-color:#636b78}.ppt-overview-page.is-current{border-color:#84a8ff;box-shadow:inset 0 0 0 1px #84a8ff}.ppt-overview-page-no{color:#8f97a3;font:600 12px/1 var(--mono,monospace)}.ppt-overview-page-title{font-size:14px;font-weight:650;line-height:1.35}.ppt-overview-page-meta{display:flex;justify-content:space-between;gap:8px;color:#7f8793;font-size:11px}
|
||
.ppt-notes-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-height:0}.ppt-notes-head{padding:16px 18px;border-bottom:1px solid #292d34}.ppt-notes-label{margin-bottom:6px;color:#848c98;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.ppt-notes-title{font-size:21px;font-weight:680;line-height:1.32}.ppt-purpose-block{margin-top:14px;padding:11px 12px;border-left:3px solid #84a8ff;background:#111318}.ppt-purpose-copy{color:#c9ced6;font-size:14px;line-height:1.55}.ppt-notes-draft-head{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin-top:15px;padding-top:13px;border-top:1px solid #2d3139}.ppt-notes-draft-head .ppt-notes-label{margin:0 0 4px}.ppt-note-meta{color:#8f97a3;font-size:12px}
|
||
.ppt-notes-tools{display:flex;align-items:center;gap:6px}.ppt-notes-tools .ppt-ui-btn{padding:7px 9px}.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-note-editor:focus{box-shadow:inset 0 0 0 1px #3d4654}
|
||
.ppt-saved{color:#72df99;white-space:nowrap}.ppt-presenter-bottom{display:grid;grid-template-columns:minmax(330px,1fr) auto minmax(110px,1fr);align-items:center;gap:20px;padding:12px 24px;border-top:1px solid #2b2e35;background:#15181d}
|
||
.ppt-time-dashboard{display:flex;align-items:stretch;min-width:0}.ppt-time-stat{min-width:90px;display:grid;align-content:center;gap:4px;padding:0 16px}.ppt-time-stat:first-child{padding-left:0}.ppt-time-stat+.ppt-time-stat{border-left:1px solid #343943}.ppt-time-label{color:#89919d;font-size:11px;font-weight:700;letter-spacing:.1em}.ppt-time-value{color:#f7f8fa;font:650 23px/1 var(--mono,monospace);font-variant-numeric:tabular-nums}.ppt-time-hint{color:#747c88;font-size:11px;white-space:nowrap}.ppt-time-stat.is-over .ppt-time-label,.ppt-time-stat.is-over .ppt-time-value{color:#ff8a90}.ppt-time-stat.is-remaining .ppt-time-value{color:#72df99}
|
||
.ppt-control-stack{display:grid;gap:7px;justify-items:center}.ppt-controls,.ppt-session-actions{display:flex;align-items:center;justify-content:center;gap:7px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:68px}.ppt-session-actions{padding-top:7px;border-top:1px solid #343943}.ppt-rehearsal-on{color:#ff8a90}.ppt-page-state{text-align:right}.ppt-page-state strong{display:block;color:#fff;font-size:17px;margin-bottom:5px}.ppt-page-state span{color:#8f97a3;font:600 13px/1 var(--mono,monospace)}.ppt-auto-state{display:inline-flex;align-items:center;gap:6px;border-radius:999px}.ppt-auto-state.is-on{color:#72df99;border-color:rgba(114,223,153,.45);background:rgba(114,223,153,.08)}.ppt-auto-state.is-paused{color:#f0c766;border-color:rgba(240,199,102,.42);background:rgba(240,199,102,.08)}
|
||
#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)}
|
||
.ppt-settings-card{padding:15px;border:1px solid #30353e;border-radius:12px;background:#12151a}.ppt-settings-card+.ppt-settings-card{margin-top:12px}.ppt-settings-card h3{margin:0 0 3px}.ppt-settings-copy{margin:0 0 14px;color:#858d99;font-size:12px}.ppt-switch-row{display:flex;align-items:center;justify-content:space-between;gap:18px;cursor:pointer}.ppt-switch-row input{position:absolute;opacity:0;pointer-events:none}.ppt-switch{width:48px;height:28px;flex:0 0 auto;padding:3px;border:1px solid #434955;border-radius:999px;background:#252a31;transition:.2s}.ppt-switch::after{content:"";display:block;width:20px;height:20px;border-radius:50%;background:#a8afb9;transition:.2s}.ppt-switch-row input:checked+.ppt-switch{border-color:#72df99;background:rgba(114,223,153,.16)}.ppt-switch-row input:checked+.ppt-switch::after{transform:translateX(20px);background:#72df99}.ppt-stepper{display:grid;grid-template-columns:38px minmax(72px,1fr) auto 38px;align-items:center;margin-top:14px;border:1px solid #363c46;border-radius:10px;background:#0d0f13;overflow:hidden}.ppt-stepper button{height:42px;border:0;background:#20242a;color:#fff;font-size:19px;cursor:pointer}.ppt-stepper button:hover{background:#2c323b}.ppt-stepper input{width:100%;height:42px;border:0!important;border-radius:0!important;background:transparent!important;text-align:center;font:650 16px var(--mono,monospace)!important;-moz-appearance:textfield}.ppt-stepper input::-webkit-inner-spin-button,.ppt-stepper input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.ppt-stepper-unit{color:#858d99;font-size:12px}.ppt-segmented{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}.ppt-segmented.screen{grid-template-columns:repeat(4,1fr)}.ppt-segmented .ppt-ui-btn{min-width:0}.ppt-settings-save{width:100%;margin-top:14px;background:#edf1f5;color:#111318;border-color:#edf1f5}.ppt-settings-save:hover{background:#fff}.ppt-settings-save+.ppt-settings-copy{margin:10px 0 0}
|
||
#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:1100px){.ppt-presenter-bottom{grid-template-columns:minmax(292px,1fr) auto minmax(86px,.6fr);gap:12px}.ppt-time-stat{min-width:82px;padding:0 11px}.ppt-time-value{font-size:20px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:60px;padding:8px 9px}}
|
||
@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-saved{display:none}.ppt-presenter-bottom{grid-template-columns:minmax(270px,1fr) auto 74px;gap:8px;padding:9px 16px}.ppt-time-stat{min-width:0;padding:0 9px}.ppt-time-value{font-size:18px}.ppt-time-hint{max-width:78px;overflow:hidden;text-overflow:ellipsis}.ppt-controls,.ppt-session-actions{gap:5px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:52px;padding:7px 8px;font-size:12px}.ppt-page-state strong{font-size:14px}.ppt-dialog-grid{grid-template-columns:1fr}.ppt-segmented.screen{grid-template-columns:repeat(2,1fr)}}
|
||
@media(max-height:700px){.ppt-presenter-top{padding-top:10px;padding-bottom:10px}.ppt-presenter-main{padding-top:10px;padding-bottom:10px}.ppt-preview-stack{grid-template-rows:minmax(0,1fr) clamp(88px,16vh,118px);gap:10px}.ppt-stage-label{padding:7px 10px}.ppt-notes-head{padding:12px 14px}.ppt-purpose-block{margin-top:10px;padding:8px 10px}.ppt-notes-draft-head{margin-top:10px;padding-top:9px}.ppt-presenter-bottom{padding-top:8px;padding-bottom:8px}.ppt-control-stack{gap:5px}.ppt-session-actions{padding-top:5px}}
|
||
|
||
/* ============================================================
|
||
============ 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,presenterOverviewOn=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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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(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 buildPresenterOverview(){
|
||
const grid=document.getElementById('ppt-presenter-overview-grid');if(!grid)return;grid.innerHTML='';
|
||
slides.forEach((slide,i)=>{
|
||
const n=noteAt(i),button=document.createElement('button');button.type='button';button.className=`ppt-overview-page${i===idx?' is-current':''}`;
|
||
button.innerHTML=`<span class="ppt-overview-page-no">${String(i+1).padStart(2,'0')} / ${String(total).padStart(2,'0')}</span><span class="ppt-overview-page-title">${escapeHTML(n.title||slideId(i))}</span><span class="ppt-overview-page-meta"><span>${escapeHTML(n.section||'未分组')}</span><span>${Math.round(((i+1)/total)*100)}%</span></span>`;
|
||
button.onclick=()=>{togglePresenterOverview(false);go(i,{force:true});};grid.appendChild(button);
|
||
});
|
||
requestAnimationFrame(()=>grid.querySelector('.is-current')?.scrollIntoView({block:'nearest',inline:'nearest'}));
|
||
}
|
||
function togglePresenterOverview(force){
|
||
presenterOverviewOn=typeof force==='boolean'?force:!presenterOverviewOn;
|
||
if(presenterOverviewOn&&annotationTool!=='none')setAnnotationTool('none');
|
||
const stack=document.querySelector('.ppt-preview-stack'),overview=document.getElementById('ppt-presenter-overview');
|
||
stack?.classList.toggle('is-overview',presenterOverviewOn);if(overview)overview.hidden=!presenterOverviewOn;
|
||
const button=document.getElementById('ppt-grid');if(button){button.classList.toggle('is-active',presenterOverviewOn);button.setAttribute('aria-pressed',String(presenterOverviewOn));}
|
||
if(presenterOverviewOn)buildPresenterOverview();else requestAnimationFrame(fitPresenterFrames);renderAutoState();
|
||
}
|
||
|
||
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-auto-toggle" class="ppt-ui-btn ppt-auto-state" type="button">自动翻页 · 关闭</button><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 id="ppt-grid" class="ppt-ui-btn ppt-grid-btn" type="button" aria-pressed="false" title="宫格选页(Esc)"><span class="ppt-grid-icon" aria-hidden="true"><i></i><i></i><i></i><i></i></span>宫格</button><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 id="ppt-presenter-overview" class="ppt-presenter-overview" hidden><div class="ppt-overview-head"><strong>选择页面</strong><button id="ppt-overview-close" class="ppt-ui-btn" type="button">返回预览</button></div><div id="ppt-presenter-overview-grid" class="ppt-overview-grid"></div></div></div>
|
||
<div class="ppt-notes-card"><div class="ppt-notes-head"><div class="ppt-notes-label">标题</div><div id="ppt-notes-title" class="ppt-notes-title"></div><div class="ppt-purpose-block"><div class="ppt-notes-label">本页目的</div><div id="ppt-notes-purpose" class="ppt-purpose-copy"></div></div><div class="ppt-notes-draft-head"><div><div class="ppt-notes-label">草稿(备注)</div><div id="ppt-note-meta" class="ppt-note-meta"></div></div><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-time-dashboard"><div class="ppt-time-stat"><span class="ppt-time-label">已进行</span><strong id="ppt-clock" class="ppt-time-value">00:00</strong><span class="ppt-time-hint">总时长</span></div><div class="ppt-time-stat"><span class="ppt-time-label">本页</span><strong id="ppt-slide-clock" class="ppt-time-value">00:00</strong><span id="ppt-slide-plan" class="ppt-time-hint">计划 —</span></div><div id="ppt-slide-status-box" class="ppt-time-stat"><span id="ppt-slide-status-label" class="ppt-time-label">计划</span><strong id="ppt-slide-status" class="ppt-time-value">—</strong><span id="ppt-slide-status-hint" class="ppt-time-hint">未设置</span></div></div><div class="ppt-control-stack"><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-next" class="ppt-ui-btn" type="button">下一页</button><button id="ppt-last" class="ppt-ui-btn" type="button">尾页</button></div><div class="ppt-session-actions"><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><div class="ppt-page-state"><strong id="ppt-page-count"></strong><span id="ppt-page-percent"></span></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=()=>togglePresenterOverview();
|
||
document.getElementById('ppt-overview-close').onclick=()=>togglePresenterOverview(false);
|
||
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} 页`;
|
||
document.getElementById('ppt-notes-purpose').textContent=n.purpose||'—';
|
||
const target=plannedMsAt(idx);document.getElementById('ppt-note-meta').textContent=`建议 ${target?(target/60000).toFixed(1):'—'} 分钟 · ${id}`;
|
||
document.getElementById('ppt-page-count').textContent=`第 ${idx+1} / ${total} 页`;
|
||
document.getElementById('ppt-page-percent').textContent=`${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='已保存';}
|
||
if(presenterOverviewOn)buildPresenterOverview();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 slideClock=document.getElementById('ppt-slide-clock'),slidePlan=document.getElementById('ppt-slide-plan'),statusBox=document.getElementById('ppt-slide-status-box'),statusLabel=document.getElementById('ppt-slide-status-label'),statusValue=document.getElementById('ppt-slide-status'),statusHint=document.getElementById('ppt-slide-status-hint');
|
||
if(slideClock)slideClock.textContent=durationText(pageUsed);if(slidePlan)slidePlan.textContent=target?`计划 ${durationText(target)}`:'计划 —';
|
||
if(statusBox&&statusLabel&&statusValue&&statusHint){
|
||
const over=Boolean(target&&pageUsed>target),remaining=Boolean(target&&!over);statusBox.classList.toggle('is-over',over);statusBox.classList.toggle('is-remaining',remaining);
|
||
statusLabel.textContent=over?'超时':(remaining?'剩余':'计划');statusValue.textContent=target?durationText(Math.abs(target-pageUsed)):'—';statusHint.textContent=over?'已超出本页计划':(remaining?'距本页计划':'未设置');
|
||
}
|
||
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(presenterOverviewOn||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-settings-card"><label class="ppt-switch-row" for="ppt-auto-enabled-setting"><span><strong>自动翻页</strong><br><span class="ppt-muted">需要时手动开启,默认关闭</span></span><input id="ppt-auto-enabled-setting" type="checkbox" ${autoEnabled?'checked':''}><span class="ppt-switch" aria-hidden="true"></span></label><div class="ppt-field"><label for="ppt-auto-seconds-setting">全局间隔</label><div class="ppt-stepper"><button id="ppt-auto-minus" type="button" aria-label="减少一秒">−</button><input id="ppt-auto-seconds-setting" type="number" min="1" step="1" value="${autoGlobalSeconds||''}" placeholder="—"><span class="ppt-stepper-unit">秒</span><button id="ppt-auto-plus" type="button" aria-label="增加一秒">+</button></div></div><button id="ppt-settings-save" class="ppt-ui-btn ppt-settings-save" type="button">保存自动翻页</button><p class="ppt-settings-copy">单页 autoAdvanceSeconds 优先于全局间隔;minutes 只是讲述计划。</p></div><div class="ppt-settings-card"><h3>演讲者布局</h3><p class="ppt-settings-copy">根据屏幕宽度调整页面与备注的比例</p><div class="ppt-segmented"><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-settings-card"><h3>观众屏</h3><p class="ppt-settings-copy">临时遮挡或冻结观众屏</p><div class="ppt-segmented screen"><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();};});
|
||
const secondsInput=document.getElementById('ppt-auto-seconds-setting'),stepSeconds=delta=>{const current=Math.max(0,Number(secondsInput.value||0));secondsInput.value=String(Math.max(1,current+delta));};
|
||
document.getElementById('ppt-auto-minus').onclick=()=>stepSeconds(-1);document.getElementById('ppt-auto-plus').onclick=()=>stepSeconds(1);
|
||
document.getElementById('ppt-settings-save').onclick=()=>{autoEnabled=document.getElementById('ppt-auto-enabled-setting').checked;autoGlobalSeconds=Math.max(0,Number(secondsInput.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;if(overviewOn)toggleOverview();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;}togglePresenterOverview(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 togglePresenterOverview();return true;}
|
||
if(presenterOverviewOn)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,toggleOverview:togglePresenterOverview,debugState:()=>({mode,idx,session,syncState:syncState(),timerRunning,rehearsalActive,presenterOverviewOn,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>
|