|
@@ -4,283 +4,145 @@
|
|
|
<meta charset="UTF-8">
|
|
<meta charset="UTF-8">
|
|
|
<title>Deck · Multi-file Slide Index</title>
|
|
<title>Deck · Multi-file Slide Index</title>
|
|
|
<!--
|
|
<!--
|
|
|
- deck_index.html — 多文件 slide deck 的拼接器(双模式)
|
|
|
|
|
|
|
+ deck_index.html — 多文件 slide deck 拼接器 · 双概览模式
|
|
|
|
|
|
|
|
- 配合「每页一个独立 HTML」架构使用。与单文件 deck_stage.js 对比:
|
|
|
|
|
- · 每页独立作用域(CSS/JS 都隔离),一页出 bug 不影响其他页
|
|
|
|
|
- · 单页可直接在浏览器打开验证,不依赖 JS goTo()
|
|
|
|
|
- · 多 agent 可并行做不同页,merge 时零冲突
|
|
|
|
|
- · 适合 ≥15 页的讲座/课件/长 deck
|
|
|
|
|
|
|
+ 概览(默认进入,两种模式按 getSeconds()%5 随机:20% 画廊 / 80% 网格):
|
|
|
|
|
+ · grid 自适应网格墙:列数随页数+视口自适应,行多则倾斜变平,scale-to-fit 居中铺满(任意页数都不溢出/不失真)
|
|
|
|
|
+ · gallery 无限画廊:固定卡片大小,无缝无限平铺 + 缓慢漂移 + 轻微呼吸;一个 tile 含全部页(洗牌),看完所有页才重复
|
|
|
|
|
+ 演示 present:单页 fit 缩放翻页。ESC / ⊞ 回概览。
|
|
|
|
|
+ 性能:manifest 每项可带 thumb(预渲染缩略图),概览即用 <img> 平铺,避免 N 个 iframe 同时加载。
|
|
|
|
|
|
|
|
- 两种模式(body[data-mode] 控制):
|
|
|
|
|
- · 概览墙 overview(默认进入):所有页缩略成 3D 透视卡片墙,景深斜铺、悬浮投影。
|
|
|
|
|
- 点任意卡片 → 从该页进入演示;右下角「▶ 开始演示」从第 1 页(或记忆页)进入。
|
|
|
|
|
- · 全屏演示 present:单页 fit 缩放翻页。ESC 或左上「⊞ 概览」回到概览墙。
|
|
|
|
|
-
|
|
|
|
|
- 用法:
|
|
|
|
|
- 1. 把本文件复制到 deck 根目录,重命名 index.html
|
|
|
|
|
- 2. 在同目录建 slides/ 子目录,放每一页独立 HTML
|
|
|
|
|
- 3. 编辑下方 MANIFEST 数组,按顺序列出文件名和人类可读标签
|
|
|
|
|
- 4. 每张 slide HTML 建议尺寸 1920×1080,自带背景/字体;不要依赖外层 CSS
|
|
|
|
|
-
|
|
|
|
|
- 共享资源(如果需要):
|
|
|
|
|
- · shared/tokens.css — 跨页 CSS 变量(色板/字号)
|
|
|
|
|
- · shared/chrome.html — 页眉页脚可复用片段
|
|
|
|
|
- · 每页 HTML 自己 <link> 进去即可
|
|
|
|
|
-
|
|
|
|
|
- 键盘(演示模式):← / → / Space / PgUp / PgDown / Home / End / 1-9 跳页 / P 打印 / ESC 回概览
|
|
|
|
|
|
|
+ 用法:复制为 index.html;建 slides/ 放每页独立 HTML;编辑 MANIFEST。
|
|
|
|
|
+ 可选覆盖:window.DECK_OVERVIEW = 'grid' | 'gallery'(不写则随机);URL ?ov=grid|gallery 临时强制。
|
|
|
|
|
+ 键盘(演示):← / → / Space / PgUp / PgDown / Home / End / 1-9 / P 打印 / ESC 回概览
|
|
|
-->
|
|
-->
|
|
|
-
|
|
|
|
|
-<!-- ═══════════════════════════════════════════════════════ -->
|
|
|
|
|
-<!-- EDIT THIS — deck 所有页按顺序列出 -->
|
|
|
|
|
-<!-- ═══════════════════════════════════════════════════════ -->
|
|
|
|
|
<script>
|
|
<script>
|
|
|
window.DECK_MANIFEST = [
|
|
window.DECK_MANIFEST = [
|
|
|
- { file: "slides/01-cover.html", label: "Cover" },
|
|
|
|
|
- { file: "slides/02-quote.html", label: "Opening Quote" },
|
|
|
|
|
- { file: "slides/03-intro.html", label: "Self-intro" },
|
|
|
|
|
- // 继续往下加。file 是相对本文件的路径,label 用于计数器
|
|
|
|
|
|
|
+ { file: "slides/01-cover.html", label: "Cover" /*, thumb: "thumbs/01.jpg" */ },
|
|
|
];
|
|
];
|
|
|
-
|
|
|
|
|
- // 固定 canvas 尺寸。每页 HTML 都应该按这个尺寸设计。
|
|
|
|
|
window.DECK_WIDTH = 1920;
|
|
window.DECK_WIDTH = 1920;
|
|
|
window.DECK_HEIGHT = 1080;
|
|
window.DECK_HEIGHT = 1080;
|
|
|
|
|
+ window.GALLERY_CARD_W = 300; // 画廊卡片基准宽度
|
|
|
|
|
+ window.GALLERY_DRIFT_SECONDS = 80; // 画廊漂移一圈时长
|
|
|
|
|
+ // window.DECK_OVERVIEW = 'grid'; // 取消注释可固定概览模式
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style>
|
|
<style>
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
- html, body {
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- background: #0a0a0a;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- font-family: -apple-system, "PingFang SC", sans-serif;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ html, body { height: 100%; overflow: hidden; font-family: -apple-system, "PingFang SC", sans-serif; background: #0a0a0a; }
|
|
|
|
|
|
|
|
- /* ── 模式显隐:默认 overview ───────────────────────────── */
|
|
|
|
|
- body[data-mode="overview"] { background: #f0eee9; overflow: auto; }
|
|
|
|
|
|
|
+ .overview { position: fixed; inset: 0; }
|
|
|
|
|
+ body[data-mode="present"] .overview { display: none; }
|
|
|
|
|
+ body[data-mode="present"] .start-btn { display: none; }
|
|
|
body[data-mode="overview"] #present-ui { display: none; }
|
|
body[data-mode="overview"] #present-ui { display: none; }
|
|
|
- body[data-mode="present"] #overview { display: none; }
|
|
|
|
|
- body[data-mode="present"] .start-btn { display: none; }
|
|
|
|
|
-
|
|
|
|
|
- /* ════════════════════════════════════════════════════════ */
|
|
|
|
|
- /* 模式 A · 概览墙(3D 透视卡片墙) */
|
|
|
|
|
- /* ════════════════════════════════════════════════════════ */
|
|
|
|
|
- #overview {
|
|
|
|
|
- min-height: 100%;
|
|
|
|
|
- padding: 7vw 5vw 14vw;
|
|
|
|
|
- /* 透视容器:景深 */
|
|
|
|
|
- perspective: 1600px;
|
|
|
|
|
- perspective-origin: 50% 38%;
|
|
|
|
|
- }
|
|
|
|
|
- .wall {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
- gap: 2.4vw;
|
|
|
|
|
- max-width: 1500px;
|
|
|
|
|
- margin: 0 auto;
|
|
|
|
|
- transform-style: preserve-3d;
|
|
|
|
|
- /* 近处向左下、远处向右上铺开 */
|
|
|
|
|
- transform: rotateX(28deg) rotateZ(-14deg);
|
|
|
|
|
- }
|
|
|
|
|
- .card {
|
|
|
|
|
- position: relative;
|
|
|
|
|
- aspect-ratio: 16 / 9;
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- box-shadow: 0 18px 40px rgba(40, 34, 24, 0.22),
|
|
|
|
|
- 0 4px 12px rgba(40, 34, 24, 0.14);
|
|
|
|
|
- transition: transform 0.45s cubic-bezier(.2,.7,.2,1),
|
|
|
|
|
- box-shadow 0.45s cubic-bezier(.2,.7,.2,1);
|
|
|
|
|
- transform-style: preserve-3d;
|
|
|
|
|
- will-change: transform;
|
|
|
|
|
- }
|
|
|
|
|
- .card:hover {
|
|
|
|
|
- /* 抬起 + 微微「正过来」给预览感 */
|
|
|
|
|
- transform: translateZ(80px) rotateX(-14deg) rotateZ(14deg);
|
|
|
|
|
- box-shadow: 0 40px 80px rgba(40, 34, 24, 0.32),
|
|
|
|
|
- 0 10px 24px rgba(40, 34, 24, 0.20);
|
|
|
|
|
- z-index: 2;
|
|
|
|
|
- }
|
|
|
|
|
- .card .thumb {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 0; left: 0;
|
|
|
|
|
- width: 1920px;
|
|
|
|
|
- height: 1080px;
|
|
|
|
|
- transform-origin: top left;
|
|
|
|
|
- /* scale 由 JS 按卡片实际宽度设置 */
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
|
|
+ body[data-ov="grid"] #ov-gallery { display: none; }
|
|
|
|
|
+ body[data-ov="gallery"] #ov-grid { display: none; }
|
|
|
|
|
+ body[data-ov="gallery"][data-mode="overview"] { background: #0a0805; }
|
|
|
|
|
+ body[data-ov="grid"][data-mode="overview"] { background: #efece4; }
|
|
|
|
|
+
|
|
|
|
|
+ /* ════════ 模式 1 · 自适应网格墙(一屏装得下→居中轻斜;装不下→舒适大小竖向滚动)════════ */
|
|
|
|
|
+ #ov-grid { display: flex; justify-content: center; perspective: 3200px; perspective-origin: 50% 42%;
|
|
|
|
|
+ background: radial-gradient(120% 90% at 50% 0%, #f4f1e9, #e7e3d8 75%); }
|
|
|
|
|
+ body[data-grid="fit"] #ov-grid { overflow: hidden; align-items: center; }
|
|
|
|
|
+ body[data-grid="scroll"] #ov-grid { overflow-y: auto; overflow-x: hidden; align-items: flex-start; padding: 74px 0 110px; }
|
|
|
|
|
+ .wall-wrap { }
|
|
|
|
|
+ body[data-grid="fit"] .wall-wrap { transform: translateY(-3vh); }
|
|
|
|
|
+ .wall { display: grid; gap: 20px; transform-origin: center center; margin: 0 auto; } /* 不用 preserve-3d:整墙作单个倾斜平面,命中测试可靠 */
|
|
|
|
|
+ #ov-grid .card {
|
|
|
|
|
+ position: relative; aspect-ratio: 16/9; border-radius: 9px; overflow: hidden; background: #fff; cursor: pointer;
|
|
|
|
|
+ box-shadow: 0 16px 34px rgba(40,34,24,0.20), 0 3px 10px rgba(40,34,24,0.12);
|
|
|
|
|
+ transition: transform .28s cubic-bezier(.2,.7,.2,1), box-shadow .28s;
|
|
|
}
|
|
}
|
|
|
- .card .thumb iframe {
|
|
|
|
|
- width: 1920px;
|
|
|
|
|
- height: 1080px;
|
|
|
|
|
- border: 0;
|
|
|
|
|
- display: block;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
|
|
+ #ov-grid .card:hover {
|
|
|
|
|
+ transform: scale(1.18);
|
|
|
|
|
+ box-shadow: 0 48px 92px rgba(40,34,24,0.36), 0 14px 30px rgba(40,34,24,0.22); z-index: 20;
|
|
|
}
|
|
}
|
|
|
- .card .num {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- bottom: 8px; left: 10px;
|
|
|
|
|
- background: rgba(20, 16, 10, 0.72);
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- line-height: 1;
|
|
|
|
|
- padding: 5px 9px;
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- font-variant-numeric: tabular-nums;
|
|
|
|
|
- letter-spacing: 0.04em;
|
|
|
|
|
- z-index: 3;
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
- }
|
|
|
|
|
- #overview-title {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- color: #5a5346;
|
|
|
|
|
- font-size: 15px;
|
|
|
|
|
- letter-spacing: 0.18em;
|
|
|
|
|
- margin: 0 auto 5vw;
|
|
|
|
|
- text-transform: uppercase;
|
|
|
|
|
- opacity: 0.7;
|
|
|
|
|
- }
|
|
|
|
|
- .start-btn {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- bottom: 28px; right: 28px;
|
|
|
|
|
- background: #1a1712;
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- border: 0;
|
|
|
|
|
- padding: 14px 26px;
|
|
|
|
|
- border-radius: 999px;
|
|
|
|
|
- font-size: 15px;
|
|
|
|
|
- font-family: inherit;
|
|
|
|
|
- letter-spacing: 0.04em;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- z-index: 200;
|
|
|
|
|
- box-shadow: 0 8px 28px rgba(40, 34, 24, 0.28);
|
|
|
|
|
- transition: transform 0.18s, box-shadow 0.18s;
|
|
|
|
|
- }
|
|
|
|
|
- .start-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 34px rgba(40, 34, 24, 0.36); }
|
|
|
|
|
|
|
|
|
|
- /* ════════════════════════════════════════════════════════ */
|
|
|
|
|
- /* 模式 B · 全屏演示 */
|
|
|
|
|
- /* ════════════════════════════════════════════════════════ */
|
|
|
|
|
- #stage {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- top: 50%; left: 50%;
|
|
|
|
|
- transform-origin: top left;
|
|
|
|
|
- will-change: transform;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- box-shadow: 0 10px 60px rgba(0,0,0,0.4);
|
|
|
|
|
- /* size set by JS from DECK_WIDTH/HEIGHT */
|
|
|
|
|
|
|
+ /* ════════ 模式 2 · 无限画廊 ════════ */
|
|
|
|
|
+ #ov-gallery { overflow: hidden; perspective: 2200px; perspective-origin: 50% 42%;
|
|
|
|
|
+ background: radial-gradient(130% 120% at 50% 38%, #1b1610, #0a0805 82%); }
|
|
|
|
|
+ .stage3d { --rx: 18; --rz: 9; position: absolute; inset: 0; transform-style: preserve-3d; transform: scale(1.22) rotateX(18deg) rotateZ(-9deg); transform-origin: center center; }
|
|
|
|
|
+ .breathe { position: absolute; inset: 0; animation: breathe 26s ease-in-out infinite; transform-origin: center center; transform-style: preserve-3d; }
|
|
|
|
|
+ .drift-layer { position: absolute; top: 0; left: 0; will-change: transform; animation: drift var(--driftSec, 80s) linear infinite; transform-style: preserve-3d; }
|
|
|
|
|
+ #ov-gallery:hover .drift-layer { animation-play-state: paused; }
|
|
|
|
|
+ .gallery { display: grid; }
|
|
|
|
|
+ #ov-gallery .card {
|
|
|
|
|
+ position: relative; width: 100%; aspect-ratio: 16/9; border-radius: 8px; overflow: hidden; background: #fff; cursor: pointer;
|
|
|
|
|
+ box-shadow: 0 14px 30px rgba(0,0,0,0.42), 0 3px 9px rgba(0,0,0,0.3);
|
|
|
|
|
+ transition: transform .34s cubic-bezier(.2,.7,.2,1), box-shadow .34s; transform-style: preserve-3d; will-change: transform;
|
|
|
}
|
|
}
|
|
|
- #stage iframe {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- border: 0;
|
|
|
|
|
- display: block;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
|
|
+ #ov-gallery .card:hover {
|
|
|
|
|
+ transform: translateZ(60px) rotateX(calc(var(--rx,18)*-1deg)) rotateZ(calc(var(--rz,9)*1deg)) scale(1.06);
|
|
|
|
|
+ box-shadow: 0 30px 64px rgba(0,0,0,0.55), 0 8px 20px rgba(0,0,0,0.4); z-index: 30;
|
|
|
}
|
|
}
|
|
|
- .counter {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- bottom: 20px;
|
|
|
|
|
- right: 20px;
|
|
|
|
|
- background: rgba(0,0,0,0.65);
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- padding: 6px 14px;
|
|
|
|
|
- border-radius: 999px;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- letter-spacing: 0.05em;
|
|
|
|
|
- font-variant-numeric: tabular-nums;
|
|
|
|
|
- z-index: 100;
|
|
|
|
|
- user-select: none;
|
|
|
|
|
- opacity: 0.7;
|
|
|
|
|
- transition: opacity 0.2s;
|
|
|
|
|
- }
|
|
|
|
|
- .counter:hover { opacity: 1; }
|
|
|
|
|
|
|
+ .vignette { position: fixed; inset: 0; pointer-events: none; z-index: 20; box-shadow: inset 0 0 240px 70px rgba(8,6,3,0.62); }
|
|
|
|
|
+ @keyframes drift { from { transform: translate3d(0,0,0); } to { transform: translate3d(calc(var(--dx)*-1), calc(var(--dy)*-1), 0); } }
|
|
|
|
|
+ @keyframes breathe { 0%,100% { transform: scale(1.0); } 50% { transform: scale(1.045); } }
|
|
|
|
|
+
|
|
|
|
|
+ /* 卡片通用内容 */
|
|
|
|
|
+ .card .thumb { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; transform-origin: top left; pointer-events: none; }
|
|
|
|
|
+ .card .thumb iframe { width: 1920px; height: 1080px; border: 0; display: block; background: #fff; pointer-events: none; }
|
|
|
|
|
+ .card .thumb-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
|
|
|
|
|
+ .card .num { position: absolute; bottom: 6px; left: 7px; background: rgba(16,12,7,0.76); color: #fff; font-size: 11px; line-height: 1; padding: 4px 7px; border-radius: 5px; font-variant-numeric: tabular-nums; letter-spacing: 0.03em; z-index: 3; pointer-events: none; max-width: 90%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
|
+ #ov-gallery .card .num { opacity: 0; transition: opacity .25s; }
|
|
|
|
|
+ #ov-gallery .card:hover .num { opacity: 1; }
|
|
|
|
|
+
|
|
|
|
|
+ .overview-title { position: fixed; top: 24px; left: 0; right: 0; text-align: center; font-size: 13px; letter-spacing: 0.22em; text-transform: uppercase; z-index: 40; pointer-events: none; }
|
|
|
|
|
+ body[data-ov="grid"] .overview-title { color: #5a5346; opacity: 0.66; }
|
|
|
|
|
+ body[data-ov="gallery"] .overview-title { color: rgba(244,238,224,0.7); text-shadow: 0 2px 12px rgba(0,0,0,0.6); }
|
|
|
|
|
+
|
|
|
|
|
+ .start-btn { position: fixed; bottom: 26px; right: 26px; border: 0; padding: 13px 24px; border-radius: 999px; font-size: 15px; font-family: inherit; letter-spacing: 0.04em; cursor: pointer; z-index: 200; transition: transform .18s, box-shadow .18s; }
|
|
|
|
|
+ body[data-ov="grid"] .start-btn { background: #1a1712; color: #fff; box-shadow: 0 8px 28px rgba(40,34,24,0.28); }
|
|
|
|
|
+ body[data-ov="gallery"] .start-btn { background: #f4eee0; color: #1a1712; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
|
|
|
|
|
+ .start-btn:hover { transform: translateY(-2px); }
|
|
|
|
|
+
|
|
|
|
|
+ /* ════════ 演示模式 ════════ */
|
|
|
|
|
+ #stage { position: fixed; top: 50%; left: 50%; transform-origin: top left; will-change: transform; background: #fff; box-shadow: 0 10px 60px rgba(0,0,0,0.4); }
|
|
|
|
|
+ #stage iframe { width: 100%; height: 100%; border: 0; display: block; background: #fff; }
|
|
|
|
|
+ .counter { position: fixed; bottom: 20px; right: 20px; background: rgba(0,0,0,0.65); color: #fff; padding: 6px 14px; border-radius: 999px; font-size: 13px; letter-spacing: 0.05em; font-variant-numeric: tabular-nums; z-index: 100; opacity: 0.7; }
|
|
|
.counter .label { color: rgba(255,255,255,0.7); margin-left: 8px; }
|
|
.counter .label { color: rgba(255,255,255,0.7); margin-left: 8px; }
|
|
|
- .overview-btn {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- top: 20px; left: 20px;
|
|
|
|
|
- background: rgba(0,0,0,0.55);
|
|
|
|
|
- color: rgba(255,255,255,0.85);
|
|
|
|
|
- border: 0;
|
|
|
|
|
- padding: 7px 14px;
|
|
|
|
|
- border-radius: 999px;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- font-family: inherit;
|
|
|
|
|
- letter-spacing: 0.04em;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- z-index: 100;
|
|
|
|
|
- opacity: 0.6;
|
|
|
|
|
- transition: opacity 0.2s;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .overview-btn { position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.55); color: rgba(255,255,255,0.85); border: 0; padding: 7px 14px; border-radius: 999px; font-size: 13px; font-family: inherit; cursor: pointer; z-index: 100; opacity: 0.6; }
|
|
|
.overview-btn:hover { opacity: 1; }
|
|
.overview-btn:hover { opacity: 1; }
|
|
|
- .nav-zone {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- top: 0; bottom: 0;
|
|
|
|
|
- width: 15%;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- z-index: 50;
|
|
|
|
|
- }
|
|
|
|
|
- .nav-zone.left { left: 0; }
|
|
|
|
|
- .nav-zone.right { right: 0; }
|
|
|
|
|
- .nav-hint {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 50%; transform: translateY(-50%);
|
|
|
|
|
- width: 44px; height: 44px;
|
|
|
|
|
- border-radius: 999px;
|
|
|
|
|
- background: rgba(255,255,255,0.08);
|
|
|
|
|
- color: rgba(255,255,255,0.6);
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- font-size: 22px;
|
|
|
|
|
- opacity: 0;
|
|
|
|
|
- transition: opacity 0.2s;
|
|
|
|
|
- }
|
|
|
|
|
- .nav-zone.left .nav-hint { left: 20px; }
|
|
|
|
|
- .nav-zone.right .nav-hint { right: 20px; }
|
|
|
|
|
|
|
+ .nav-zone { position: fixed; top: 0; bottom: 0; width: 15%; cursor: pointer; z-index: 50; }
|
|
|
|
|
+ .nav-zone.left { left: 0; } .nav-zone.right { right: 0; }
|
|
|
|
|
+ .nav-hint { position: absolute; top: 50%; transform: translateY(-50%); width: 44px; height: 44px; border-radius: 999px; background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.6); display: flex; align-items: center; justify-content: center; font-size: 22px; opacity: 0; transition: opacity .2s; }
|
|
|
|
|
+ .nav-zone.left .nav-hint { left: 20px; } .nav-zone.right .nav-hint { right: 20px; }
|
|
|
.nav-zone:hover .nav-hint { opacity: 1; }
|
|
.nav-zone:hover .nav-hint { opacity: 1; }
|
|
|
|
|
|
|
|
- /* Print: one slide per page, no navigation UI */
|
|
|
|
|
@media print {
|
|
@media print {
|
|
|
@page { size: 1920px 1080px; margin: 0; }
|
|
@page { size: 1920px 1080px; margin: 0; }
|
|
|
html, body { background: #fff; overflow: visible; height: auto; }
|
|
html, body { background: #fff; overflow: visible; height: auto; }
|
|
|
- body[data-mode] #overview { display: none !important; }
|
|
|
|
|
|
|
+ body[data-mode] .overview { display: none !important; }
|
|
|
#stage { position: static; transform: none !important; box-shadow: none; }
|
|
#stage { position: static; transform: none !important; box-shadow: none; }
|
|
|
.counter, .nav-zone, .overview-btn, .start-btn { display: none !important; }
|
|
.counter, .nav-zone, .overview-btn, .start-btn { display: none !important; }
|
|
|
- /* In print mode we render all slides sequentially — see JS */
|
|
|
|
|
.print-stack { display: block; }
|
|
.print-stack { display: block; }
|
|
|
- .print-stack iframe {
|
|
|
|
|
- width: 1920px;
|
|
|
|
|
- height: 1080px;
|
|
|
|
|
- page-break-after: always;
|
|
|
|
|
- display: block;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .print-stack iframe { width: 1920px; height: 1080px; page-break-after: always; display: block; }
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|
|
|
</head>
|
|
</head>
|
|
|
<body data-mode="overview">
|
|
<body data-mode="overview">
|
|
|
|
|
|
|
|
-<!-- ── 模式 A · 概览墙 ─────────────────────────────────────── -->
|
|
|
|
|
-<div id="overview">
|
|
|
|
|
- <div id="overview-title">Overview · 点击任意页进入演示</div>
|
|
|
|
|
- <div class="wall" id="wall"></div>
|
|
|
|
|
|
|
+<div id="ov-grid" class="overview">
|
|
|
|
|
+ <div class="overview-title">Overview · 点击任意页进入演示</div>
|
|
|
|
|
+ <div class="wall-wrap"><div class="wall" id="wall"></div></div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+<div id="ov-gallery" class="overview">
|
|
|
|
|
+ <div class="stage3d"><div class="breathe"><div class="drift-layer" id="driftLayer"><div class="gallery" id="gallery"></div></div></div></div>
|
|
|
|
|
+ <div class="vignette"></div>
|
|
|
|
|
+ <div class="overview-title">Infinite Gallery · 悬停暂停 · 点击任意页进入演示</div>
|
|
|
|
|
+</div>
|
|
|
|
|
+
|
|
|
<button class="start-btn" id="startBtn">▶ 开始演示</button>
|
|
<button class="start-btn" id="startBtn">▶ 开始演示</button>
|
|
|
|
|
|
|
|
-<!-- ── 模式 B · 全屏演示 ───────────────────────────────────── -->
|
|
|
|
|
<div id="present-ui">
|
|
<div id="present-ui">
|
|
|
- <div id="stage">
|
|
|
|
|
- <iframe id="frame" src="about:blank"></iframe>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div id="stage"><iframe id="frame" src="about:blank"></iframe></div>
|
|
|
<button class="overview-btn" id="overviewBtn">⊞ 概览</button>
|
|
<button class="overview-btn" id="overviewBtn">⊞ 概览</button>
|
|
|
<div class="nav-zone left" id="navL"><div class="nav-hint">‹</div></div>
|
|
<div class="nav-zone left" id="navL"><div class="nav-hint">‹</div></div>
|
|
|
<div class="nav-zone right" id="navR"><div class="nav-hint">›</div></div>
|
|
<div class="nav-zone right" id="navR"><div class="nav-hint">›</div></div>
|
|
|
<div class="counter" id="counter">1 / 1</div>
|
|
<div class="counter" id="counter">1 / 1</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
-<!-- Print-only stack: populated on beforeprint, stripped on afterprint -->
|
|
|
|
|
<div class="print-stack" id="printStack" style="display:none;"></div>
|
|
<div class="print-stack" id="printStack" style="display:none;"></div>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
@@ -288,159 +150,189 @@
|
|
|
const W = window.DECK_WIDTH || 1920;
|
|
const W = window.DECK_WIDTH || 1920;
|
|
|
const H = window.DECK_HEIGHT || 1080;
|
|
const H = window.DECK_HEIGHT || 1080;
|
|
|
const deck = window.DECK_MANIFEST || [];
|
|
const deck = window.DECK_MANIFEST || [];
|
|
|
|
|
+ const CARD_W = window.GALLERY_CARD_W || 300;
|
|
|
|
|
+ const DRIFT_SEC = window.GALLERY_DRIFT_SECONDS || 80;
|
|
|
const stage = document.getElementById('stage');
|
|
const stage = document.getElementById('stage');
|
|
|
const frame = document.getElementById('frame');
|
|
const frame = document.getElementById('frame');
|
|
|
const counter = document.getElementById('counter');
|
|
const counter = document.getElementById('counter');
|
|
|
const printStack = document.getElementById('printStack');
|
|
const printStack = document.getElementById('printStack');
|
|
|
const wall = document.getElementById('wall');
|
|
const wall = document.getElementById('wall');
|
|
|
|
|
+ const gallery = document.getElementById('gallery');
|
|
|
|
|
+ const driftLayer = document.getElementById('driftLayer');
|
|
|
const storageKey = 'deck-index-' + location.pathname;
|
|
const storageKey = 'deck-index-' + location.pathname;
|
|
|
let current = 0;
|
|
let current = 0;
|
|
|
|
|
|
|
|
- stage.style.width = W + 'px';
|
|
|
|
|
- stage.style.height = H + 'px';
|
|
|
|
|
|
|
+ stage.style.width = W + 'px'; stage.style.height = H + 'px';
|
|
|
|
|
+ driftLayer.style.setProperty('--driftSec', DRIFT_SEC + 's');
|
|
|
|
|
+
|
|
|
|
|
+ // 概览模式:URL ?ov= > window.DECK_OVERVIEW > 随机(秒%5==0 → 20% 画廊)
|
|
|
|
|
+ const qp = new URLSearchParams(location.search).get('ov');
|
|
|
|
|
+ // 用户未指定时按秒数随机:网格(iframe 真页面) 60% / 无限画廊(图片缩略图) 40%。可用 ?ov=grid|gallery 或 window.DECK_OVERVIEW 固定。
|
|
|
|
|
+ const OVERVIEW = qp || window.DECK_OVERVIEW || ((new Date().getSeconds() % 5 < 2) ? 'gallery' : 'grid');
|
|
|
|
|
+ document.body.setAttribute('data-ov', OVERVIEW === 'gallery' ? 'gallery' : 'grid');
|
|
|
|
|
|
|
|
- /* ── 模式切换 ─────────────────────────────────────────── */
|
|
|
|
|
function setMode(mode) {
|
|
function setMode(mode) {
|
|
|
document.body.setAttribute('data-mode', mode);
|
|
document.body.setAttribute('data-mode', mode);
|
|
|
if (mode === 'present') { fit(); show(current); }
|
|
if (mode === 'present') { fit(); show(current); }
|
|
|
|
|
+ else { requestAnimationFrame(buildOverview); }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* ════════ 模式 A · 概览墙 ════════ */
|
|
|
|
|
- function buildWall() {
|
|
|
|
|
- wall.innerHTML = '';
|
|
|
|
|
- deck.forEach((item, i) => {
|
|
|
|
|
- const card = document.createElement('div');
|
|
|
|
|
- card.className = 'card';
|
|
|
|
|
- card.dataset.idx = i;
|
|
|
|
|
-
|
|
|
|
|
|
|
+ /* ─ 媒体(缩略图优先,否则 iframe) ─ */
|
|
|
|
|
+ function makeCard(idx, useScale, useImg) {
|
|
|
|
|
+ const card = document.createElement('div');
|
|
|
|
|
+ card.className = 'card'; card.dataset.idx = idx;
|
|
|
|
|
+ const item = deck[idx];
|
|
|
|
|
+ if (useImg && item.thumb) {
|
|
|
|
|
+ // 仅画廊用:预渲染缩略图,扛无限平铺的性能
|
|
|
|
|
+ const im = document.createElement('img');
|
|
|
|
|
+ im.className = 'thumb-img'; im.src = item.thumb; im.decoding = 'async';
|
|
|
|
|
+ card.appendChild(im);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 网格默认用:真实 HTML 子页面(清晰、所见即所得)
|
|
|
const thumb = document.createElement('div');
|
|
const thumb = document.createElement('div');
|
|
|
thumb.className = 'thumb';
|
|
thumb.className = 'thumb';
|
|
|
|
|
+ if (useScale != null) thumb.style.transform = 'scale(' + useScale + ')';
|
|
|
const ifr = document.createElement('iframe');
|
|
const ifr = document.createElement('iframe');
|
|
|
- ifr.src = item.file;
|
|
|
|
|
- ifr.setAttribute('scrolling', 'no');
|
|
|
|
|
- thumb.appendChild(ifr);
|
|
|
|
|
|
|
+ ifr.src = item.file; ifr.setAttribute('scrolling', 'no');
|
|
|
|
|
+ thumb.appendChild(ifr); card.appendChild(thumb);
|
|
|
|
|
+ }
|
|
|
|
|
+ const num = document.createElement('div');
|
|
|
|
|
+ num.className = 'num';
|
|
|
|
|
+ num.textContent = (idx + 1) + (item.label ? ' · ' + item.label : '');
|
|
|
|
|
+ card.appendChild(num);
|
|
|
|
|
+ card.addEventListener('click', () => { current = idx; setMode('present'); });
|
|
|
|
|
+ return card;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const num = document.createElement('div');
|
|
|
|
|
- num.className = 'num';
|
|
|
|
|
- num.textContent = (i + 1) + (item.label ? ' · ' + item.label : '');
|
|
|
|
|
|
|
+ function buildOverview() {
|
|
|
|
|
+ if (document.body.getAttribute('data-mode') !== 'overview') return;
|
|
|
|
|
+ if (OVERVIEW === 'gallery') buildGallery(); else buildGrid();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- card.appendChild(thumb);
|
|
|
|
|
- card.appendChild(num);
|
|
|
|
|
- card.addEventListener('click', () => { current = i; setMode('present'); });
|
|
|
|
|
- wall.appendChild(card);
|
|
|
|
|
- });
|
|
|
|
|
- scaleThumbs();
|
|
|
|
|
|
|
+ /* ════════ 网格墙(自适应) ════════ */
|
|
|
|
|
+ function buildGrid() {
|
|
|
|
|
+ const n = deck.length; if (!n) return;
|
|
|
|
|
+ wall.innerHTML = '';
|
|
|
|
|
+ for (let i = 0; i < n; i++) wall.appendChild(makeCard(i, null, false)); // 网格 = iframe 真页面
|
|
|
|
|
+ layoutWall();
|
|
|
|
|
+ }
|
|
|
|
|
+ function layoutWall() {
|
|
|
|
|
+ const n = deck.length; if (!n) return;
|
|
|
|
|
+ const vw = innerWidth, vh = innerHeight, gap = 26; // 大间距:倾斜后行也绝不重叠 → 每张可点
|
|
|
|
|
+ const target = 290;
|
|
|
|
|
+ let cols = Math.max(3, Math.min(9, Math.floor((vw * 0.94 + gap) / (target + gap))));
|
|
|
|
|
+ cols = Math.min(cols, n);
|
|
|
|
|
+ const rows = Math.ceil(n / cols);
|
|
|
|
|
+ const cardW = Math.min(target + 40, (vw * 0.94 - (cols - 1) * gap) / cols);
|
|
|
|
|
+ const cardH = cardW * 9 / 16;
|
|
|
|
|
+ const baseW = cols * cardW + (cols - 1) * gap;
|
|
|
|
|
+ const wallH = rows * cardH + (rows - 1) * gap;
|
|
|
|
|
+ wall.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
|
|
|
|
|
+ wall.style.gap = gap + 'px';
|
|
|
|
|
+ wall.style.width = baseW + 'px';
|
|
|
|
|
+ if (wallH <= vh * 0.80) {
|
|
|
|
|
+ // 一屏装得下:对角倾斜(rotateZ 主导 + 克制 rotateX,大 perspective→几乎不前后压缩→不重叠),居中铺满
|
|
|
|
|
+ document.body.setAttribute('data-grid', 'fit');
|
|
|
|
|
+ const rx = Math.max(9, Math.min(15, 15 - (rows - 2) * 1.4));
|
|
|
|
|
+ const rz = Math.max(5, Math.min(9, 9 - (rows - 2) * 0.8));
|
|
|
|
|
+ wall.style.setProperty('--rx', rx); wall.style.setProperty('--rz', rz);
|
|
|
|
|
+ const tilt = 'rotateX(' + rx + 'deg) rotateZ(-' + rz + 'deg)';
|
|
|
|
|
+ wall.style.transform = tilt;
|
|
|
|
|
+ const r = wall.getBoundingClientRect();
|
|
|
|
|
+ let s = Math.min(vw * 0.92 / r.width, vh * 0.80 / r.height);
|
|
|
|
|
+ s = Math.max(0.6, Math.min(s, 1.6));
|
|
|
|
|
+ wall.style.transform = 'scale(' + s + ') ' + tilt;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 页太多:舒适大小 + 竖向滚动;仅轻 rotateX 后仰(rotateZ 会让高墙横向裁切),平铺不重叠 → 稳定可点
|
|
|
|
|
+ document.body.setAttribute('data-grid', 'scroll');
|
|
|
|
|
+ wall.style.setProperty('--rx', 12); wall.style.setProperty('--rz', 0);
|
|
|
|
|
+ wall.style.transform = 'rotateX(12deg)';
|
|
|
|
|
+ }
|
|
|
|
|
+ wall.querySelectorAll('.card .thumb').forEach(t => { const c = t.parentElement; if (c.clientWidth) t.style.transform = 'scale(' + (c.clientWidth / W) + ')'; });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 把 1920×1080 的缩略 iframe 缩放到卡片实际宽度
|
|
|
|
|
- function scaleThumbs() {
|
|
|
|
|
- document.querySelectorAll('.card').forEach(card => {
|
|
|
|
|
- const thumb = card.querySelector('.thumb');
|
|
|
|
|
- if (!thumb) return;
|
|
|
|
|
- const w = card.clientWidth;
|
|
|
|
|
- if (!w) return;
|
|
|
|
|
- thumb.style.transform = 'scale(' + (w / W) + ')';
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ /* ════════ 无限画廊(洗牌不重复) ════════ */
|
|
|
|
|
+ function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
|
|
|
|
|
+ function shuffledRange(n, seed) { const rnd = mulberry32(seed); const a = Array.from({ length: n }, (_, i) => i); for (let i = n - 1; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); const tmp = a[i]; a[i] = a[j]; a[j] = tmp; } return a; }
|
|
|
|
|
+
|
|
|
|
|
+ function buildGallery() {
|
|
|
|
|
+ const n = deck.length; if (!n) return;
|
|
|
|
|
+ const vw = innerWidth, vh = innerHeight, gap = 18;
|
|
|
|
|
+ const cardW = CARD_W, cardH = cardW * 9 / 16;
|
|
|
|
|
+ const cellW = cardW + gap, cellH = cardH + gap;
|
|
|
|
|
+ const visCols = Math.ceil(vw / cellW), visRows = Math.ceil(vh / cellH);
|
|
|
|
|
+ // tile:宽够铺满;行数取「够铺满」与「装下全部 N 页」的较大者 → 看完所有页才重复
|
|
|
|
|
+ const tileCols = visCols + 1;
|
|
|
|
|
+ const tileRows = Math.max(visRows + 1, Math.ceil(n / tileCols));
|
|
|
|
|
+ const tileCells = tileCols * tileRows;
|
|
|
|
|
+ const perm = shuffledRange(n, 0x9e3779b9); // 洗牌一次,打散规则感
|
|
|
|
|
+ const tilePage = new Array(tileCells);
|
|
|
|
|
+ for (let k = 0; k < tileCells; k++) tilePage[k] = perm[k % n];
|
|
|
|
|
+
|
|
|
|
|
+ const cols = tileCols * 2, rows = tileRows * 2; // 2× tile 保证无缝漂移
|
|
|
|
|
+ driftLayer.style.setProperty('--dx', (tileCols * cellW) + 'px');
|
|
|
|
|
+ driftLayer.style.setProperty('--dy', (tileRows * cellH) + 'px');
|
|
|
|
|
+ gallery.style.gridTemplateColumns = 'repeat(' + cols + ', ' + cardW + 'px)';
|
|
|
|
|
+ gallery.style.gridAutoRows = cardH + 'px';
|
|
|
|
|
+ gallery.style.gap = gap + 'px';
|
|
|
|
|
+ gallery.innerHTML = '';
|
|
|
|
|
+ const scale = cardW / W;
|
|
|
|
|
+ for (let r = 0; r < rows; r++) {
|
|
|
|
|
+ for (let c = 0; c < cols; c++) {
|
|
|
|
|
+ const idx = tilePage[(r % tileRows) * tileCols + (c % tileCols)];
|
|
|
|
|
+ gallery.appendChild(makeCard(idx, scale, true)); // 画廊 = 图片缩略图(性能)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* ════════ 模式 B · 全屏演示 ════════ */
|
|
|
|
|
|
|
+ /* ════════ 演示 ════════ */
|
|
|
function fit() {
|
|
function fit() {
|
|
|
- const s = Math.min(window.innerWidth / W, window.innerHeight / H);
|
|
|
|
|
- const x = (window.innerWidth - W * s) / 2;
|
|
|
|
|
- const y = (window.innerHeight - H * s) / 2;
|
|
|
|
|
- stage.style.transform = `translate(${x}px, ${y}px) scale(${s})`;
|
|
|
|
|
- stage.style.top = '0';
|
|
|
|
|
- stage.style.left = '0';
|
|
|
|
|
|
|
+ const s = Math.min(innerWidth / W, innerHeight / H);
|
|
|
|
|
+ stage.style.transform = 'translate(' + ((innerWidth - W * s) / 2) + 'px, ' + ((innerHeight - H * s) / 2) + 'px) scale(' + s + ')';
|
|
|
|
|
+ stage.style.top = '0'; stage.style.left = '0';
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
function show(idx) {
|
|
function show(idx) {
|
|
|
if (idx < 0 || idx >= deck.length) return;
|
|
if (idx < 0 || idx >= deck.length) return;
|
|
|
current = idx;
|
|
current = idx;
|
|
|
- const wanted = deck[idx].file;
|
|
|
|
|
- if (frame.getAttribute('src') !== wanted) frame.src = wanted;
|
|
|
|
|
- counter.innerHTML = `${idx + 1} / ${deck.length} <span class="label">${deck[idx].label || ''}</span>`;
|
|
|
|
|
|
|
+ if (frame.getAttribute('src') !== deck[idx].file) frame.src = deck[idx].file;
|
|
|
|
|
+ counter.innerHTML = (idx + 1) + ' / ' + deck.length + ' <span class="label">' + (deck[idx].label || '') + '</span>';
|
|
|
try { localStorage.setItem(storageKey, String(idx)); } catch (_) {}
|
|
try { localStorage.setItem(storageKey, String(idx)); } catch (_) {}
|
|
|
- if (location.hash !== '#' + (idx + 1)) {
|
|
|
|
|
- history.replaceState(null, '', '#' + (idx + 1));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (location.hash !== '#' + (idx + 1)) history.replaceState(null, '', '#' + (idx + 1));
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
function next() { show(Math.min(current + 1, deck.length - 1)); }
|
|
function next() { show(Math.min(current + 1, deck.length - 1)); }
|
|
|
function prev() { show(Math.max(current - 1, 0)); }
|
|
function prev() { show(Math.max(current - 1, 0)); }
|
|
|
|
|
|
|
|
- // Keyboard
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
document.addEventListener('keydown', (e) => {
|
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
|
- // ESC 回概览(演示模式下)
|
|
|
|
|
- if (e.key === 'Escape') {
|
|
|
|
|
- if (document.body.getAttribute('data-mode') === 'present') { e.preventDefault(); setMode('overview'); }
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (e.key === 'Escape') { if (document.body.getAttribute('data-mode') === 'present') { e.preventDefault(); setMode('overview'); } return; }
|
|
|
if (e.key === 'p' || e.key === 'P') { window.print(); return; }
|
|
if (e.key === 'p' || e.key === 'P') { window.print(); return; }
|
|
|
- // 其余翻页键仅在演示模式生效
|
|
|
|
|
if (document.body.getAttribute('data-mode') !== 'present') return;
|
|
if (document.body.getAttribute('data-mode') !== 'present') return;
|
|
|
switch (e.key) {
|
|
switch (e.key) {
|
|
|
case 'ArrowRight': case ' ': case 'PageDown': e.preventDefault(); next(); break;
|
|
case 'ArrowRight': case ' ': case 'PageDown': e.preventDefault(); next(); break;
|
|
|
- case 'ArrowLeft': case 'PageUp': e.preventDefault(); prev(); break;
|
|
|
|
|
- case 'Home': e.preventDefault(); show(0); break;
|
|
|
|
|
- case 'End': e.preventDefault(); show(deck.length - 1); break;
|
|
|
|
|
- default:
|
|
|
|
|
- if (e.key >= '1' && e.key <= '9') {
|
|
|
|
|
- const i = parseInt(e.key, 10) - 1;
|
|
|
|
|
- if (i < deck.length) { e.preventDefault(); show(i); }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ case 'ArrowLeft': case 'PageUp': e.preventDefault(); prev(); break;
|
|
|
|
|
+ case 'Home': e.preventDefault(); show(0); break;
|
|
|
|
|
+ case 'End': e.preventDefault(); show(deck.length - 1); break;
|
|
|
|
|
+ default: if (e.key >= '1' && e.key <= '9') { const i = parseInt(e.key, 10) - 1; if (i < deck.length) { e.preventDefault(); show(i); } }
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
document.getElementById('navL').addEventListener('click', prev);
|
|
document.getElementById('navL').addEventListener('click', prev);
|
|
|
document.getElementById('navR').addEventListener('click', next);
|
|
document.getElementById('navR').addEventListener('click', next);
|
|
|
document.getElementById('startBtn').addEventListener('click', () => setMode('present'));
|
|
document.getElementById('startBtn').addEventListener('click', () => setMode('present'));
|
|
|
document.getElementById('overviewBtn').addEventListener('click', () => setMode('overview'));
|
|
document.getElementById('overviewBtn').addEventListener('click', () => setMode('overview'));
|
|
|
- window.addEventListener('resize', () => { fit(); scaleThumbs(); });
|
|
|
|
|
- window.addEventListener('hashchange', () => {
|
|
|
|
|
- const m = location.hash.match(/^#(\d+)$/);
|
|
|
|
|
- if (m) { current = parseInt(m[1], 10) - 1; setMode('present'); }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ let rT;
|
|
|
|
|
+ window.addEventListener('resize', () => { fit(); clearTimeout(rT); rT = setTimeout(buildOverview, 140); });
|
|
|
|
|
+ window.addEventListener('hashchange', () => { const m = location.hash.match(/^#(\d+)$/); if (m) { current = parseInt(m[1], 10) - 1; setMode('present'); } });
|
|
|
|
|
|
|
|
- // 起始页:hash > localStorage > 0(决定 current;hash 还会直接进演示)
|
|
|
|
|
const hashMatch = location.hash.match(/^#(\d+)$/);
|
|
const hashMatch = location.hash.match(/^#(\d+)$/);
|
|
|
- if (hashMatch) {
|
|
|
|
|
- current = Math.min(parseInt(hashMatch[1], 10) - 1, deck.length - 1);
|
|
|
|
|
- } else {
|
|
|
|
|
- try {
|
|
|
|
|
- const v = parseInt(localStorage.getItem(storageKey), 10);
|
|
|
|
|
- if (!isNaN(v) && v >= 0 && v < deck.length) current = v;
|
|
|
|
|
- } catch (_) {}
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (hashMatch) current = Math.min(parseInt(hashMatch[1], 10) - 1, deck.length - 1);
|
|
|
|
|
+ else { try { const v = parseInt(localStorage.getItem(storageKey), 10); if (!isNaN(v) && v >= 0 && v < deck.length) current = v; } catch (_) {} }
|
|
|
|
|
|
|
|
- // 先把演示舞台准备好(即便默认在概览,CSS 隐藏它)
|
|
|
|
|
- fit();
|
|
|
|
|
- show(current);
|
|
|
|
|
|
|
+ fit(); show(current);
|
|
|
|
|
+ if (hashMatch) setMode('present'); else { setMode('overview'); buildOverview(); }
|
|
|
|
|
|
|
|
- buildWall();
|
|
|
|
|
-
|
|
|
|
|
- // 默认进概览;但若 URL 带 #N,直接进演示从该页开始
|
|
|
|
|
- if (hashMatch) setMode('present');
|
|
|
|
|
- else setMode('overview');
|
|
|
|
|
-
|
|
|
|
|
- // Print: build a stack of all iframes so browser prints every slide
|
|
|
|
|
- window.addEventListener('beforeprint', () => {
|
|
|
|
|
- printStack.innerHTML = '';
|
|
|
|
|
- deck.forEach(item => {
|
|
|
|
|
- const f = document.createElement('iframe');
|
|
|
|
|
- f.src = item.file;
|
|
|
|
|
- printStack.appendChild(f);
|
|
|
|
|
- });
|
|
|
|
|
- printStack.style.display = 'block';
|
|
|
|
|
- stage.style.display = 'none';
|
|
|
|
|
- });
|
|
|
|
|
- window.addEventListener('afterprint', () => {
|
|
|
|
|
- printStack.innerHTML = '';
|
|
|
|
|
- printStack.style.display = 'none';
|
|
|
|
|
- stage.style.display = '';
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ window.addEventListener('beforeprint', () => { printStack.innerHTML = ''; deck.forEach(item => { const f = document.createElement('iframe'); f.src = item.file; printStack.appendChild(f); }); printStack.style.display = 'block'; stage.style.display = 'none'; });
|
|
|
|
|
+ window.addEventListener('afterprint', () => { printStack.innerHTML = ''; printStack.style.display = 'none'; stage.style.display = ''; });
|
|
|
})();
|
|
})();
|
|
|
</script>
|
|
</script>
|
|
|
-
|
|
|
|
|
</body>
|
|
</body>
|
|
|
</html>
|
|
</html>
|