2915 lines
158 KiB
HTML
2915 lines
158 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=Inter:wght@200;300;400;500;600;700;800;900&family=JetBrains+Mono:wght@300;400;500;600&family=Noto+Sans+SC:wght@200;300;400;500;700;900&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
/* ============ 主题色(默认: 🔵 克莱因蓝 IKB) ============
|
||
切换主题: 从 references/themes-swiss.md 复制对应的 :root 块
|
||
整体替换标有"主题色"注释的所有变量,其他散落的 var() 引用无需逐处改 */
|
||
--paper:#fafaf8; /* 主底色: 高级灰白 */
|
||
--paper-rgb:250,250,248;
|
||
--ink:#0a0a0a; /* 文字主色: 近黑 */
|
||
--ink-rgb:10,10,10;
|
||
--grey-1:#f0f0ee; /* 浅灰底 */
|
||
--grey-2:#d4d4d2; /* 中灰(分割线) */
|
||
--grey-3:#737373; /* 暗灰(辅助文字) */
|
||
--accent:#002FA7; /* 高亮色: 克莱因蓝 IKB */
|
||
--accent-rgb:0,47,167;
|
||
--accent-on:#ffffff; /* accent 上的反色文字 */
|
||
--accent-bright:#5B7BFF; /* 暗底高亮: IKB 提亮版 */
|
||
|
||
/* ============ Carbon 文本角色 token (role-based,代替 opacity) ============
|
||
亮底: primary 主文 / secondary 次文 / helper 辅助 / placeholder 占位
|
||
暗底: inverse 自动反向 */
|
||
--text-primary:#0a0a0a; /* = ink, 100% */
|
||
--text-secondary:#525252; /* gray 70 */
|
||
--text-helper:#737373; /* gray 60 */
|
||
--text-placeholder:#a3a3a3; /* gray 40 */
|
||
--text-on-color:#ffffff; /* accent/dark 反色 */
|
||
--border-subtle:#e0e0e0; /* gray 20, 极细分隔 */
|
||
--border-strong:#a3a3a3; /* gray 40, 强分隔 */
|
||
|
||
/* ============ 字体(跨主题固定) ============ */
|
||
--sans:"Inter","Helvetica Neue","Helvetica","Arial","Segoe UI Variable","Segoe UI",system-ui,-apple-system,sans-serif;
|
||
--sans-zh:"PingFang SC","Hiragino Sans GB","Source Han Sans SC","Noto Sans SC","Microsoft YaHei UI","Microsoft YaHei","微软雅黑",sans-serif;
|
||
--mono:"JetBrains Mono","IBM Plex Mono","SF Mono","Cascadia Code","Consolas","Courier New",ui-monospace,monospace;
|
||
|
||
/* ============ Carbon 2x Grid 间距模数 (基础 8px) ============
|
||
参考: https://carbondesignsystem.com/elements/2x-grid/overview/
|
||
任何 padding/gap/margin 优先用这套 token,确保 8px 基线对齐 */
|
||
--sp-3:8px; /* 02 token */
|
||
--sp-4:12px; /* 03 */
|
||
--sp-5:16px; /* 04 */
|
||
--sp-6:24px; /* 05 */
|
||
--sp-7:32px; /* 06 */
|
||
--sp-8:40px; /* 07 */
|
||
--sp-9:48px; /* 08 */
|
||
--sp-10:64px; /* 09 */
|
||
--sp-11:80px; /* 10 */
|
||
--sp-12:96px; /* 11 */
|
||
--sp-13:160px; /* 12 */
|
||
|
||
/* ============ Carbon Motion tokens ============
|
||
https://carbondesignsystem.com/guidelines/motion/overview/
|
||
两套体系:productive (功能) 短 + 锐 / expressive (叙事) 长 + 软 */
|
||
--ease-prod:cubic-bezier(.2,0,.38,.9); /* productive standard */
|
||
--ease-exp:cubic-bezier(.4,.14,.3,1); /* expressive standard */
|
||
--ease-entry-prod:cubic-bezier(0,0,.38,.9); /* productive entrance */
|
||
--ease-entry-exp:cubic-bezier(0,0,.3,1); /* expressive entrance */
|
||
--dur-fast-1:.07s; /* 70ms */
|
||
--dur-fast-2:.11s; /* 110ms */
|
||
--dur-mod-1:.15s; /* 150ms */
|
||
--dur-mod-2:.24s; /* 240ms */
|
||
--dur-slow-1:.4s; /* 400ms */
|
||
--dur-slow-2:.7s; /* 700ms */
|
||
|
||
/* 底部分页组件安全区: 主内容最低处不要进入这条区域 */
|
||
--nav-safe-bottom:8vh;
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
html,body{
|
||
width:100%;height:100%;overflow:hidden;
|
||
background:var(--paper);color:var(--ink);
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-feature-settings:"ss01","cv11";
|
||
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility
|
||
}
|
||
|
||
/* ============ WebGL 网格背景 ============ */
|
||
canvas.bg{position:fixed;inset:0;width:100vw;height:100vh;z-index:0;display:block;opacity:.55;mix-blend-mode:multiply;pointer-events:none}
|
||
body.dark-bg canvas.bg{mix-blend-mode:screen;opacity:.42}
|
||
body.low-power canvas.bg,
|
||
body.low-power canvas.ascii-bg{display:none!important}
|
||
|
||
/* ============ Deck 容器 + 翻页 ============ */
|
||
#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:5.5vh 5vw 7vh 5vw;
|
||
display:flex;flex-direction:column;
|
||
overflow:hidden;
|
||
background:var(--paper);color:var(--ink);
|
||
}
|
||
.slide.grey{background:var(--grey-1)}
|
||
.slide.dark{background:var(--ink);color:var(--paper)}
|
||
.slide.dark .grey-only{display:none}
|
||
.slide.accent{background:var(--accent);color:var(--accent-on)}
|
||
.slide.accent .accent-block{background:var(--accent-on);color:var(--accent)}
|
||
|
||
/* Hero 页透出网格背景多一点 */
|
||
.slide.hero{background:transparent}
|
||
.slide.hero.grey{background:rgba(var(--paper-rgb),.86);backdrop-filter:blur(1px)}
|
||
.slide.hero.dark{background:rgba(var(--ink-rgb),.92);color:var(--paper)}
|
||
|
||
/* ============ 装饰: 极细分隔线 + 点阵矩阵 ============ */
|
||
.rule{width:100%;height:1px;background:currentColor;opacity:.18;margin:0}
|
||
.rule.thick{height:2px;opacity:.85}
|
||
.rule.accent{background:var(--accent);opacity:1;height:2px}
|
||
.rule.v{width:1px;height:100%;margin:0}
|
||
|
||
/* 点阵矩阵 (dot matrix) - 用 radial-gradient */
|
||
.dots{
|
||
background-image:radial-gradient(currentColor 1px, transparent 1px);
|
||
background-size:12px 12px;
|
||
background-position:0 0;
|
||
opacity:.18;
|
||
}
|
||
.dots-fine{
|
||
background-image:radial-gradient(currentColor 0.8px, transparent 0.8px);
|
||
background-size:8px 8px;
|
||
opacity:.14;
|
||
}
|
||
.dots-bold{
|
||
background-image:radial-gradient(currentColor 1.4px, transparent 1.4px);
|
||
background-size:18px 18px;
|
||
opacity:.22;
|
||
}
|
||
|
||
/* ============ Chrome (顶部 meta) + Foot (底部) ============ */
|
||
.chrome{
|
||
display:flex;justify-content:space-between;align-items:flex-start;
|
||
font-family:var(--mono);
|
||
font-size:14px;letter-spacing:.16em;text-transform:uppercase;
|
||
opacity:.7;margin-bottom:auto
|
||
}
|
||
.chrome .l,.chrome .r{display:flex;gap:1.6em;align-items:center}
|
||
.chrome .sep{width:24px;height:1px;background:currentColor;opacity:.5}
|
||
|
||
.foot{
|
||
margin-top:auto;
|
||
display:flex;justify-content:space-between;align-items:flex-end;
|
||
font-family:var(--mono);
|
||
font-size:14px;letter-spacing:.14em;text-transform:uppercase;
|
||
opacity:.55;
|
||
padding-top:2vh;
|
||
border-top:1px solid currentColor;
|
||
border-color:rgba(127,127,127,.25)
|
||
}
|
||
.foot .nb{font-family:var(--sans);font-weight:600;letter-spacing:.04em}
|
||
|
||
/* ============ Tag / Kicker / Meta 标签 ============ */
|
||
.kicker{
|
||
font-family:var(--mono);
|
||
font-size:14px;letter-spacing:.24em;text-transform:uppercase;
|
||
opacity:.65;margin-bottom:2.4vh;
|
||
display:inline-flex;align-items:center;gap:.8em
|
||
}
|
||
.kicker::before{
|
||
content:"";width:24px;height:1px;background:currentColor;opacity:.6
|
||
}
|
||
.kicker.no-line::before{display:none}
|
||
.kicker.accent{color:var(--accent);opacity:1;font-weight:600}
|
||
|
||
.tag{
|
||
display:inline-block;
|
||
font-family:var(--mono);
|
||
font-size:14px;letter-spacing:.18em;text-transform:uppercase;
|
||
padding:5px 10px;border:1px solid currentColor;opacity:.85
|
||
}
|
||
.tag.solid{background:currentColor;color:var(--paper);border-color:transparent}
|
||
.tag.accent{background:var(--accent);color:var(--accent-on);border-color:transparent;opacity:1}
|
||
|
||
/* ============ 标题层级 (无衬线 · 极轻字重 · 极致字号对比) ============ */
|
||
.h-hero{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:200;
|
||
font-size:11vw;
|
||
line-height:.92;
|
||
letter-spacing:-.04em;
|
||
}
|
||
.h-hero-zh{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:200;
|
||
font-size:8.4vw;
|
||
line-height:.96;
|
||
letter-spacing:-.025em;
|
||
}
|
||
.h-xl{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:200;
|
||
font-size:6vw;
|
||
line-height:1;
|
||
letter-spacing:-.03em;
|
||
}
|
||
.h-xl-zh{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:200;
|
||
font-size:5vw;
|
||
line-height:1.05;
|
||
letter-spacing:-.025em;
|
||
}
|
||
.h-md{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:300;
|
||
font-size:2.6vw;
|
||
line-height:1.18;
|
||
letter-spacing:-.015em;
|
||
}
|
||
.h-sub{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:400;
|
||
font-size:2.2vw;
|
||
line-height:1.3;
|
||
letter-spacing:-.01em;
|
||
opacity:.7;
|
||
}
|
||
|
||
/* ============ 正文 / 引语 / 元数据 ============ */
|
||
.lead{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:400;
|
||
font-size:1.55vw;
|
||
line-height:1.4;
|
||
letter-spacing:-.005em;
|
||
opacity:.86;
|
||
}
|
||
.body{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:400;
|
||
font-size:max(18px,1.08vw);
|
||
line-height:1.6;
|
||
letter-spacing:0;
|
||
opacity:.78;
|
||
}
|
||
.body-sm{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:400;
|
||
font-size:max(16px,.92vw);
|
||
line-height:1.55;
|
||
opacity:.7;
|
||
}
|
||
.meta{
|
||
font-family:var(--mono);
|
||
font-size:max(14px,.82vw);
|
||
letter-spacing:.18em;text-transform:uppercase;
|
||
opacity:.6;
|
||
}
|
||
.meta-row{
|
||
display:flex;gap:1.4em;align-items:baseline;flex-wrap:wrap;
|
||
font-family:var(--mono);
|
||
font-size:max(14px,.88vw);letter-spacing:.16em;text-transform:uppercase;
|
||
opacity:.65;
|
||
}
|
||
.meta-row span:not(.dot){display:inline-block}
|
||
.meta-row .dot{
|
||
display:inline-block;width:4px;height:4px;border-radius:50%;
|
||
background:currentColor;opacity:.5;vertical-align:middle
|
||
}
|
||
|
||
/* ============ KPI Hero (视觉英雄数据) ============ */
|
||
.kpi-hero{
|
||
font-family:var(--sans);
|
||
font-weight:800;
|
||
font-size:22vw;
|
||
line-height:.82;
|
||
letter-spacing:-.05em;
|
||
font-feature-settings:"tnum","ss01";
|
||
}
|
||
.kpi-hero .unit{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:500;
|
||
font-size:.18em;
|
||
letter-spacing:0;
|
||
opacity:.5;
|
||
margin-left:.12em;
|
||
vertical-align:.5em;
|
||
}
|
||
.kpi-hero.accent{color:var(--accent)}
|
||
.kpi-big{
|
||
font-family:var(--sans);
|
||
font-weight:800;
|
||
font-size:11vw;
|
||
line-height:.85;
|
||
letter-spacing:-.04em;
|
||
font-feature-settings:"tnum"
|
||
}
|
||
.kpi-mid{
|
||
font-family:var(--sans);
|
||
font-weight:700;
|
||
font-size:6vw;
|
||
line-height:.88;
|
||
letter-spacing:-.03em;
|
||
font-feature-settings:"tnum"
|
||
}
|
||
|
||
/* ============ Stat Card (数据卡片 · 极简) ============ */
|
||
.stat-card{
|
||
display:flex;flex-direction:column;
|
||
gap:.6vh;align-items:flex-start;
|
||
padding-top:1.6vh;
|
||
border-top:2px solid currentColor;
|
||
}
|
||
.stat-card.thin{border-top-width:1px;border-color:rgba(127,127,127,.4)}
|
||
.stat-card.accent-top{border-top-color:var(--accent);border-top-width:3px}
|
||
.stat-card .stat-label{
|
||
font-family:var(--mono);
|
||
font-size:max(14px,.82vw);
|
||
letter-spacing:.24em;text-transform:uppercase;
|
||
opacity:.6;
|
||
}
|
||
.stat-card .stat-nb{
|
||
font-family:var(--sans);
|
||
font-weight:800;
|
||
font-size:5.6vw;
|
||
line-height:.88;
|
||
letter-spacing:-.035em;
|
||
font-feature-settings:"tnum";
|
||
margin-top:.4vh;
|
||
}
|
||
.stat-card .stat-nb .stat-unit{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:500;
|
||
font-size:.32em;
|
||
letter-spacing:0;
|
||
opacity:.6;
|
||
margin-left:.14em;
|
||
vertical-align:.4em;
|
||
}
|
||
.stat-card .stat-note{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:400;
|
||
font-size:max(16px,.98vw);
|
||
line-height:1.5;
|
||
opacity:.7;
|
||
margin-top:.6vh;
|
||
}
|
||
.grid-4 .stat-card .stat-nb{font-size:4.6vw}
|
||
.grid-3 .stat-card .stat-nb{font-size:6.4vw}
|
||
.grid-6 .stat-card .stat-nb{font-size:4vw}
|
||
|
||
/* ============ Accent Block (高亮色块包裹内容) ============ */
|
||
.accent-block{
|
||
background:var(--accent);color:var(--accent-on);
|
||
padding:2.4vh 2vw;
|
||
}
|
||
.accent-block.tight{padding:1.4vh 1.4vw}
|
||
.accent-block .h-md,.accent-block .h-xl,.accent-block .kpi-mid{color:var(--accent-on)}
|
||
|
||
.ink-block{
|
||
background:var(--ink);color:var(--paper);
|
||
padding:2.4vh 2vw
|
||
}
|
||
.grey-block{
|
||
background:var(--grey-1);
|
||
padding:2.4vh 2vw
|
||
}
|
||
|
||
/* 高亮文字 (mark 风格) */
|
||
.mark{
|
||
background:var(--accent);color:var(--accent-on);
|
||
padding:0 .2em;
|
||
box-decoration-break:clone;
|
||
-webkit-box-decoration-break:clone;
|
||
}
|
||
.mark.ink{background:var(--ink);color:var(--paper)}
|
||
.underline-accent{
|
||
background-image:linear-gradient(to bottom, transparent 70%, var(--accent) 70%, var(--accent) 96%, transparent 96%);
|
||
padding:0 .05em
|
||
}
|
||
|
||
/* ============ 网格系统 (模块化网格) ============ */
|
||
.frame{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}
|
||
.frame.grid-2-7-5,
|
||
.frame.grid-2-6-6,
|
||
.frame.grid-2-8-4,
|
||
.frame.grid-2-4-8,
|
||
.frame.grid-3-3,
|
||
.frame.grid-12,
|
||
.frame.grid-6,
|
||
.frame.grid-4,
|
||
.frame.grid-3{display:grid}
|
||
|
||
/* 12 列模块化网格 (瑞士风核心) */
|
||
.grid-12{
|
||
display:grid;
|
||
grid-template-columns:repeat(12,1fr);
|
||
gap:2vh 1.2vw;
|
||
align-items:start;
|
||
}
|
||
.span-2{grid-column:span 2}
|
||
.span-3{grid-column:span 3}
|
||
.span-4{grid-column:span 4}
|
||
.span-5{grid-column:span 5}
|
||
.span-6{grid-column:span 6}
|
||
.span-7{grid-column:span 7}
|
||
.span-8{grid-column:span 8}
|
||
.span-9{grid-column:span 9}
|
||
.span-12{grid-column:span 12}
|
||
|
||
/* 经典分栏 */
|
||
.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-2-4-8{display:grid;grid-template-columns:4fr 8fr;gap:3vw 4vh;align-items:start}
|
||
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);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-4{display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr);gap:3vh 3vw;flex:1;align-content:center}
|
||
.grid-6{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr);gap:3vh 2.4vw;flex:1;align-content:center}
|
||
|
||
/* 工具类 */
|
||
.col{display:flex;flex-direction:column;gap:2vh}
|
||
.row{display:flex;align-items:center;gap:2vw}
|
||
.fill{flex:1}
|
||
.center{align-items:center;justify-content:center;text-align:center}
|
||
.right{text-align:right;justify-self:end}
|
||
.top{align-self:start}
|
||
.bottom{align-self:end}
|
||
.va-center{align-self:center}
|
||
.nowrap{white-space:nowrap}
|
||
|
||
/* 非对称定位 */
|
||
.pos-absolute{position:absolute}
|
||
.bottom-left{position:absolute;left:5vw;bottom:8vh;max-width:50vw}
|
||
.bottom-right{position:absolute;right:5vw;bottom:8vh;max-width:50vw;text-align:right}
|
||
.top-right{position:absolute;right:5vw;top:5.5vh;text-align:right}
|
||
.top-left{position:absolute;left:5vw;top:5.5vh}
|
||
|
||
/* ============ Callout (引用框 · 极简) ============ */
|
||
.callout{
|
||
padding:2vh 2vw;
|
||
border-left:3px solid var(--accent);
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-size:max(14px,1vw);
|
||
line-height:1.55;
|
||
opacity:.9;
|
||
}
|
||
.callout.ink{border-left-color:currentColor}
|
||
.callout .cite,.callout .callout-src{
|
||
display:block;margin-top:1.2vh;
|
||
font-family:var(--mono);
|
||
font-size:14px;letter-spacing:.16em;text-transform:uppercase;
|
||
opacity:.6;
|
||
}
|
||
|
||
/* ============ Icons (Lucide via CDN) ============ */
|
||
.ico{width:1em;height:1em;display:inline-block;vertical-align:-.12em;stroke:currentColor;fill:none;stroke-width:1.6;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.4vw;height:2.4vw;stroke-width:1.4;display:inline-block}
|
||
.ico-md{width:1.6vw;height:1.6vw;stroke-width:1.6;display:inline-block;vertical-align:-.4em}
|
||
.ico-sm{width:1vw;height:1vw;stroke-width:1.8;display:inline-block;vertical-align:-.15em;opacity:.7}
|
||
|
||
/* ============ 几何小图标 (装饰) ============ */
|
||
.geo-dot{width:.7vw;height:.7vw;border-radius:50%;background:var(--accent);display:inline-block;vertical-align:middle}
|
||
.geo-square{width:.7vw;height:.7vw;background:var(--accent);display:inline-block;vertical-align:middle}
|
||
.geo-line{width:2vw;height:2px;background:var(--accent);display:inline-block;vertical-align:middle}
|
||
.geo-circle-o{width:.9vw;height:.9vw;border:2px solid currentColor;border-radius:50%;display:inline-block;vertical-align:middle}
|
||
|
||
/* ============ 图片 frame-img ============ */
|
||
.frame-img{overflow:hidden;position:relative;background:var(--paper);box-sizing:border-box;width:100%}
|
||
.slide.dark .frame-img{background:rgba(255,255,255,.06)}
|
||
.frame-img > img{width:100%;height:100%;object-fit:cover;object-position:center center;display:block}
|
||
.frame-img.fit-contain > img{object-fit:contain;object-position:center center}
|
||
.frame-img.pos-top > img{object-position:top center}
|
||
.frame-img.pos-face > img{object-position:center 35%}
|
||
.frame-img.r-16x9{aspect-ratio:16/9;max-height:64vh}
|
||
.frame-img.r-21x9{aspect-ratio:21/9;max-height:54vh}
|
||
.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-img.h-32{height:32vh}
|
||
.frame-img.swiss-lined{border-top:2px solid var(--accent)}
|
||
.frame-img.swiss-keyline{border:0}
|
||
|
||
/* P22 Image Hero: 下半屏内容区必须和图片拉开距离,不要贴在图下沿 */
|
||
.image-hero-body{
|
||
display:grid;grid-template-columns:6fr 6fr;gap:3vw;
|
||
padding:6.6vh 5vw 4.4vh;flex:1;align-content:start
|
||
}
|
||
.image-hero-stats{
|
||
display:grid;grid-template-columns:repeat(3,1fr);gap:2vw;align-items:start
|
||
}
|
||
.img-cap{
|
||
display:block;margin-top:.8vh;
|
||
font-family:var(--mono);
|
||
font-size:max(14px,.82vw);
|
||
letter-spacing:.2em;text-transform:uppercase;
|
||
opacity:.6;
|
||
}
|
||
figure.frame-img,figure.tile{margin:0;display:flex;flex-direction:column;min-width:0}
|
||
figure.tile > .frame-img{flex:0 0 auto}
|
||
|
||
/* 瑞士风图文混排: 只用直角、发丝线、单一 accent;图像容器不加圆角/阴影 */
|
||
.swiss-img-split{display:grid;grid-template-columns:5fr 7fr;gap:3vw;align-items:start;flex:1;min-height:0}
|
||
.swiss-img-split.reverse{grid-template-columns:7fr 5fr}
|
||
.swiss-img-split.align-bottom{align-items:end}
|
||
.swiss-img-split.align-bottom .swiss-img-copy{align-self:end}
|
||
.swiss-img-split.align-image-bottom{align-items:end;padding-bottom:var(--nav-safe-bottom)}
|
||
.swiss-img-split.align-image-bottom .swiss-img-copy{align-self:end}
|
||
.swiss-img-split.align-image-bottom figure.tile{align-self:end;position:relative}
|
||
.swiss-img-split.align-image-bottom figure.tile > .swiss-img-caption{
|
||
position:absolute;left:0;right:0;top:calc(100% + var(--sp-4));margin-top:0
|
||
}
|
||
.nav-safe-bottom{padding-bottom:var(--nav-safe-bottom)}
|
||
.nav-safe-bottom-tight{padding-bottom:calc(var(--nav-safe-bottom) * .65)}
|
||
.swiss-img-copy{display:flex;flex-direction:column;gap:var(--sp-6);min-width:0}
|
||
.swiss-img-copy .rule{margin:0}
|
||
.swiss-img-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-5);align-items:start;margin-top:var(--sp-7)}
|
||
.swiss-img-grid.two{grid-template-columns:repeat(2,1fr)}
|
||
.swiss-img-grid.tight{margin-top:0}
|
||
.swiss-img-caption{
|
||
display:flex;justify-content:space-between;gap:var(--sp-5);
|
||
margin-top:var(--sp-4);
|
||
font-family:var(--mono);font-size:max(14px,.74vw);
|
||
letter-spacing:.16em;text-transform:uppercase;color:var(--text-helper);
|
||
border-top:1px solid var(--border-subtle);padding-top:var(--sp-4)
|
||
}
|
||
.swiss-img-caption strong{
|
||
font-family:var(--sans),var(--sans-zh);font-size:max(16px,.9vw);
|
||
font-weight:600;letter-spacing:0;text-transform:none;color:var(--text-primary)
|
||
}
|
||
|
||
/* ============ Bar Chart (扁平几何图表) ============ */
|
||
.bar-chart{display:flex;flex-direction:column;gap:1.4vh}
|
||
.bar-row{display:grid;grid-template-columns:8em 1fr 4em;gap:1.4vw;align-items:center}
|
||
.bar-row .bar-label{font-family:var(--mono);font-size:max(14px,.84vw);letter-spacing:.14em;text-transform:uppercase;opacity:.7}
|
||
.bar-row .bar-track{height:14px;background:rgba(127,127,127,.18);position:relative}
|
||
.bar-row .bar-fill{height:100%;background:var(--accent);position:absolute;left:0;top:0}
|
||
.bar-row .bar-fill.ink{background:currentColor}
|
||
.bar-row .bar-value{font-family:var(--sans);font-weight:700;font-size:max(16px,1.05vw);text-align:right;font-feature-settings:"tnum"}
|
||
|
||
/* ============ 导航 ============ */
|
||
/* 底部分页导航: 仅保留方块本身,无背景无描边 */
|
||
#nav{position:fixed;left:50%;bottom:2vh;transform:translateX(-50%);z-index:30;display:flex;gap:10px;padding:0;background:transparent;border:0}
|
||
#nav .dot{width:6px;height:6px;background:rgba(0,0,0,.28);cursor:pointer;transition:all .25s ease;border:0;padding:0;border-radius:0}
|
||
#nav .dot:hover{background:rgba(0,0,0,.55)}
|
||
#nav .dot.active{background:var(--accent);width:18px}
|
||
body.dark-bg #nav .dot{background:rgba(255,255,255,.32)}
|
||
body.dark-bg #nav .dot.active{background:var(--accent)}
|
||
|
||
#hint{position:fixed;bottom:2.4vh;right:2.5vw;z-index:30;display:flex;align-items:center;gap:.65em;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;opacity:.4}
|
||
#hint button{all:unset;display:inline-flex;align-items:center;gap:.42em;cursor:pointer;color:inherit;white-space:nowrap}
|
||
#hint button:hover,#hint button:focus-visible{opacity:1;text-decoration:underline;text-underline-offset:.3em}
|
||
#hint .ppt-key{padding:.12em .34em;border:1px solid currentColor;letter-spacing:.06em}
|
||
#hint .ppt-sep{opacity:.55}
|
||
body.dark-bg #hint{color:var(--paper);opacity:.4}
|
||
body.low-power #hint{color:var(--accent);opacity:.72}
|
||
body.dark-bg.low-power #hint{color:var(--paper);opacity:.72}
|
||
|
||
/* ============ 演讲者模式 ============ */
|
||
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}}
|
||
|
||
/* ESC 索引页: 动画元素强制可见 (覆盖 motion-ready 的 opacity:0) */
|
||
#overview [data-anim]{opacity:1!important;transform:none!important}
|
||
#overview .slide *{animation:none!important;transition:none!important}
|
||
|
||
/* 统一卡片样式 token: card-fill (默认灰底 · 中性) + card-ink (反转高对比) + card-accent (单一焦点) */
|
||
.card-fill{background:#f5f5f4;border:0;color:var(--text-primary)}
|
||
.card-ink{background:var(--ink);border:0;color:var(--paper)}
|
||
.card-ink .t-meta,.card-ink .t-cat{color:rgba(255,255,255,.6)}
|
||
.card-accent{background:var(--accent);border:0;color:var(--accent-on)}
|
||
.card-accent .t-meta,.card-accent .t-cat{color:var(--accent-on)}
|
||
|
||
/* ============ 动效 (与原模板一致) ============ */
|
||
[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.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}
|
||
|
||
/* ============================================================
|
||
↓↓↓ V2 EXTENSIONS · Canvas Mode + 参考图新类
|
||
验证效果后沉淀回 template-swiss.html
|
||
============================================================ */
|
||
|
||
/* Windows 适配:雅黑没有 ExtraLight 200,中文大字号字重补偿 */
|
||
body.is-win .name-mega,
|
||
body.is-win .num-mega,
|
||
body.is-win .kpi-thin,
|
||
body.is-win .tl-node .multi{
|
||
font-weight:300;letter-spacing:-.02em;
|
||
}
|
||
body.is-win [style*="font-weight:200"]{font-weight:300 !important}
|
||
|
||
/* 全屏铺满模式 · 关闭 WebGL · 卡片即页面 */
|
||
body.canvas-mode{background:var(--paper)}
|
||
body.canvas-mode canvas.bg{display:none !important}
|
||
body.canvas-mode .slide{background:var(--paper);padding:0;align-items:stretch;justify-content:stretch}
|
||
body.canvas-mode .slide.hero{background:var(--paper)}
|
||
|
||
.canvas-card{
|
||
width:100vw;
|
||
height:100vh;
|
||
background:var(--paper);color:var(--ink);
|
||
padding:5.6vh 5vw 4.4vh;
|
||
display:flex;flex-direction:column;
|
||
position:relative;overflow:hidden;
|
||
box-shadow:none;
|
||
border-radius:0;
|
||
}
|
||
.slide.dark .canvas-card{background:var(--ink);color:var(--paper)}
|
||
.slide.accent .canvas-card{background:var(--accent);color:var(--accent-on)}
|
||
.slide.grey .canvas-card{background:var(--grey-1);color:var(--ink)}
|
||
.slide.split .canvas-card{padding:0;flex-direction:row}
|
||
|
||
/* ============ ASCII 点阵呼吸场 · IKB 封面/封底专用 ============
|
||
用法:在 .canvas-card(或 split .half.b-accent)内首位插入 <canvas class="ascii-bg" aria-hidden="true">.
|
||
动画由本文件底部的 ASCII IIFE 自动启动,所有 canvas.ascii-bg 都会被扫到.
|
||
其他内容靠 .canvas-card > *:not(.ascii-bg){z-index:1} 自动浮在上层. */
|
||
canvas.ascii-bg{
|
||
position:absolute;inset:0;width:100%;height:100%;
|
||
pointer-events:none;z-index:0;
|
||
mix-blend-mode:screen;opacity:.92;
|
||
}
|
||
.canvas-card > *:not(.ascii-bg){position:relative;z-index:1}
|
||
.slide.accent .canvas-card .chrome-min{color:rgba(255,255,255,.62)}
|
||
.slide.accent .canvas-card .t-meta{color:rgba(255,255,255,.7)}
|
||
.split-half > .half.b-accent .chrome-min{color:rgba(255,255,255,.62)}
|
||
|
||
/* 编号目录页 · 超大数字 + 国家名风格 — 越大越细 */
|
||
.num-mega{
|
||
font-family:var(--sans);font-weight:200;
|
||
font-size:9vw;line-height:1;letter-spacing:-.04em;
|
||
font-feature-settings:"tnum"
|
||
}
|
||
.num-mega.thin{font-weight:200}
|
||
.name-mega{
|
||
font-family:var(--sans);font-weight:200;
|
||
font-size:9vw;line-height:1;letter-spacing:-.035em;
|
||
}
|
||
.name-mega.muted{color:var(--grey-3)}
|
||
|
||
/* 细体超大 KPI — 字号越大权重越低 */
|
||
.kpi-thin{
|
||
font-family:var(--sans);font-weight:200;
|
||
font-size:14vw;line-height:.92;letter-spacing:-.045em;
|
||
font-feature-settings:"tnum"
|
||
}
|
||
.kpi-thin .unit{font-size:.3em;font-weight:300;opacity:.55;margin-left:.15em;vertical-align:.6em}
|
||
.kpi-thin.accent{color:var(--accent)}
|
||
.kpi-thin-sm{
|
||
font-family:var(--sans);font-weight:250;
|
||
font-size:5.6vw;line-height:1.04;letter-spacing:-.03em;
|
||
font-feature-settings:"tnum"
|
||
}
|
||
.kpi-thin-sm .unit{font-size:.3em;font-weight:300;opacity:.55;margin-left:.12em;vertical-align:.45em}
|
||
|
||
/* 4 列细线 KPI 行 — 顶部一根 hairline,内部不再加竖线 */
|
||
/* 4 列 KPI: 用纵向分割线建立网格感 (Carbon 2x grid 模数) */
|
||
.kpi-row-4{
|
||
display:grid;grid-template-columns:repeat(4,1fr);
|
||
gap:0;padding-top:2.4vh;
|
||
border-top:1px solid var(--grey-2)
|
||
}
|
||
.kpi-row-4 > .kpi-cell{
|
||
padding:1.6vh 1.6vw 0;
|
||
border-left:1px solid var(--grey-2)
|
||
}
|
||
.kpi-row-4 > .kpi-cell:first-child{padding-left:0;border-left:none}
|
||
.kpi-cell .lbl{font-family:var(--mono);font-size:max(14px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.55;margin-bottom:1.2vh}
|
||
.kpi-cell .nb{font-family:var(--sans);font-weight:250;font-size:3.2vw;line-height:1;letter-spacing:-.025em;font-feature-settings:"tnum"}
|
||
.kpi-cell .nb .unit{font-size:.32em;font-weight:300;opacity:.6;margin-left:.1em;vertical-align:.4em}
|
||
.kpi-cell .note{font-family:var(--sans),var(--sans-zh);font-size:max(16px,.92vw);line-height:1.5;opacity:.7;margin-top:1.2vh}
|
||
|
||
/* 时间线轴 — 通用 axis token, 横纵共享
|
||
axis 列 = 24px 固定宽,dot 直径 8px,绝对居中在 axis 列中线 (12px)
|
||
虚线绝对定位 left:12px,与 dot 中心严格对齐 */
|
||
.timeline-v{
|
||
--tl-axis-w:24px; /* axis 列固定宽度 */
|
||
--tl-dot:8px; /* 圆点直径 */
|
||
position:relative;margin-top:var(--sp-7)
|
||
}
|
||
.timeline-v::before{
|
||
content:"";position:absolute;
|
||
left:calc(var(--tl-axis-w) / 2);
|
||
transform:translateX(-50%);
|
||
top:var(--sp-5);bottom:var(--sp-5);
|
||
width:1px;
|
||
background:repeating-linear-gradient(to bottom,currentColor 0 4px,transparent 4px 8px);
|
||
opacity:.35;pointer-events:none;z-index:0
|
||
}
|
||
.tl-node{
|
||
position:relative;
|
||
display:grid;
|
||
grid-template-columns:var(--tl-axis-w) minmax(0,7em) minmax(0,7.6em) 1fr;
|
||
gap:0 var(--sp-5);
|
||
align-items:center;
|
||
padding:var(--sp-7) 0
|
||
}
|
||
/* 进度点: 纯色实心圆,无背景无描边,精确居中在 axis 列中线 */
|
||
.tl-node .dot{
|
||
width:var(--tl-dot);height:var(--tl-dot);
|
||
border-radius:50%;
|
||
background:currentColor;
|
||
justify-self:center;
|
||
z-index:1
|
||
}
|
||
.tl-node.accent .dot{background:var(--accent)}
|
||
.tl-node .yr{
|
||
font-family:var(--mono);font-weight:500;
|
||
font-size:max(14px,1vw);letter-spacing:.04em
|
||
}
|
||
.tl-node .multi{
|
||
font-family:var(--sans);font-weight:200;
|
||
font-size:clamp(28px,2.8vw,56px);
|
||
line-height:.95;letter-spacing:-.025em;
|
||
white-space:nowrap;
|
||
overflow:hidden;
|
||
min-width:0
|
||
}
|
||
.tl-node .multi .unit{
|
||
font-size:.36em;font-weight:300;opacity:.6;
|
||
margin-left:.2em;
|
||
vertical-align:.42em;
|
||
letter-spacing:0
|
||
}
|
||
.tl-node.accent .multi{color:var(--accent)}
|
||
.tl-node .desc{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-size:max(16px,.94vw);line-height:1.55;opacity:.78;
|
||
min-width:0
|
||
}
|
||
|
||
/* 横向时间线 (.timeline-h) — 与 .timeline-v 共享 axis token, 视觉语言一致 */
|
||
.timeline-h{
|
||
--tl-axis-w:8px;
|
||
--tl-dot:8px;
|
||
position:relative;
|
||
flex:1;
|
||
display:flex;align-items:center
|
||
}
|
||
.timeline-h::before{
|
||
content:"";position:absolute;
|
||
top:50%;left:5%;right:5%;height:1px;
|
||
transform:translateY(-50%);
|
||
background:repeating-linear-gradient(to right,currentColor 0 4px,transparent 4px 8px);
|
||
opacity:.35;pointer-events:none;z-index:0
|
||
}
|
||
.timeline-h .tl-row{
|
||
position:relative;width:100%;
|
||
display:grid;grid-template-columns:repeat(5,1fr);
|
||
align-items:center
|
||
}
|
||
.timeline-h .th-node{
|
||
position:relative;display:flex;justify-content:center
|
||
}
|
||
/* 横向 dot: 8px 纯色实心,无描边无阴影 (与 P2 保持一致) */
|
||
.timeline-h .th-node .dot{
|
||
width:var(--tl-dot);height:var(--tl-dot);
|
||
border-radius:50%;
|
||
background:var(--ink);
|
||
z-index:1;position:relative
|
||
}
|
||
.timeline-h .th-node.accent .dot{background:var(--accent)}
|
||
.timeline-h .th-node .label{
|
||
position:absolute;left:50%;transform:translateX(-50%);
|
||
width:13vw;text-align:center;
|
||
display:flex;flex-direction:column;gap:.4vh
|
||
}
|
||
.timeline-h .th-node.up .label{bottom:calc(50% + 22px)}
|
||
.timeline-h .th-node.down .label{top:calc(50% + 22px)}
|
||
.timeline-h .th-node .yr{
|
||
font-family:var(--mono);font-size:max(14px,.78vw);letter-spacing:.05em;
|
||
color:var(--text-helper);font-weight:500
|
||
}
|
||
.timeline-h .th-node.accent .yr{color:var(--accent)}
|
||
.timeline-h .th-node .name{
|
||
font-family:var(--sans);font-size:max(16px,1.05vw);font-weight:400;
|
||
color:var(--text-primary);line-height:1.2;letter-spacing:-.005em
|
||
}
|
||
.timeline-h .th-node.accent .name{color:var(--accent)}
|
||
.timeline-h .th-node .desc{
|
||
font-family:var(--sans),var(--sans-zh);font-size:max(14px,.84vw);
|
||
color:var(--text-secondary);font-weight:400;line-height:1.4
|
||
}
|
||
|
||
/* 几何图示 (参考图 5) — SVG-friendly 容器 */
|
||
.geo-icon{width:5vw;height:5vw;display:block;margin-bottom:2.2vh;color:var(--ink);flex-shrink:0}
|
||
.slide.dark .geo-icon{color:var(--paper)}
|
||
.geo-icon svg{width:100%;height:100%;overflow:visible}
|
||
.geo-icon .stroke{fill:none;stroke:currentColor;stroke-width:1.4}
|
||
.geo-icon .stroke-accent{fill:none;stroke:var(--accent);stroke-width:1.4}
|
||
.geo-icon .fill-accent{fill:var(--accent)}
|
||
|
||
/* 卡片网格 (参考图 1 卡片漂浮) — 在 canvas-card 内部再分卡 */
|
||
.sub-grid-3-2{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr);gap:1.4vh 1.4vw;flex:1;align-content:stretch;margin-top:3vh}
|
||
.sub-card{
|
||
background:var(--grey-1);
|
||
padding:2.4vh 1.6vw 2vh;
|
||
display:flex;flex-direction:column;
|
||
position:relative;border-radius:3px;
|
||
min-height:0
|
||
}
|
||
.slide.dark .sub-card{background:rgba(255,255,255,.06)}
|
||
.sub-card.accent{background:var(--accent);color:var(--accent-on)}
|
||
.sub-card.ink{background:var(--ink);color:var(--paper)}
|
||
.sub-card .nb-corner{
|
||
position:absolute;top:1.6vh;right:1.4vw;
|
||
font-family:var(--mono);font-size:max(14px,.8vw);
|
||
letter-spacing:.16em;opacity:.55
|
||
}
|
||
.sub-card .ttl{font-family:var(--sans),var(--sans-zh);font-weight:500;font-size:max(17px,1.5vw);line-height:1.2;letter-spacing:-.015em;margin-bottom:1vh}
|
||
.sub-card .desc{font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.55;opacity:.78;margin-top:auto}
|
||
.sub-card .lucide{width:2.4vw;height:2.4vw;stroke-width:1.4;color:currentColor;margin-bottom:1.6vh;flex-shrink:0}
|
||
.sub-card.accent .lucide{color:var(--accent-on)}
|
||
|
||
/* 三层架构纯色块拼图 (参考图 4 + 6) — 横排等高色块 */
|
||
.stack-row{display:grid;grid-template-columns:repeat(3,1fr);gap:1.6vw;flex:1;margin-top:6vh;align-items:stretch}
|
||
.stack-block{
|
||
display:flex;flex-direction:column;
|
||
padding:3.2vh 1.8vw 2.4vh;
|
||
position:relative;
|
||
min-height:0
|
||
}
|
||
.stack-block.b-accent{background:var(--accent);color:var(--accent-on)}
|
||
.stack-block.b-grey{background:var(--grey-1);color:var(--ink)}
|
||
.stack-block.b-ink{background:var(--ink);color:var(--paper)}
|
||
.stack-block .layer-nb{font-family:var(--mono);font-size:max(14px,.84vw);letter-spacing:.18em;opacity:.65;margin-bottom:auto}
|
||
.stack-block .layer-icon{margin-bottom:1.6vh}
|
||
.stack-block .layer-icon svg{width:3vw;height:3vw}
|
||
.stack-block .layer-icon .stroke{fill:none;stroke:currentColor;stroke-width:1.6}
|
||
.stack-block .layer-ttl{font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(17px,2vw);line-height:1.1;margin-top:1vh;letter-spacing:-.02em}
|
||
.stack-block .layer-desc{font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(16px,.94vw);line-height:1.55;opacity:.88;margin-top:1.4vh}
|
||
.stack-block .lucide{width:2.6vw;height:2.6vw;stroke-width:1.4;margin-bottom:1.6vh;flex-shrink:0}
|
||
.stack-block .layer-tag{font-family:var(--mono);font-size:max(14px,.74vw);letter-spacing:.16em;text-transform:uppercase;opacity:.7;margin-top:1.6vh;border-top:1px solid currentColor;padding-top:1vh}
|
||
|
||
/* 不等高柱状 KPI 塔 (参考图 6) */
|
||
.bar-towers{
|
||
display:grid;grid-template-columns:repeat(4,1fr);
|
||
gap:1.2vw;flex:1;align-items:end;margin-top:auto
|
||
}
|
||
.bar-tower{
|
||
display:flex;flex-direction:column;justify-content:flex-end;
|
||
min-height:0;height:100%
|
||
}
|
||
.bar-tower .cap{
|
||
background:var(--grey-1);
|
||
height:5.6vh;
|
||
display:flex;align-items:center;justify-content:center;
|
||
margin-bottom:.4vh
|
||
}
|
||
.bar-tower .cap svg{width:1.6vw;height:1.6vw;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
|
||
.bar-tower .body-block{
|
||
flex:0 1 auto;
|
||
padding:2vh 1.2vw 2vh;
|
||
display:flex;flex-direction:column;justify-content:flex-end;
|
||
min-height:18vh
|
||
}
|
||
.bar-tower .body-block.h-1{min-height:22vh}
|
||
.bar-tower .body-block.h-2{min-height:30vh}
|
||
.bar-tower .body-block.h-3{min-height:38vh}
|
||
.bar-tower .body-block.h-4{min-height:46vh}
|
||
/* 默认所有 KPI 塔统一为浅描边卡 — 不抢戏;只有 .b-accent 突出为 IKB */
|
||
.bar-tower .body-block{background:var(--paper);color:var(--ink);border:1px solid var(--grey-2)}
|
||
.bar-tower .body-block.b-accent{background:var(--accent);color:var(--accent-on);border-color:var(--accent)}
|
||
.bar-tower .lbl{font-family:var(--mono);font-size:max(14px,.82vw);letter-spacing:.16em;text-transform:uppercase;opacity:.65;margin-bottom:1vh}
|
||
.bar-tower .nb{font-family:var(--sans);font-weight:250;font-size:max(20px,2.8vw);line-height:1;letter-spacing:-.03em;font-feature-settings:"tnum"}
|
||
.bar-tower .body-block.b-accent .nb{font-weight:300}
|
||
.bar-tower .nb .unit{font-size:.36em;font-weight:300;opacity:.7;margin-left:.08em;vertical-align:.4em}
|
||
.bar-tower .sub{font-family:var(--sans),var(--sans-zh);font-size:max(16px,.92vw);opacity:.75;margin-top:1.2vh;line-height:1.5}
|
||
.bar-tower .cap{background:var(--grey-1);color:var(--ink)}
|
||
.bar-tower .lucide{width:1.6vw;height:1.6vw;stroke-width:1.4}
|
||
|
||
/* ============ Carbon Productive Type Tokens ============
|
||
用于 PPT 中的"productive 时刻": 列表、表格行、说明文、章节标签
|
||
固定 px 字号,密集紧凑;与 vw-based 的 Expressive 巨字形成双极对比 */
|
||
|
||
/* category label / eyebrow / section tag — small, bold, uppercase */
|
||
.t-cat{
|
||
font-family:var(--mono);
|
||
font-size:14px;font-weight:600;
|
||
letter-spacing:.15em;text-transform:uppercase;
|
||
color:var(--text-helper);line-height:1.3
|
||
}
|
||
.t-cat.accent{color:var(--accent)}
|
||
.t-cat.on-dark{color:rgba(255,255,255,.78)}
|
||
|
||
/* page chrome / breadcrumb / running header — extra-small mono */
|
||
.t-meta{
|
||
font-family:var(--mono);
|
||
font-size:14px;font-weight:500;
|
||
letter-spacing:.14em;text-transform:uppercase;
|
||
color:var(--text-helper);line-height:1.45
|
||
}
|
||
|
||
/* helper / caption — secondary text */
|
||
.t-helper{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-size:14px;font-weight:400;
|
||
color:var(--text-helper);line-height:1.5;
|
||
letter-spacing:.005em
|
||
}
|
||
|
||
/* body small — list items, table rows, captions */
|
||
.t-body-sm{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-size:16px;font-weight:400;
|
||
color:var(--text-secondary);line-height:1.55;
|
||
letter-spacing:0
|
||
}
|
||
|
||
/* body — paragraphs, descriptions */
|
||
.t-body{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-size:18px;font-weight:400;
|
||
color:var(--text-primary);line-height:1.5;
|
||
letter-spacing:-.005em
|
||
}
|
||
|
||
/* body emphasis — 强调正文 */
|
||
.t-body-emp{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-size:18px;font-weight:600; /* SemiBold per Carbon */
|
||
color:var(--text-primary);line-height:1.5;
|
||
letter-spacing:-.005em
|
||
}
|
||
|
||
/* productive heading — section title within slide */
|
||
.t-h-prod{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-size:22px;font-weight:600;
|
||
color:var(--text-primary);line-height:1.4;
|
||
letter-spacing:-.01em
|
||
}
|
||
|
||
/* dark background variants */
|
||
.slide.dark .t-cat,.slide.dark .t-meta,.slide.dark .t-helper{color:rgba(255,255,255,.62)}
|
||
.slide.dark .t-body-sm{color:rgba(255,255,255,.78)}
|
||
.slide.dark .t-body,.slide.dark .t-body-emp,.slide.dark .t-h-prod{color:var(--paper)}
|
||
.split-half .half.b-ink .t-cat,.split-half .half.b-ink .t-meta,.split-half .half.b-ink .t-helper{color:rgba(255,255,255,.62)}
|
||
.split-half .half.b-ink .t-body-sm{color:rgba(255,255,255,.78)}
|
||
.split-half .half.b-ink .t-body,.split-half .half.b-ink .t-body-emp,.split-half .half.b-ink .t-h-prod{color:var(--paper)}
|
||
|
||
/* 斜杠点阵装饰 (参考图 6 左下) */
|
||
.hatch{
|
||
background-image:repeating-linear-gradient(135deg,currentColor 0 1px,transparent 1px 8px);
|
||
opacity:.55
|
||
}
|
||
|
||
/* ========== V3: 高级点阵装饰 — 圆点 / × 号 / 圆圈 ========== */
|
||
/* 实心圆点矩阵 — 用于大面积装饰 */
|
||
.dot-mat{
|
||
--d:14px;
|
||
background-image:radial-gradient(currentColor 1.4px,transparent 1.6px);
|
||
background-size:var(--d) var(--d);
|
||
background-position:0 0;
|
||
opacity:.5
|
||
}
|
||
.dot-mat.lg{--d:22px}
|
||
.dot-mat.xl{--d:34px}
|
||
.dot-mat.dense{--d:9px;opacity:.62}
|
||
|
||
/* 描边圆圈矩阵 — 工业感 */
|
||
.ring-mat{
|
||
--d:18px;
|
||
background-image:
|
||
radial-gradient(circle at 50% 50%,transparent 2px,currentColor 2px,currentColor 2.6px,transparent 2.7px);
|
||
background-size:var(--d) var(--d);
|
||
opacity:.55
|
||
}
|
||
.ring-mat.lg{--d:28px}
|
||
|
||
/* × 号矩阵 — SVG mask 实现真正的 × 网格平铺 */
|
||
.cross-mat{
|
||
--d:22px;
|
||
background-color:currentColor;
|
||
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><g stroke='black' stroke-width='1.4' stroke-linecap='round' fill='none'><line x1='8' y1='8' x2='14' y2='14'/><line x1='14' y1='8' x2='8' y2='14'/></g></svg>");
|
||
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><g stroke='black' stroke-width='1.4' stroke-linecap='round' fill='none'><line x1='8' y1='8' x2='14' y2='14'/><line x1='14' y1='8' x2='8' y2='14'/></g></svg>");
|
||
-webkit-mask-size:var(--d) var(--d);
|
||
mask-size:var(--d) var(--d);
|
||
-webkit-mask-repeat:repeat;mask-repeat:repeat;
|
||
opacity:.42
|
||
}
|
||
.cross-mat.lg{--d:32px}
|
||
|
||
/* ========== V3: 几何水平柱状图 ========== */
|
||
/* 横向柱图: 标签列 + 柱体列 + 数值列 — 严格瑞士网格 */
|
||
.h-bar-chart{
|
||
display:grid;
|
||
grid-template-columns:11em minmax(0,1fr) 8em;
|
||
gap:1.6vh 1.6vw;
|
||
align-items:center;
|
||
margin-top:2.4vh;
|
||
font-feature-settings:"tnum"
|
||
}
|
||
.h-bar-chart .row-lbl{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-weight:500;
|
||
font-size:max(14px,1vw);
|
||
letter-spacing:-.005em;
|
||
text-align:left
|
||
}
|
||
.h-bar-chart .row-track{
|
||
height:3.2vh;
|
||
background:var(--grey-1);
|
||
position:relative;
|
||
overflow:hidden
|
||
}
|
||
.h-bar-chart .row-fill{
|
||
height:100%;
|
||
background:var(--ink);
|
||
transition:width 1s cubic-bezier(.5,0,.2,1)
|
||
}
|
||
.h-bar-chart .row-fill.accent{background:var(--accent)}
|
||
.h-bar-chart .row-fill.grey{background:var(--grey-3)}
|
||
.h-bar-chart .row-val{
|
||
font-family:var(--sans);font-weight:250;
|
||
font-size:max(16px,1.5vw);
|
||
letter-spacing:-.02em;
|
||
line-height:1
|
||
}
|
||
.h-bar-chart .row-val .unit{
|
||
font-size:.5em;opacity:.55;font-weight:300;
|
||
margin-left:.15em;letter-spacing:.04em
|
||
}
|
||
|
||
/* 垂直柱图: 用于 KPI 对比页(章节 P8) */
|
||
.v-bar-chart{
|
||
display:grid;
|
||
grid-template-columns:repeat(var(--cols,4),1fr);
|
||
align-items:end;
|
||
gap:1.4vw;
|
||
height:50vh;
|
||
margin-top:3vh
|
||
}
|
||
.v-bar-chart .col{display:flex;flex-direction:column;gap:1.4vh;align-items:stretch;height:100%}
|
||
.v-bar-chart .col-bar{
|
||
flex:1 1 auto;
|
||
background:var(--grey-1);
|
||
border-top:2px solid var(--ink);
|
||
position:relative;
|
||
display:flex;align-items:flex-start;justify-content:center;
|
||
padding-top:1vh
|
||
}
|
||
.v-bar-chart .col-bar.accent{background:var(--accent);border-top-color:var(--accent);color:var(--accent-on)}
|
||
.v-bar-chart .col-bar.ink{background:var(--ink);color:var(--paper);border-top-color:var(--ink)}
|
||
.v-bar-chart .col-bar .v{
|
||
font-family:var(--sans);font-weight:250;
|
||
font-size:max(18px,1.6vw);letter-spacing:-.02em
|
||
}
|
||
.v-bar-chart .col-lbl{
|
||
font-family:var(--mono);font-size:max(14px,.78vw);
|
||
letter-spacing:.14em;text-transform:uppercase;opacity:.6;
|
||
text-align:center;flex:0 0 auto
|
||
}
|
||
|
||
/* 对仗对比双栏 (章节 P9) */
|
||
.duo-compare{
|
||
display:grid;grid-template-columns:1fr 1px 1fr;
|
||
gap:0 3.4vw;
|
||
flex:1;align-items:stretch;margin-top:8vh
|
||
}
|
||
.duo-compare .vrule{background:var(--grey-2);width:1px;align-self:stretch}
|
||
.duo-compare .col{display:flex;flex-direction:column;gap:1.6vh;padding:0 .4vw}
|
||
.duo-compare .col-tag{
|
||
font-family:var(--mono);font-size:max(14px,.78vw);
|
||
letter-spacing:.16em;text-transform:uppercase;
|
||
color:var(--grey-3);
|
||
display:flex;align-items:center;gap:.6vw
|
||
}
|
||
.duo-compare .col-tag .num{
|
||
font-weight:600;color:var(--ink);
|
||
border:1px solid var(--ink);
|
||
padding:.2em .6em;font-size:.92em
|
||
}
|
||
.duo-compare .col.accent .col-tag .num{color:var(--accent);border-color:var(--accent)}
|
||
.duo-compare .col-ttl{
|
||
font-family:var(--sans),var(--sans-zh);font-weight:200;
|
||
font-size:3.6vw;line-height:1;letter-spacing:-.03em
|
||
}
|
||
.duo-compare .col.accent .col-ttl{color:var(--accent)}
|
||
.duo-compare .col-desc{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-size:max(16px,1.04vw);line-height:1.55;opacity:.78;
|
||
max-width:30vw
|
||
}
|
||
.duo-compare .col-list{
|
||
list-style:none;display:flex;flex-direction:column;gap:1vh;
|
||
margin-top:auto;padding-top:2vh;border-top:1px solid var(--grey-2)
|
||
}
|
||
.duo-compare .col-list li{
|
||
font-family:var(--sans),var(--sans-zh);
|
||
font-size:max(16px,.94vw);line-height:1.5;
|
||
padding-left:1.4em;position:relative
|
||
}
|
||
.duo-compare .col-list li::before{
|
||
content:"";position:absolute;left:0;top:.6em;
|
||
width:.5em;height:1px;background:currentColor;opacity:.55
|
||
}
|
||
.duo-compare .col.accent .col-list li::before{background:var(--accent);opacity:1}
|
||
|
||
/* 半屏 statement (参考图 7) */
|
||
.split-half{display:grid;grid-template-columns:1fr 1fr;gap:0;flex:1;align-items:stretch}
|
||
.split-half > .half{padding:5vh 3.4vw;display:flex;flex-direction:column;min-width:0}
|
||
.split-half > .half.r-border{border-left:1px solid rgba(127,127,127,.22)}
|
||
.split-half > .half.b-grey{background:var(--grey-1)}
|
||
.split-half > .half.b-accent{background:var(--accent);color:var(--accent-on)}
|
||
.split-half > .half.b-ink{background:var(--ink);color:var(--paper)}
|
||
|
||
/* 极简页眉 — t-meta 风格 (Carbon productive label-01) */
|
||
.canvas-card .chrome-min{
|
||
display:flex;justify-content:space-between;align-items:flex-start;
|
||
font-family:var(--mono);font-size:14px;font-weight:500;
|
||
letter-spacing:.14em;text-transform:uppercase;
|
||
color:var(--text-helper);
|
||
flex:0 0 auto;
|
||
margin-bottom:var(--sp-9); /* 48px */
|
||
}
|
||
.canvas-card .chrome-min.tight{margin-bottom:var(--sp-7)} /* 32px */
|
||
.canvas-card .chrome-min .l, .canvas-card .chrome-min .r{
|
||
max-width:48vw;line-height:1.5
|
||
}
|
||
.slide.dark .canvas-card .chrome-min,.split-half .half.b-ink .canvas-card .chrome-min{color:rgba(255,255,255,.62)}
|
||
|
||
/* 响应式降级 */
|
||
@media (max-width:900px){
|
||
.h-hero{font-size:16vw}
|
||
.h-hero-zh{font-size:13vw}
|
||
.h-xl{font-size:9vw}
|
||
.h-xl-zh{font-size:8vw}
|
||
.kpi-hero{font-size:32vw}
|
||
.kpi-big{font-size:16vw}
|
||
.grid-2-7-5,.grid-2-6-6,.grid-2-8-4,.grid-2-4-8{grid-template-columns:1fr}
|
||
.grid-12{grid-template-columns:repeat(6,1fr)}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="canvas-mode">
|
||
<script>
|
||
// Windows 平台标记 — 雅黑没有 ExtraLight,需要字重补偿
|
||
if(/Win/i.test(navigator.platform || navigator.userAgentData?.platform || '')){
|
||
document.body.classList.add('is-win');
|
||
}
|
||
(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('swiss-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-grid" 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/layouts-swiss.md
|
||
主题色配置参考 references/themes-swiss.md
|
||
============================================================ -->
|
||
<!-- SLIDES_HERE · 在此处粘贴 <section class="slide ..."> 页面块
|
||
- 页面骨架直接从 references/layouts-swiss.md 拷贝
|
||
- data-animate="..." 必须命中下方 RECIPES 字典里的已有 recipe 名之一(P23/P24 可复用 grid-reveal)
|
||
- 主题色配置参考 references/themes-swiss.md (默认 IKB 克莱因蓝) -->
|
||
|
||
<!-- ============ 示例:第 1 页 · Hero Cover · IKB 满屏 + ASCII 呼吸场(默认推荐) ============
|
||
⚠️ P0 对齐法则(每页都要过):
|
||
1. .canvas-card 已自带 padding:5.6vh 5vw 4.4vh,所有页面内容直接放在 canvas-card 子元素里,
|
||
子元素**不要再加水平 padding**,否则会比 chrome-min 内缩一圈、左右不对齐.
|
||
2. .slide.split .canvas-card{padding:0} 已被 CSS 覆盖,
|
||
split 模式下两个 .half 自己控制 padding(常用 5.6vh 3.6vw 4.4vh),与本规则不冲突.
|
||
3. 大字号一律用双约束 font-size:min(Xvw, Yvh),Y ≥ X * 1.6 才不会在 16:9 屏被高度截断.
|
||
4. kicker 必须在大标题"上方",不是左侧——禁止 grid-template-columns:auto 1fr 把它们压成左右.
|
||
|
||
封面/封底设计语言(默认 IKB 满屏 + ASCII 呼吸场):
|
||
- section 用 .slide.accent (满屏 IKB,不是 light 白底)
|
||
- canvas-card 内首位插入 <canvas class="ascii-bg" aria-hidden="true">,本文件底部 IIFE 自动启动
|
||
- 主标题反白 weight 200,强调字用斜体而非 var(--accent)(底已是蓝,蓝压蓝看不见)
|
||
- 不要再放编号大字"01"——chrome-min 已经标 01/NN -->
|
||
<section class="slide accent" data-animate="hero" data-slide-id="cover">
|
||
<div class="canvas-card">
|
||
<canvas class="ascii-bg" aria-hidden="true"></canvas>
|
||
<div class="chrome-min">
|
||
<div class="l">[必填] Deck 标题 · Issue/Field Note 编号</div>
|
||
<div class="r">SS · 25.05.10 · 01 / NN</div>
|
||
</div>
|
||
|
||
<!-- 主体:padding 必须为 0,不要再叠 5vw,否则左右对不齐 chrome-min -->
|
||
<div style="flex:1;padding:0;display:grid;grid-template-rows:auto 1fr auto;gap:2.6vh">
|
||
<div data-anim="kicker" class="t-meta" style="color:rgba(255,255,255,.78);letter-spacing:.22em">[必填] 章节英文 / Section En</div>
|
||
|
||
<h1 data-anim="title" style="align-self:center;font-family:var(--sans),var(--sans-zh);font-weight:200;font-size:min(11.6vw,19vh);line-height:.94;letter-spacing:-.025em;color:#fff">[必填] 中文主标题<br/>(≤ 12 字,可在某字加 <span style="font-style:italic;font-weight:300">italic</span> 微强调)</h1>
|
||
|
||
<div data-anim="bottom" style="display:grid;grid-template-rows:auto auto;gap:1.6vh;border-top:1px solid rgba(255,255,255,.22);padding-top:2vh">
|
||
<div data-anim="lead" class="lead" style="max-width:52ch;color:rgba(255,255,255,.86)">[必填] 一段 1-2 行的副标 / 引子,定调全场.</div>
|
||
<div style="display:flex;justify-content:space-between;align-items:end">
|
||
<div class="t-meta" style="color:rgba(255,255,255,.6)">[选填] 作者 · 日期 · 出处</div>
|
||
<div class="t-meta" style="color:rgba(255,255,255,.6)">→ swipe / arrow keys</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ 示例:最后一页 · Closing Manifesto · 左 IKB+ASCII / 右白底 takeaway ============
|
||
与封面 IKB 首尾呼应,但收束更克制:左半保留 ASCII 呼吸场承载宣言,右半白底列 3 条 takeaway.
|
||
第 03 条用 var(--accent) IKB 蓝强调("单一锚点"原则),首尾形成"色彩闭环". -->
|
||
<section class="slide split" data-animate="split-statement" data-slide-id="closing">
|
||
<div class="canvas-card">
|
||
<div class="split-half">
|
||
<!-- 左半 · IKB 宣言 + ASCII 呼吸场 -->
|
||
<div class="half b-accent" style="padding:5.6vh 3.6vw 4.4vh;justify-content:space-between;position:relative;overflow:hidden">
|
||
<canvas class="ascii-bg" aria-hidden="true"></canvas>
|
||
<div class="chrome-min" style="margin-bottom:0;position:relative;z-index:1">
|
||
<div class="l">NN / NN</div>
|
||
<div class="r">CLOSING</div>
|
||
</div>
|
||
|
||
<div data-anim="manifesto" style="display:flex;flex-direction:column;gap:2vh;position:relative;z-index:1">
|
||
<div class="t-meta" style="color:rgba(255,255,255,.78);letter-spacing:.22em;margin-bottom:1.6vh">MANIFESTO</div>
|
||
<h2 style="font-family:var(--sans),var(--sans-zh);font-size:min(8vw,14vh);line-height:.94;letter-spacing:-.025em;font-weight:200;color:#fff">[必填] Build a model.<br/>Run <span style="font-style:italic;font-weight:300">forever</span>.</h2>
|
||
<div style="font-family:var(--sans),var(--sans-zh);font-size:max(14px,1vw);line-height:1.6;color:rgba(255,255,255,.82);font-weight:400;max-width:36ch;margin-top:1.4vh">[必填] 一句 1-2 行的中文/英文注脚,把宣言落地.</div>
|
||
</div>
|
||
|
||
<div data-anim="signature" style="display:flex;justify-content:space-between;align-items:end;border-top:1px solid rgba(255,255,255,.22);padding-top:2vh;position:relative;z-index:1">
|
||
<div class="t-meta" style="color:rgba(255,255,255,.62)">[选填] 作者 · 头衔</div>
|
||
<div class="t-meta" style="color:rgba(255,255,255,.62)">YY.MM.DD</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右半 · 三条 takeaway · 白底承载理性收束 -->
|
||
<div class="half" style="padding:5.6vh 3.6vw 4.4vh;justify-content:space-between">
|
||
<div class="chrome-min">
|
||
<div class="l">TAKEAWAYS</div>
|
||
<div class="r">03 RULES</div>
|
||
</div>
|
||
|
||
<div data-anim="rules" style="display:flex;flex-direction:column;gap:0">
|
||
<div style="display:grid;grid-template-columns:auto 1fr;gap:2vw;align-items:start;padding:2.6vh 0;border-top:1px solid var(--border-subtle)">
|
||
<div style="font-family:var(--sans);font-weight:200;font-size:min(4.4vw,7.8vh);line-height:.9;color:var(--text-primary)">01</div>
|
||
<div>
|
||
<h3 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(18px,1.8vw);line-height:1.2;letter-spacing:-.015em;color:var(--text-primary);margin-bottom:1vh">[必填] takeaway 标题 01</h3>
|
||
<p style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.6;color:var(--text-secondary);font-weight:400">[必填] 1-2 行展开说明.</p>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:auto 1fr;gap:2vw;align-items:start;padding:2.6vh 0;border-top:1px solid var(--border-subtle)">
|
||
<div style="font-family:var(--sans);font-weight:200;font-size:min(4.4vw,7.8vh);line-height:.9;color:var(--text-primary)">02</div>
|
||
<div>
|
||
<h3 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(18px,1.8vw);line-height:1.2;letter-spacing:-.015em;color:var(--text-primary);margin-bottom:1vh">[必填] takeaway 标题 02</h3>
|
||
<p style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.6;color:var(--text-secondary);font-weight:400">[必填] 1-2 行展开说明.</p>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:auto 1fr;gap:2vw;align-items:start;padding:2.6vh 0;border-top:1px solid var(--border-subtle);border-bottom:2px solid var(--accent)">
|
||
<div style="font-family:var(--sans);font-weight:200;font-size:min(4.4vw,7.8vh);line-height:.9;color:var(--accent)">03</div>
|
||
<div>
|
||
<h3 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(18px,1.8vw);line-height:1.2;letter-spacing:-.015em;color:var(--accent);margin-bottom:1vh">[必填] takeaway 标题 03 · accent 强调</h3>
|
||
<p style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.6;color:var(--text-secondary);font-weight:400">[必填] 最后一条用 IKB 强调,与封面色彩首尾闭环.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div data-anim="foot" class="t-meta" style="color:var(--text-helper);text-align:right">→ 完 · END OF FIELD NOTE</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<div id="nav"></div>
|
||
|
||
<!-- 演讲备注:替换示例页时同步替换,id 必须与 section[data-slide-id] 一致。 -->
|
||
<script>
|
||
const SPEAKER_NOTES = [
|
||
{
|
||
id: 'cover', title: '[必填] Deck 标题', section: '开场', minutes: 0.6,
|
||
purpose: '建立主题与现场预期',
|
||
talk: ['用一句反差或问题开场', '说明为什么是现在讲这件事', '只预告主线,不在封面展开细节'],
|
||
transition: '从开场问题进入核心内容', cue: '停顿,确认观众屏已同步'
|
||
},
|
||
{
|
||
id: 'closing', title: '[必填] Closing', section: '收束', minutes: 0.8,
|
||
purpose: '收束全场并留下可记忆的行动点',
|
||
talk: ['回扣开场时提出的问题', '把三条 takeaway 说成一个完整判断', '给观众一个会后可以立即采取的动作'],
|
||
transition: '结束演讲,进入提问或致谢', cue: '停顿 2 秒后致谢'
|
||
}
|
||
];
|
||
window.__SPEAKER_NOTES__ = SPEAKER_NOTES;
|
||
</script>
|
||
|
||
<script>
|
||
/* =============== WebGL 网格背景 (瑞士风专用) ===============
|
||
极简移动网格 + 微弱点阵叠加,营造"工业感、精准感"
|
||
- 主网格: 缓慢漂移的细线网格
|
||
- 次级: 鼠标附近的极细点阵微扰
|
||
- 颜色: 跟随主题(浅底深线 / 深底亮线),配合 mix-blend-mode
|
||
*/
|
||
const VS = `attribute vec2 position;void main(){gl_Position=vec4(position,0.0,1.0);}`;
|
||
|
||
const FS = `precision highp float;
|
||
uniform vec2 u_resolution;
|
||
uniform float u_time;
|
||
uniform vec2 u_mouse;
|
||
uniform float u_dark; // 0 = light, 1 = dark
|
||
uniform vec3 u_accent;
|
||
|
||
float gridLine(vec2 uv, float spacing, float thickness){
|
||
vec2 g = abs(fract(uv / spacing) - 0.5);
|
||
float d = min(g.x, g.y);
|
||
return 1.0 - smoothstep(thickness - 0.005, thickness + 0.005, d);
|
||
}
|
||
|
||
float dot2(vec2 p){ return dot(p,p); }
|
||
|
||
void main(){
|
||
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
||
float aspect = u_resolution.x / u_resolution.y;
|
||
vec2 p = uv;
|
||
p.x *= aspect;
|
||
|
||
// 缓慢平移
|
||
vec2 drift = vec2(u_time * 0.008, u_time * 0.005);
|
||
vec2 gp = p + drift;
|
||
|
||
// 主细网格 (大间距)
|
||
float mainGrid = gridLine(gp, 0.12, 0.012);
|
||
// 次级网格 (更细更密)
|
||
float subGrid = gridLine(gp, 0.024, 0.04) * 0.4;
|
||
|
||
// 鼠标附近的强化
|
||
vec2 m = u_mouse;
|
||
m.x *= aspect;
|
||
float md = length(p - m);
|
||
float mInfluence = exp(-md * 4.0) * 0.5;
|
||
|
||
float gridStrength = (mainGrid + subGrid * 0.5) * (0.45 + mInfluence);
|
||
|
||
// 点阵 (作为基底)
|
||
vec2 dotGrid = fract(gp * 50.0) - 0.5;
|
||
float dotMask = 1.0 - smoothstep(0.05, 0.14, length(dotGrid));
|
||
// 用低频噪声调制点阵密度
|
||
float wave = sin(gp.x * 1.4 + u_time * 0.15) * cos(gp.y * 1.6 - u_time * 0.12);
|
||
dotMask *= smoothstep(-0.3, 0.6, wave) * 0.6;
|
||
|
||
// 颜色: 浅底用深线条,深底用浅线条;高亮处带 accent 痕迹
|
||
vec3 lineColor = mix(vec3(0.08), vec3(0.92), u_dark);
|
||
vec3 bgColor = mix(vec3(0.97, 0.97, 0.96), vec3(0.06, 0.06, 0.07), u_dark);
|
||
|
||
// accent 暗示 (鼠标附近偷渡一点 accent 色)
|
||
vec3 col = bgColor;
|
||
col = mix(col, lineColor, gridStrength * 0.55);
|
||
col = mix(col, lineColor, dotMask * 0.35);
|
||
col = mix(col, u_accent, mInfluence * 0.18);
|
||
|
||
gl_FragColor = vec4(col, 1.0);
|
||
}`;
|
||
|
||
const mouse={x:0.5,y:0.5};
|
||
addEventListener('mousemove',e=>{mouse.x=e.clientX/innerWidth;mouse.y=1-e.clientY/innerHeight});
|
||
|
||
function bootGL(canvasId, fsSrc){
|
||
const canvas=document.getElementById(canvasId);
|
||
const gl=canvas.getContext('webgl',{alpha:true,antialias:true,premultipliedAlpha:false});
|
||
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 lD=gl.getUniformLocation(prog,'u_dark');
|
||
const lA=gl.getUniformLocation(prog,'u_accent');
|
||
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();
|
||
|
||
// 读取 CSS 变量,把 accent 颜色塞进 shader
|
||
function readAccent(){
|
||
const cs = getComputedStyle(document.documentElement);
|
||
const hex = cs.getPropertyValue('--accent').trim() || '#002FA7';
|
||
const m = hex.match(/^#([0-9a-f]{6})$/i);
|
||
if(!m) return [0, 0.18, 0.65];
|
||
const n = parseInt(m[1], 16);
|
||
return [((n>>16)&255)/255, ((n>>8)&255)/255, (n&255)/255];
|
||
}
|
||
let accent = readAccent();
|
||
let dark = 0;
|
||
|
||
return (tSec, isDark)=>{
|
||
if(isDark !== undefined) dark = isDark ? 1 : 0;
|
||
accent = readAccent();
|
||
gl.uniform2f(lRes,canvas.width,canvas.height);
|
||
gl.uniform1f(lT,tSec);
|
||
gl.uniform2f(lM,mouse.x,mouse.y);
|
||
gl.uniform1f(lD,dark);
|
||
gl.uniform3f(lA,accent[0],accent[1],accent[2]);
|
||
gl.drawArrays(gl.TRIANGLES,0,6);
|
||
return true;
|
||
};
|
||
}
|
||
// canvas-mode / low-power: skip WebGL draw loop (no active RAF loop)
|
||
let darkMode=false;
|
||
let gridCtrl=null, gridRAF=0, gridT0=Date.now();
|
||
function startGrid(){
|
||
if(document.body.classList.contains('canvas-mode') || window.__lowPowerMode || gridRAF) return;
|
||
if(!gridCtrl) gridCtrl = bootGL('bg-grid',FS);
|
||
if(!gridCtrl) return;
|
||
gridT0=Date.now();
|
||
function loop(){
|
||
if(window.__lowPowerMode){gridRAF=0;return;}
|
||
const t=(Date.now()-gridT0)/1000;
|
||
gridCtrl(t, darkMode);
|
||
gridRAF=requestAnimationFrame(loop);
|
||
}
|
||
gridRAF=requestAnimationFrame(loop);
|
||
}
|
||
function stopGrid(){
|
||
if(gridRAF) cancelAnimationFrame(gridRAF);
|
||
gridRAF=0;
|
||
}
|
||
if(document.body.classList.contains('canvas-mode')){
|
||
const c=document.getElementById('bg-grid');
|
||
if(c) c.remove();
|
||
}else{
|
||
startGrid();
|
||
}
|
||
addEventListener('swiss-low-power-change', e=>{e.detail.on ? stopGrid() : startGrid();});
|
||
|
||
// =============== 导航 ===============
|
||
const deck=document.getElementById('deck');
|
||
const slides=deck.querySelectorAll('.slide');
|
||
const nav=document.getElementById('nav');
|
||
let idx=0,total=slides.length,lock=false;
|
||
|
||
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));
|
||
const el=slides[idx];
|
||
const isDark = el.classList.contains('dark') || el.classList.contains('accent');
|
||
document.body.classList.toggle('dark-bg', isDark);
|
||
darkMode = isDark;
|
||
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(250,250,248,.96);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;overflow:hidden;border:2px solid '+(i===idx?'var(--accent)':'rgba(0,0,0,.12)')+';transition:border-color .2s';
|
||
card.onmouseenter=()=>card.style.borderColor='rgba(0,0,0,.4)';
|
||
card.onmouseleave=()=>card.style.borderColor=i===idx?'var(--accent)':'rgba(0,0,0,.12)';
|
||
const wrap=document.createElement('div');
|
||
const isDark = s.classList.contains('dark') || s.classList.contains('accent');
|
||
wrap.style.cssText='width:100%;aspect-ratio:16/9;overflow:hidden;position:relative;pointer-events:none;background:'+(isDark?'var(--ink)':'var(--paper)');
|
||
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');
|
||
/* ESC 索引卡 label */
|
||
label.style.cssText='padding:6px 10px;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);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'){
|
||
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){
|
||
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)){
|
||
go(idx+(dx<0?1:-1));
|
||
}
|
||
},{passive:true});
|
||
|
||
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 动效引擎 (与原模板一致) -->
|
||
<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'});
|
||
}
|
||
}
|
||
|
||
if(motion){
|
||
const { animate } = motion;
|
||
document.body.classList.add('motion-ready');
|
||
|
||
/* ============================================================
|
||
IBM Carbon Motion · 每个 recipe 服务一种表达
|
||
不是一刀切的 stagger,而是把动效绑在内容语义上
|
||
============================================================ */
|
||
/* 缓动从 :root 的 Carbon motion token 读取,CSS 是唯一事实源(硬编码仅作解析失败兜底) */
|
||
const cssEase = (name, fallback) => {
|
||
const v = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||
const m = v && v.match(/cubic-bezier\(([^)]+)\)/);
|
||
const nums = m ? m[1].split(',').map(Number) : [];
|
||
return (nums.length === 4 && nums.every(Number.isFinite)) ? nums : fallback;
|
||
};
|
||
const EASE_PROD = cssEase('--ease-prod', [.2, 0, .38, .9]);
|
||
const EASE_ENTRY_EXP = cssEase('--ease-entry-exp', [0, 0, .3, 1]);
|
||
|
||
const slides = [...document.querySelectorAll('.slide')];
|
||
let lastIdx = -1;
|
||
|
||
function resetAnims(slide){
|
||
slide.querySelectorAll('[data-anim]').forEach(el=>{
|
||
el.style.opacity='';
|
||
el.style.transform='';
|
||
});
|
||
/* 同时复位需要被 recipe 接管的元素 */
|
||
slide.querySelectorAll('.row-fill,.tl-node,.stack-block,.bar-tower,.sub-card,.col,.vrule,.kpi-cell')
|
||
.forEach(el=>{el.style.cssText = el.dataset._origCss || el.style.cssText;});
|
||
}
|
||
|
||
/* ---------- 通用工具 ---------- */
|
||
const fade = (el, opts={})=>animate(el,
|
||
{opacity:[0,1], y:[opts.y ?? 12, 0]},
|
||
{duration:opts.duration ?? .6, delay:opts.delay ?? 0,
|
||
easing:opts.easing ?? EASE_ENTRY_EXP});
|
||
|
||
/* ---------- recipe: hero · 封面索引 ----------
|
||
大编号一个个亮起 → 索引行最后落定 */
|
||
function rHero(slide, all){
|
||
const numRows = [...slide.querySelectorAll('.cover-row')];
|
||
const rest = all.filter(el=>!numRows.length || el !== numRows[0]);
|
||
/* 先入: chrome 240ms */
|
||
const chrome = slide.querySelector('.chrome-min');
|
||
if(chrome) animate(chrome, {opacity:[0,1]}, {duration:.24, easing:EASE_PROD});
|
||
/* 大编号 01/02/03 像点名一样依次亮 */
|
||
numRows.forEach((row, i)=>{
|
||
animate(row, {opacity:[0,1], x:[-12,0]},
|
||
{duration:.5, delay:.15 + i*.18, easing:EASE_ENTRY_EXP});
|
||
});
|
||
/* 索引底栏最后慢慢落定 */
|
||
const idx = slide.querySelector('[data-anim="line"]');
|
||
if(idx) fade(idx, {delay:.15 + numRows.length*.18 + .1, duration:.5, y:6});
|
||
}
|
||
|
||
/* ---------- recipe: progression · 1× → 10× → 1000× ----------
|
||
节点依次入场,每个节点的数字单独"递进生长"营造跃迁 */
|
||
function rProgression(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.6, y:10});
|
||
|
||
const nodes = [...slide.querySelectorAll('.tl-node')];
|
||
nodes.forEach((node, i)=>{
|
||
const base = .35 + i*.32; /* 节点之间间隔大,营造时间感 */
|
||
/* 整个节点先轻微浮入 */
|
||
animate(node, {opacity:[0,1], y:[14, 0]},
|
||
{duration:.55, delay:base, easing:EASE_ENTRY_EXP});
|
||
/* 再让 multi(数字)从 .85 scale 弹到 1,延迟 100ms */
|
||
const multi = node.querySelector('.multi');
|
||
if(multi) animate(multi, {scale:[.92, 1], opacity:[0,1]},
|
||
{duration:.5, delay:base + .12, easing:EASE_ENTRY_EXP});
|
||
});
|
||
|
||
/* 底部 KPI 4 列最后落定,内部 60ms stagger */
|
||
const kpis = [...slide.querySelectorAll('.kpi-cell')];
|
||
kpis.forEach((cell, i)=>{
|
||
animate(cell, {opacity:[0,1], y:[8, 0]},
|
||
{duration:.4, delay:1.4 + i*.07, easing:EASE_PROD});
|
||
});
|
||
}
|
||
|
||
/* ---------- recipe: statement · 大宣言 ----------
|
||
左半屏标题逐行落下,右半屏 leaked 信息晚 600ms 进 */
|
||
function rStatement(slide, all){
|
||
const halves = [...slide.querySelectorAll('.half')];
|
||
if(halves.length === 2){
|
||
animate(halves[0], {opacity:[0,1], y:[18,0]},
|
||
{duration:.7, delay:0, easing:EASE_ENTRY_EXP});
|
||
animate(halves[1], {opacity:[0,1], y:[18,0]},
|
||
{duration:.7, delay:.6, easing:EASE_ENTRY_EXP});
|
||
} else {
|
||
/* P9 Index Card — 三行像盖章一样依次落 */
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.5, y:6});
|
||
const blocks = all.filter(el=>el !== head);
|
||
blocks.forEach((el, i)=>{
|
||
animate(el, {opacity:[0,1], y:[20,0]},
|
||
{duration:.55, delay:.25 + i*.18, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ---------- recipe: grid-reveal · 五个定义 ----------
|
||
卡片按 nb-corner 序号 01→02→03→04→05→Σ 依次揭示 */
|
||
function rGridReveal(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.6, y:10});
|
||
const cards = [...slide.querySelectorAll('.sub-card')];
|
||
cards.forEach((card, i)=>{
|
||
animate(card, {opacity:[0,1], y:[20,0], scale:[.96, 1]},
|
||
{duration:.5, delay:.3 + i*.09, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
|
||
/* ---------- recipe: stack-build · 三层架构 ----------
|
||
中间 thin 先入 → 上层 fat skills 从顶推下 → 下层 application 从底推上 */
|
||
function rStackBuild(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.6, y:10});
|
||
|
||
const blocks = [...slide.querySelectorAll('.stack-block')];
|
||
/* 先入: 中间薄层(LAYER 02) */
|
||
if(blocks[1]) animate(blocks[1], {opacity:[0,1], scaleY:[.85, 1]},
|
||
{duration:.55, delay:.3, easing:EASE_ENTRY_EXP});
|
||
/* 上推下: LAYER 01 fat skills 从顶部 push down */
|
||
if(blocks[0]) animate(blocks[0], {opacity:[0,1], y:[-22, 0]},
|
||
{duration:.6, delay:.6, easing:EASE_ENTRY_EXP});
|
||
/* 下推上: LAYER 03 application 从底部 push up */
|
||
if(blocks[2]) animate(blocks[2], {opacity:[0,1], y:[22, 0]},
|
||
{duration:.6, delay:.6, easing:EASE_ENTRY_EXP});
|
||
|
||
const foot = slide.querySelector('.t-meta');
|
||
if(foot) animate(foot, {opacity:[0,1]}, {duration:.3, delay:1.3, easing:EASE_PROD});
|
||
}
|
||
|
||
/* ---------- recipe: measure-up · YC KPI 塔 ----------
|
||
塔从底部 scaleY 0→1 生长 + 数字最后弹入 */
|
||
function rMeasureUp(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.6, y:10});
|
||
|
||
const towers = [...slide.querySelectorAll('.bar-tower')];
|
||
towers.forEach((tower, i)=>{
|
||
const block = tower.querySelector('.body-block');
|
||
if(block){
|
||
block.style.transformOrigin = 'bottom center';
|
||
animate(block, {opacity:[0,1], scaleY:[.05, 1]},
|
||
{duration:.7, delay:.35 + i*.12, easing:EASE_ENTRY_EXP});
|
||
}
|
||
/* cap (顶部图标) 等柱体长好后弹入 */
|
||
const cap = tower.querySelector('.cap');
|
||
if(cap) animate(cap, {opacity:[0,1], y:[-8, 0]},
|
||
{duration:.4, delay:.85 + i*.12, easing:EASE_PROD});
|
||
});
|
||
}
|
||
|
||
/* ---------- recipe: bar-grow · 90% 价值分布 ----------
|
||
标题先入 → hairline 从中点向两侧 stroke draw → bar 依次 width 0→target → 数值 fade in */
|
||
function rBarGrow(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.6, y:10});
|
||
|
||
/* 中部 hairline:从 100% width 0 拉到 100% (transformOrigin: center) */
|
||
const midRow = slide.querySelector('[data-anim="up"]');
|
||
if(midRow){
|
||
const midLabel = midRow.querySelector('.t-cat');
|
||
const midLine = midRow.querySelector('div[style*="height:1px"]');
|
||
if(midLabel) animate(midLabel, {opacity:[0,1], x:[-8,0]},
|
||
{duration:.4, delay:.4, easing:EASE_PROD});
|
||
if(midLine){
|
||
midLine.style.transformOrigin = 'center';
|
||
animate(midLine, {opacity:[0,1], scaleX:[0, 1]},
|
||
{duration:.55, delay:.5, easing:EASE_ENTRY_EXP});
|
||
}
|
||
}
|
||
|
||
/* bar 行依次 width 增长 */
|
||
const fills = [...slide.querySelectorAll('.row-fill')];
|
||
const labels = [...slide.querySelectorAll('.row-lbl')];
|
||
const values = [...slide.querySelectorAll('.row-val')];
|
||
fills.forEach((fill, i)=>{
|
||
const target = fill.style.width;
|
||
fill.style.width = '0%';
|
||
if(labels[i]) animate(labels[i], {opacity:[0,1], x:[-12,0]},
|
||
{duration:.4, delay:.85 + i*.14, easing:EASE_PROD});
|
||
animate(fill, {width:['0%', target]},
|
||
{duration:.65, delay:.95 + i*.14, easing:EASE_ENTRY_EXP});
|
||
if(values[i]) animate(values[i], {opacity:[0,1]},
|
||
{duration:.3, delay:1.5 + i*.14, easing:EASE_PROD});
|
||
});
|
||
}
|
||
|
||
/* ---------- recipe: duo-mirror · Latent vs Deterministic ----------
|
||
左 80ms 入,vrule 从中心 scaleY 0→1,右 240ms 入 */
|
||
function rDuoMirror(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.6, y:10});
|
||
|
||
const cols = [...slide.querySelectorAll('.duo-compare .col')];
|
||
const vrule = slide.querySelector('.duo-compare .vrule');
|
||
if(cols[0]) animate(cols[0], {opacity:[0,1], x:[-24, 0]},
|
||
{duration:.65, delay:.4, easing:EASE_ENTRY_EXP});
|
||
if(vrule){
|
||
vrule.style.transformOrigin = 'center';
|
||
animate(vrule, {opacity:[0,1], scaleY:[0, 1]},
|
||
{duration:.55, delay:.55, easing:EASE_ENTRY_EXP});
|
||
}
|
||
if(cols[1]) animate(cols[1], {opacity:[0,1], x:[24, 0]},
|
||
{duration:.65, delay:.7, easing:EASE_ENTRY_EXP});
|
||
|
||
const foot = slide.querySelector('.t-meta');
|
||
if(foot) animate(foot, {opacity:[0,1]}, {duration:.3, delay:1.3, easing:EASE_PROD});
|
||
}
|
||
|
||
/* ---------- recipe: split-statement · 收尾 ----------
|
||
左黑半屏的 once / forever 错位入场;右白半屏 takeaway list 后跟 */
|
||
function rSplitStatement(slide, all){
|
||
const halves = [...slide.querySelectorAll('.half')];
|
||
/* 左黑半屏 — once 先入,forever 间隔 600ms */
|
||
if(halves[0]){
|
||
animate(halves[0], {opacity:[0,1]}, {duration:.4, easing:EASE_PROD});
|
||
const kpis = halves[0].querySelectorAll('.kpi-thin');
|
||
kpis.forEach((k, i)=>{
|
||
animate(k, {opacity:[0,1], y:[24,0]},
|
||
{duration:.7, delay:.25 + i*.55, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
/* 右白半屏 — list 三条依次入,在左侧 once 出现后开始 */
|
||
if(halves[1]){
|
||
animate(halves[1], {opacity:[0,1]}, {duration:.4, delay:.3, easing:EASE_PROD});
|
||
const items = halves[1].querySelectorAll('.takeaway-list li');
|
||
items.forEach((li, i)=>{
|
||
animate(li, {opacity:[0,1], x:[20, 0]},
|
||
{duration:.45, delay:1.0 + i*.12, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ---------- recipe: timeline-walk · P11 横向 evolution ----------
|
||
标题先入 → 横轴虚线 scaleX 拉开(伪) → 5 个 dot 按年代依次 scale 入 → label 跟随 */
|
||
function rTimelineWalk(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.55, y:10});
|
||
|
||
const tl = slide.querySelector('.timeline-h');
|
||
if(tl) animate(tl, {opacity:[0,1]}, {duration:.4, delay:.35, easing:EASE_PROD});
|
||
|
||
const nodes = [...slide.querySelectorAll('.timeline-h .th-node')];
|
||
nodes.forEach((node, i)=>{
|
||
const base = .55 + i*.18;
|
||
const dot = node.querySelector('.dot');
|
||
const label = node.querySelector('.label');
|
||
if(dot){
|
||
dot.style.transformOrigin='center';
|
||
animate(dot, {opacity:[0,1], scale:[.2, 1]},
|
||
{duration:.45, delay:base, easing:EASE_ENTRY_EXP});
|
||
}
|
||
if(label){
|
||
const fromY = node.classList.contains('up') ? 8 : -8;
|
||
/* 保留 CSS 的水平居中 translateX(-50%),避免动效覆盖后 label 与 dot 错位 */
|
||
animate(label, {opacity:[0,1], transform:[`translate(-50%, ${fromY}px)`, 'translate(-50%, 0px)']},
|
||
{duration:.5, delay:base + .12, easing:EASE_ENTRY_EXP});
|
||
}
|
||
});
|
||
|
||
const foot = slide.querySelector('.t-meta');
|
||
if(foot) animate(foot, {opacity:[0,1]}, {duration:.3, delay:1.7, easing:EASE_PROD});
|
||
}
|
||
|
||
/* ---------- recipe: manifesto · P12 Form & Found ----------
|
||
副标先入 → 大字两段错峰落 → 底部 ink 通栏条从下推上 */
|
||
function rManifesto(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head){
|
||
const cat = head.querySelector('.t-cat');
|
||
const title = head.querySelector('div:nth-child(2)');
|
||
if(cat) animate(cat, {opacity:[0,1], x:[-10,0]},
|
||
{duration:.4, delay:.1, easing:EASE_PROD});
|
||
if(title) animate(title, {opacity:[0,1], y:[26, 0]},
|
||
{duration:.85, delay:.3, easing:EASE_ENTRY_EXP});
|
||
}
|
||
/* 底部 ink 条从下推入 */
|
||
const foot = [...slide.querySelectorAll('[data-anim="up"]')];
|
||
foot.forEach((el, i)=>{
|
||
animate(el, {opacity:[0,1], y:[40, 0]},
|
||
{duration:.75, delay:.85 + i*.12, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
|
||
/* ---------- recipe: three-forces · P13 ----------
|
||
左 ink hero 先入 → 右 3 张卡按 1/2/3 依次从右滑入 + 每张大数字单独弹入 */
|
||
function rThreeForces(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.5, y:8});
|
||
|
||
const grid = slide.querySelector('[data-anim="up"]');
|
||
if(grid) animate(grid, {opacity:[0,1]}, {duration:.3, delay:.3, easing:EASE_PROD});
|
||
|
||
const heroBlock = grid?.querySelector(':scope > div:first-child');
|
||
if(heroBlock) animate(heroBlock, {opacity:[0,1], x:[-26, 0]},
|
||
{duration:.6, delay:.4, easing:EASE_ENTRY_EXP});
|
||
|
||
const cards = grid ? [...grid.querySelectorAll(':scope > div:nth-child(2) > .card-fill')] : [];
|
||
cards.forEach((card, i)=>{
|
||
const base = .6 + i*.18;
|
||
animate(card, {opacity:[0,1], x:[28, 0]},
|
||
{duration:.6, delay:base, easing:EASE_ENTRY_EXP});
|
||
const num = card.querySelector(':scope > div:first-child');
|
||
if(num) animate(num, {opacity:[0,1], scale:[.7, 1]},
|
||
{duration:.5, delay:base + .15, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
|
||
/* ---------- recipe: loop-form · P14 自学闭环 ----------
|
||
左 4 步像台阶依次入 → 右环图节点按时钟顺序入 → 中心 improves scale 入 */
|
||
function rLoopForm(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.55, y:10});
|
||
|
||
const grid = slide.querySelector('[data-anim="up"]');
|
||
if(grid) animate(grid, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
|
||
|
||
/* 左侧 4 步台阶,每步从左滑入 */
|
||
const steps = grid ? [...grid.querySelectorAll(':scope > div:first-child > div')] : [];
|
||
steps.forEach((step, i)=>{
|
||
animate(step, {opacity:[0,1], x:[-18, 0]},
|
||
{duration:.5, delay:.5 + i*.14, easing:EASE_ENTRY_EXP});
|
||
});
|
||
|
||
/* 右侧 SVG 节点 (4 个 circle + label) 按 01→04 顺序入 */
|
||
const svg = grid?.querySelector('svg');
|
||
if(svg){
|
||
const ring = svg.querySelector('circle:first-of-type');
|
||
if(ring) animate(ring, {opacity:[0,.25]}, {duration:.5, delay:.6, easing:EASE_PROD});
|
||
|
||
const nodeCircles = [...svg.querySelectorAll('circle')].slice(1);
|
||
nodeCircles.forEach((c, i)=>{
|
||
c.style.transformOrigin = `${c.getAttribute('cx')}px ${c.getAttribute('cy')}px`;
|
||
animate(c, {opacity:[0,1], scale:[.4, 1]},
|
||
{duration:.45, delay:.7 + i*.16, easing:EASE_ENTRY_EXP});
|
||
});
|
||
|
||
const arrows = [...svg.querySelectorAll('path[marker-end]')];
|
||
arrows.forEach((p, i)=>{
|
||
animate(p, {opacity:[0,1]},
|
||
{duration:.4, delay:.85 + i*.16, easing:EASE_PROD});
|
||
});
|
||
|
||
const center = [...svg.querySelectorAll('text')].slice(-2);
|
||
center.forEach((t, i)=>{
|
||
animate(t, {opacity:[0,1], scale:[.7, 1]},
|
||
{duration:.5, delay:1.55 + i*.1, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ---------- recipe: matrix-fill · P15 skill 矩阵 ----------
|
||
标题入 → 12 张卡按对角线波 (i+j) 扫入 → 底部 20,000 大数字最后 fade 入 */
|
||
function rMatrixFill(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.55, y:10});
|
||
|
||
const matrix = slide.querySelector('[data-anim="up"]');
|
||
if(!matrix) return;
|
||
animate(matrix, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
|
||
|
||
const cards = [...matrix.children];
|
||
const cols = 6;
|
||
cards.forEach((card, i)=>{
|
||
const row = Math.floor(i/cols), col = i%cols;
|
||
const wave = (row + col) * .055;
|
||
animate(card, {opacity:[0,1], y:[14, 0], scale:[.92, 1]},
|
||
{duration:.42, delay:.5 + wave, easing:EASE_ENTRY_EXP});
|
||
});
|
||
|
||
/* 底部 20,000 区块 */
|
||
const foot = [...slide.querySelectorAll('[data-anim="up"]')][1];
|
||
if(foot){
|
||
animate(foot, {opacity:[0,1], y:[18, 0]},
|
||
{duration:.7, delay:1.4, easing:EASE_ENTRY_EXP});
|
||
const bigNum = foot.querySelector('div:nth-child(1) > div:nth-child(2)');
|
||
if(bigNum) animate(bigNum, {opacity:[0,1], scale:[.94, 1]},
|
||
{duration:.7, delay:1.55, easing:EASE_ENTRY_EXP});
|
||
}
|
||
}
|
||
|
||
/* ---------- recipe: field-notes · P16 散点观察 ----------
|
||
标题入 → 6 张卡按"散点"乱序延迟入,微小旋转复位 */
|
||
function rFieldNotes(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.55, y:10});
|
||
|
||
const grid = slide.querySelector('[data-anim="up"]');
|
||
if(!grid) return;
|
||
animate(grid, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
|
||
|
||
/* 散点顺序: 用一个稍微打乱的索引数组,营造"乱中有序"感 */
|
||
const order = [0, 3, 1, 4, 2, 5];
|
||
const cards = [...grid.children];
|
||
order.forEach((idx, i)=>{
|
||
const card = cards[idx];
|
||
if(!card) return;
|
||
animate(card, {opacity:[0,1], y:[18, 0], rotate:[(idx%2?-.6:.6), 0]},
|
||
{duration:.55, delay:.5 + i*.11, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
|
||
/* ---------- recipe: system-diagram · P17 三圆系统图 ----------
|
||
标题入 → SVG 三组图依次入 + 中间同心圆从外向内 scale 入 → 下方注释列依次入 */
|
||
function rSystemDiagram(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.55, y:10});
|
||
|
||
const stage = slide.querySelector('[data-anim="up"]');
|
||
if(!stage) return;
|
||
animate(stage, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
|
||
|
||
const svgs = [...stage.querySelectorAll('svg')];
|
||
svgs.forEach((svg, i)=>{
|
||
const base = .55 + i*.22;
|
||
const circles = [...svg.querySelectorAll('circle')];
|
||
/* 中间是同心圆: 从外圈到内圈依次 scale 入 */
|
||
if(circles.length > 1){
|
||
circles.forEach((c, j)=>{
|
||
c.style.transformOrigin = `${c.getAttribute('cx')}px ${c.getAttribute('cy')}px`;
|
||
animate(c, {opacity:[0,1], scale:[.4, 1]},
|
||
{duration:.5, delay:base + j*.13, easing:EASE_ENTRY_EXP});
|
||
});
|
||
} else if(circles[0]){
|
||
circles[0].style.transformOrigin = `${circles[0].getAttribute('cx')}px ${circles[0].getAttribute('cy')}px`;
|
||
animate(circles[0], {opacity:[0,1], scale:[.4, 1]},
|
||
{duration:.5, delay:base, easing:EASE_ENTRY_EXP});
|
||
}
|
||
const labels = [...svg.querySelectorAll('text')];
|
||
labels.forEach((t, j)=>{
|
||
animate(t, {opacity:[0,1]},
|
||
{duration:.4, delay:base + .25 + j*.06, easing:EASE_PROD});
|
||
});
|
||
});
|
||
|
||
/* 下方注释列 */
|
||
const cols = [...stage.querySelectorAll(':scope > div:last-child > div')];
|
||
cols.forEach((col, i)=>{
|
||
animate(col, {opacity:[0,1], y:[12, 0]},
|
||
{duration:.45, delay:1.3 + i*.1, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
|
||
/* ---------- recipe: why-now · P18 三列 + 巨大底数 ----------
|
||
标题入 → 三列文本入 → 三个底部巨数 01/02/03 错峰 scale 落定 */
|
||
function rWhyNow(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.55, y:10});
|
||
|
||
const grid = slide.querySelector('[data-anim="up"]');
|
||
if(!grid) return;
|
||
animate(grid, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
|
||
|
||
const cols = [...grid.children];
|
||
cols.forEach((col, i)=>{
|
||
const base = .5 + i*.16;
|
||
const body = col.querySelector(':scope > div:not(:last-child)');
|
||
const big = col.querySelector(':scope > div:last-child');
|
||
if(body) animate(body, {opacity:[0,1], y:[14, 0]},
|
||
{duration:.55, delay:base, easing:EASE_ENTRY_EXP});
|
||
if(big) animate(big, {opacity:[0,1], scale:[.7, 1]},
|
||
{duration:.7, delay:base + .35, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
|
||
/* ---------- recipe: four-cards · P19 4 列卡片 ----------
|
||
顶部红线 scaleX 0→1 → 标题入 → 4 卡按 01-04 依次入 */
|
||
function rFourCards(slide, all){
|
||
/* 顶部红线 */
|
||
const topRule = slide.querySelector('[data-anim="line"] > div:first-child');
|
||
if(topRule){
|
||
topRule.style.transformOrigin = 'left center';
|
||
animate(topRule, {opacity:[0,1], scaleX:[0, 1]},
|
||
{duration:.5, delay:.1, easing:EASE_ENTRY_EXP});
|
||
}
|
||
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head){
|
||
const title = head.querySelector(':scope > div:nth-child(2)');
|
||
if(title) animate(title, {opacity:[0,1], y:[14, 0]},
|
||
{duration:.55, delay:.4, easing:EASE_ENTRY_EXP});
|
||
}
|
||
|
||
const grid = slide.querySelector('[data-anim="up"]');
|
||
if(!grid) return;
|
||
animate(grid, {opacity:[0,1]}, {duration:.3, delay:.55, easing:EASE_PROD});
|
||
|
||
const cards = [...grid.children];
|
||
cards.forEach((card, i)=>{
|
||
animate(card, {opacity:[0,1], y:[18, 0]},
|
||
{duration:.55, delay:.7 + i*.13, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
|
||
/* ============ P20 · Stacked KPI Ledger · 4 行账单逐行点亮 + 行间发丝从左画 ============ */
|
||
function rStackedLedger(slide, all){
|
||
const ledger = slide.querySelector('[data-anim="ledger"]');
|
||
if(!ledger) return;
|
||
animate(ledger, {opacity:[0,1]}, {duration:.3, delay:.1, easing:EASE_PROD});
|
||
|
||
const rows = [...ledger.querySelectorAll('.ledger-row')];
|
||
rows.forEach((row, i)=>{
|
||
const base = .25 + i*.18;
|
||
const num = row.querySelector('.ledger-num');
|
||
const label = row.querySelector('.ledger-label');
|
||
const icon = row.querySelector('.ledger-icon');
|
||
if(num) animate(num, {opacity:[0,1], y:[20, 0]}, {duration:.7, delay:base, easing:EASE_ENTRY_EXP});
|
||
if(label) animate(label, {opacity:[0,1], x:[-12, 0]}, {duration:.55, delay:base + .12, easing:EASE_ENTRY_EXP});
|
||
if(icon) animate(icon, {opacity:[0,1], scale:[.6,1]},{duration:.55, delay:base + .22, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
|
||
/* ============ P21 · Tech Spec Sheet · 标题分行 / KPI 顶线画出 + count 风感 / 竖线弹起 / 底巨数 ============ */
|
||
function rTechSpec(slide, all){
|
||
const head = slide.querySelector('[data-anim="line"]');
|
||
if(head) fade(head, {duration:.5, y:8});
|
||
|
||
const main = slide.querySelector('[data-anim="up"]');
|
||
if(main){
|
||
animate(main, {opacity:[0,1]}, {duration:.3, delay:.25, easing:EASE_PROD});
|
||
|
||
/* 左大标题分行 */
|
||
const titleLines = main.querySelector(':scope > div:first-child > div:first-child');
|
||
if(titleLines){
|
||
animate(titleLines, {opacity:[0,1], y:[18, 0]}, {duration:.7, delay:.35, easing:EASE_ENTRY_EXP});
|
||
}
|
||
const titleNote = main.querySelector(':scope > div:first-child > div:nth-child(2)');
|
||
if(titleNote){
|
||
animate(titleNote, {opacity:[0,1], y:[10, 0]}, {duration:.5, delay:.95, easing:EASE_ENTRY_EXP});
|
||
}
|
||
|
||
/* 三 KPI · 顶线 scaleX + 数字 fade-up + 副文字 */
|
||
const kpis = [...main.querySelectorAll(':scope > div:not([data-anim]):not(:first-child)')];
|
||
kpis.forEach((kpi, i)=>{
|
||
const base = .55 + i*.18;
|
||
const topRule = kpi.querySelector(':scope > div:first-child');
|
||
if(topRule){
|
||
topRule.style.transformOrigin = 'left center';
|
||
animate(topRule, {scaleX:[0,1], opacity:[0,1]}, {duration:.5, delay:base, easing:EASE_ENTRY_EXP});
|
||
}
|
||
const num = kpi.querySelector('.kpi-num');
|
||
if(num) animate(num, {opacity:[0,1], y:[14, 0]}, {duration:.6, delay:base + .15, easing:EASE_ENTRY_EXP});
|
||
const otherKids = [...kpi.children].filter(el=>el !== topRule && el !== num);
|
||
otherKids.forEach((el, j)=>{
|
||
animate(el, {opacity:[0,1]}, {duration:.4, delay:base + .25 + j*.05, easing:EASE_PROD});
|
||
});
|
||
});
|
||
|
||
}
|
||
|
||
/* 底部 hero 区: 巨数 + goal + tags + 右下竖线 */
|
||
const hero = slide.querySelector('[data-anim="hero"]');
|
||
if(hero){
|
||
animate(hero, {opacity:[0,1]}, {duration:.3, delay:1.3, easing:EASE_PROD});
|
||
const bottomHero = hero.querySelector('.bottom-hero');
|
||
if(bottomHero) animate(bottomHero, {opacity:[0,1], y:[24, 0], scale:[.92, 1]}, {duration:.7, delay:1.4, easing:EASE_ENTRY_EXP});
|
||
const middle = hero.querySelector(':scope > div:nth-child(2)');
|
||
if(middle){
|
||
const kids = [...middle.children];
|
||
kids.forEach((el, i)=>{
|
||
if(el.style && el.style.background === 'var(--ink)'){
|
||
el.style.transformOrigin = 'left center';
|
||
animate(el, {scaleX:[0,1], opacity:[0,1]}, {duration:.5, delay:1.6 + i*.1, easing:EASE_ENTRY_EXP});
|
||
} else {
|
||
animate(el, {opacity:[0,1], y:[10, 0]}, {duration:.5, delay:1.55 + i*.1, easing:EASE_ENTRY_EXP});
|
||
}
|
||
});
|
||
}
|
||
/* 右下: 文字先入, 9 根竖线再从底部 scaleY 弹起 */
|
||
const right = hero.querySelector(':scope > div:nth-child(3)');
|
||
if(right){
|
||
const rightText = right.querySelector(':scope > div:last-child');
|
||
if(rightText) animate(rightText, {opacity:[0,1], y:[10, 0]}, {duration:.5, delay:1.85, easing:EASE_ENTRY_EXP});
|
||
}
|
||
const bars = slide.querySelectorAll('[data-anim="bars"] .vbar');
|
||
bars.forEach((bar, i)=>{
|
||
bar.style.transformOrigin = 'bottom';
|
||
animate(bar, {scaleY:[0,1], opacity:[0,1]}, {duration:.5, delay:2.0 + i*.04, easing:EASE_ENTRY_EXP});
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ============ P22 · Image Hero · 图缓推 + 标题白块从左滑入 + 三 KPI 顶线画出 ============ */
|
||
function rImageHero(slide, all){
|
||
const img = slide.querySelector('[data-anim="img"] img');
|
||
if(img){
|
||
animate(img, {opacity:[0,1], scale:[1.06, 1]}, {duration:1.1, delay:.05, easing:EASE_ENTRY_EXP});
|
||
}
|
||
|
||
const titleBlock = slide.querySelector('[data-anim="title-block"]');
|
||
if(titleBlock){
|
||
titleBlock.style.transformOrigin = 'left center';
|
||
animate(titleBlock, {opacity:[0,1], scaleX:[0, 1]}, {duration:.7, delay:.45, easing:EASE_ENTRY_EXP});
|
||
const titleText = titleBlock.querySelector('div');
|
||
if(titleText) animate(titleText, {opacity:[0,1]}, {duration:.4, delay:.85, easing:EASE_PROD});
|
||
}
|
||
|
||
const kpiWrap = slide.querySelector('[data-anim="kpi"]');
|
||
if(kpiWrap){
|
||
animate(kpiWrap, {opacity:[0,1]}, {duration:.3, delay:.7, easing:EASE_PROD});
|
||
|
||
/* 段落 */
|
||
const para = kpiWrap.querySelector(':scope > div:first-child');
|
||
if(para) animate(para, {opacity:[0,1], y:[14, 0]}, {duration:.6, delay:.85, easing:EASE_ENTRY_EXP});
|
||
|
||
/* 三列 KPI · 顶线 scaleX + 数字升起 */
|
||
const cols = [...kpiWrap.querySelectorAll(':scope > div:nth-child(2) > div')];
|
||
cols.forEach((col, i)=>{
|
||
const base = 1.1 + i*.18;
|
||
const topRule = col.querySelector(':scope > div:first-child');
|
||
if(topRule){
|
||
topRule.style.transformOrigin = 'left center';
|
||
animate(topRule, {scaleX:[0,1], opacity:[0,1]}, {duration:.5, delay:base, easing:EASE_ENTRY_EXP});
|
||
}
|
||
const cat = col.querySelector('.t-meta');
|
||
if(cat) animate(cat, {opacity:[0,1]}, {duration:.4, delay:base + .15, easing:EASE_PROD});
|
||
const hero = col.querySelector('.kpi-hero');
|
||
if(hero) animate(hero, {opacity:[0,1], y:[18, 0]}, {duration:.7, delay:base + .25, easing:EASE_ENTRY_EXP});
|
||
const handled = new Set([topRule, cat, hero]);
|
||
[...col.children]
|
||
.filter(el => !handled.has(el))
|
||
.forEach((el, j)=>{
|
||
animate(el, {opacity:[0,1]}, {duration:.4, delay:base + .45 + j*.05, easing:EASE_PROD});
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
const RECIPES = {
|
||
'hero': rHero,
|
||
'progression': rProgression,
|
||
'statement': rStatement,
|
||
'grid-reveal': rGridReveal,
|
||
'stack-build': rStackBuild,
|
||
'measure-up': rMeasureUp,
|
||
'bar-grow': rBarGrow,
|
||
'duo-mirror': rDuoMirror,
|
||
'split-statement': rSplitStatement,
|
||
'timeline-walk': rTimelineWalk,
|
||
'manifesto': rManifesto,
|
||
'three-forces': rThreeForces,
|
||
'loop-form': rLoopForm,
|
||
'matrix-fill': rMatrixFill,
|
||
'field-notes': rFieldNotes,
|
||
'system-diagram': rSystemDiagram,
|
||
'why-now': rWhyNow,
|
||
'four-cards': rFourCards,
|
||
'stacked-ledger': rStackedLedger,
|
||
'tech-spec': rTechSpec,
|
||
'image-hero': rImageHero,
|
||
};
|
||
|
||
function revealStatic(slide){
|
||
resetAnims(slide);
|
||
document.getAnimations?.().forEach(a=>a.cancel());
|
||
slide.querySelectorAll('[data-anim],.row-fill,.tl-node,.stack-block,.bar-tower,.sub-card,.col,.vrule,.kpi-cell,.card-fill,.card-accent,.card-ink')
|
||
.forEach(el=>{
|
||
el.style.opacity='1';
|
||
el.style.transform='none';
|
||
});
|
||
}
|
||
|
||
function playSlide(i){
|
||
const slide = slides[i];
|
||
if(!slide) return;
|
||
lastIdx = i;
|
||
|
||
if(window.__lowPowerMode){
|
||
revealStatic(slide);
|
||
return;
|
||
}
|
||
|
||
resetAnims(slide);
|
||
|
||
/* 关键:[data-anim] 容器很多时候只是占位标记,真正的几何动画在子元素上.
|
||
默认强制 reveal 所有 [data-anim] 容器, recipe 想做块入场时用 motion 的 {opacity:[0,1]} 会自动覆盖 */
|
||
slide.querySelectorAll('[data-anim]').forEach(el=>{
|
||
el.style.opacity = '1';
|
||
el.style.transform = 'none';
|
||
});
|
||
|
||
const all = [...slide.querySelectorAll('[data-anim]')];
|
||
const recipe = slide.dataset.animate;
|
||
const fn = RECIPES[recipe];
|
||
if(fn){ fn(slide, all); return; }
|
||
|
||
/* fallback: 平凡 fade */
|
||
if(all.length) animate(all, {opacity:[0,1], y:[12,0]},
|
||
{duration:.6, delay:i=>i*.08, easing:EASE_ENTRY_EXP});
|
||
}
|
||
|
||
window.__playSlide = playSlide;
|
||
|
||
playSlide(window.__currentSlideIndex || 0);
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
/* ============== ASCII 点阵呼吸场 · IKB 封面/封底专用 ==============
|
||
sin/cos 二维噪声场驱动字符显隐,营造工业仪表板的"涌动呼吸"质感.
|
||
纯 canvas 2D, mix-blend-mode:screen 让字符在 IKB 底色上自然发亮.
|
||
用法:在需要呼吸场的容器(.canvas-card 或 split .half.b-accent)内首位插入
|
||
<canvas class="ascii-bg" aria-hidden="true">,本脚本会自动扫描并启动. */
|
||
(function(){
|
||
const canvases = [...document.querySelectorAll('canvas.ascii-bg')];
|
||
if(!canvases.length) return;
|
||
|
||
const PALETTE = ' ...:::---+++***◦◦••▢▣';
|
||
const CELL = 16;
|
||
const FONT_SIZE = 13;
|
||
|
||
function setup(c){
|
||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||
const rect = c.getBoundingClientRect();
|
||
if(rect.width < 4 || rect.height < 4) return false;
|
||
c.width = Math.round(rect.width * dpr);
|
||
c.height = Math.round(rect.height * dpr);
|
||
c.__dpr = dpr;
|
||
c.__w = rect.width;
|
||
c.__h = rect.height;
|
||
const ctx = c.getContext('2d');
|
||
ctx.setTransform(dpr,0,0,dpr,0,0);
|
||
const mono = (getComputedStyle(document.documentElement).getPropertyValue('--mono') || 'JetBrains Mono, monospace').trim();
|
||
ctx.font = `500 ${FONT_SIZE}px ${mono}`;
|
||
ctx.textBaseline = 'top';
|
||
c.__ctx = ctx;
|
||
return true;
|
||
}
|
||
|
||
function draw(c, t){
|
||
if(!c.__ctx) return;
|
||
const ctx = c.__ctx, w = c.__w, h = c.__h;
|
||
ctx.clearRect(0, 0, w, h);
|
||
const cols = Math.ceil(w / CELL);
|
||
const rows = Math.ceil(h / CELL);
|
||
for(let r=0; r<rows; r++){
|
||
for(let cc=0; cc<cols; cc++){
|
||
const n = (
|
||
Math.sin(cc * 0.18 + t) +
|
||
Math.sin(r * 0.24 - t * 0.7) +
|
||
Math.sin((cc + r) * 0.12 + t * 0.45) +
|
||
Math.sin(Math.hypot(cc - cols * 0.5, r - rows * 0.5) * 0.16 - t * 0.55)
|
||
) / 4; // [-1, 1]
|
||
const v = (n + 1) / 2; // [0, 1]
|
||
if(v < 0.22) continue;
|
||
const idx = Math.min(PALETTE.length - 1, Math.floor(v * PALETTE.length));
|
||
const ch = PALETTE[idx];
|
||
if(ch === ' ') continue;
|
||
const alpha = 0.08 + (v - 0.22) * 0.55;
|
||
ctx.fillStyle = `rgba(255,255,255,${alpha.toFixed(3)})`;
|
||
ctx.fillText(ch, cc * CELL, r * CELL);
|
||
}
|
||
}
|
||
}
|
||
|
||
function resizeAll(){ canvases.forEach(setup); }
|
||
let pending = null;
|
||
window.addEventListener('resize', ()=>{
|
||
if(window.__lowPowerMode) return;
|
||
if(pending) cancelAnimationFrame(pending);
|
||
pending = requestAnimationFrame(resizeAll);
|
||
}, {passive:true});
|
||
|
||
let t0 = performance.now();
|
||
let frame = 0, asciiRAF = 0, running = false;
|
||
function tick(now){
|
||
if(!running || window.__lowPowerMode){running=false;asciiRAF=0;return;}
|
||
const t = (now - t0) / 1000 * 0.55;
|
||
frame++;
|
||
canvases.forEach(c=>{
|
||
// 离屏 slide 降帧:每 4 帧渲染一次,在屏 slide 每帧渲染
|
||
const slide = c.closest('.slide');
|
||
const rect = slide ? slide.getBoundingClientRect() : null;
|
||
const onscreen = rect && rect.right > 0 && rect.left < window.innerWidth;
|
||
if(!onscreen && (frame & 3) !== 0) return;
|
||
draw(c, t);
|
||
});
|
||
asciiRAF = requestAnimationFrame(tick);
|
||
}
|
||
function start(){
|
||
if(running || window.__lowPowerMode) return;
|
||
resizeAll();
|
||
t0 = performance.now();
|
||
frame = 0;
|
||
running = true;
|
||
asciiRAF = requestAnimationFrame(tick);
|
||
}
|
||
function stop(){
|
||
running = false;
|
||
if(asciiRAF) cancelAnimationFrame(asciiRAF);
|
||
if(pending) cancelAnimationFrame(pending);
|
||
asciiRAF = 0;
|
||
pending = null;
|
||
canvases.forEach(c=>{
|
||
if(c.__ctx) c.__ctx.clearRect(0,0,c.__w || 0,c.__h || 0);
|
||
});
|
||
}
|
||
addEventListener('swiss-low-power-change', e=>{e.detail.on ? stop() : start();});
|
||
start();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|