Files
BZJZ_Material/文档润色流和知识库构建流/huashu-design/assets/deck_index.html

349 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Deck · Multi-file Slide Index</title>
<!--
deck_index.html — 多文件 slide deck 拼接器 · 双概览模式
概览(默认进入,两种模式按 getSeconds()%5 随机20% 画廊 / 80% 网格):
· grid 自适应网格墙:列数随页数+视口自适应行多则倾斜变平scale-to-fit 居中铺满(任意页数都不溢出/不失真)
· gallery 无限画廊:固定卡片大小,无缝无限平铺 + 缓慢漂移 + 轻微呼吸;一个 tile 含全部页(洗牌),看完所有页才重复
演示 present单页 fit 缩放翻页。ESC / ⊞ 回概览。
性能manifest 每项可带 thumb预渲染缩略图概览即用 <img> 平铺,避免 N 个 iframe 同时加载。
用法:复制为 index.html建 slides/ 放每页独立 HTML编辑 MANIFEST。
可选覆盖window.DECK_OVERVIEW = 'grid' | 'gallery'不写则随机URL ?ov=grid|gallery 临时强制。
键盘(演示):← / → / Space / PgUp / PgDown / Home / End / 1-9 / P 打印 / ESC 回概览
-->
<script>
window.DECK_MANIFEST = [
{ file: "slides/01-cover.html", label: "Cover" /*, thumb: "thumbs/01.jpg" */ },
];
window.DECK_WIDTH = 1920;
window.DECK_HEIGHT = 1080;
window.GALLERY_CARD_W = 300; // 画廊卡片基准宽度
window.GALLERY_DRIFT_SECONDS = 80; // 画廊漂移一圈时长
// window.DECK_OVERVIEW = 'grid'; // 取消注释可固定概览模式
</script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; font-family: -apple-system, "PingFang SC", sans-serif; background: #0a0a0a; }
.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-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: 2200px; 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;
}
#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;
}
/* ════════ 模式 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;
}
#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;
}
.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; }
.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; }
.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; }
@media print {
@page { size: 1920px 1080px; margin: 0; }
html, body { background: #fff; overflow: visible; height: auto; }
body[data-mode] .overview { display: none !important; }
#stage { position: static; transform: none !important; box-shadow: none; }
.counter, .nav-zone, .overview-btn, .start-btn { display: none !important; }
.print-stack { display: block; }
.print-stack iframe { width: 1920px; height: 1080px; page-break-after: always; display: block; }
}
</style>
</head>
<body data-mode="overview">
<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 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>
<div id="present-ui">
<div id="stage"><iframe id="frame" src="about:blank"></iframe></div>
<button class="overview-btn" id="overviewBtn">⊞ 概览</button>
<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="counter" id="counter">1 / 1</div>
</div>
<div class="print-stack" id="printStack" style="display:none;"></div>
<script>
(function () {
const W = window.DECK_WIDTH || 1920;
const H = window.DECK_HEIGHT || 1080;
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 frame = document.getElementById('frame');
const counter = document.getElementById('counter');
const printStack = document.getElementById('printStack');
const wall = document.getElementById('wall');
const gallery = document.getElementById('gallery');
const driftLayer = document.getElementById('driftLayer');
const storageKey = 'deck-index-' + location.pathname;
let current = 0;
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) {
document.body.setAttribute('data-mode', mode);
if (mode === 'present') { fit(); show(current); }
else { requestAnimationFrame(buildOverview); }
}
/* ─ 媒体(缩略图优先,否则 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');
thumb.className = 'thumb';
if (useScale != null) thumb.style.transform = 'scale(' + useScale + ')';
const ifr = document.createElement('iframe');
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;
}
function buildOverview() {
if (document.body.getAttribute('data-mode') !== 'overview') return;
if (OVERVIEW === 'gallery') buildGallery(); else buildGrid();
}
/* ════════ 网格墙(自适应) ════════ */
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();
}
const GRID_PAD = 74; // #ov-grid scroll 顶部 padding算动态 transform-origin 用
function layoutWall() {
const n = deck.length; if (!n) return;
const vw = innerWidth, vh = innerHeight, gap = 26;
const availW = vw * 0.86; // 留 rotateZ 横向余量(强对角也不裁切)
const target = 280;
let cols = Math.max(3, Math.min(9, Math.floor((availW + gap) / (target + gap))));
cols = Math.min(cols, n);
const rows = Math.ceil(n / cols);
const cardW = Math.min(target + 40, (availW - (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';
// 单平面(无 preserve-3d)→任意角度都不重叠、可点fit 与 scroll 都给强对角倾斜
if (wallH <= vh * 0.80) {
document.body.setAttribute('data-grid', 'fit');
const rx = Math.max(11, Math.min(15, 15 - (rows - 2) * 1.1));
const rz = Math.max(8, Math.min(11, 11 - (rows - 2) * 0.7));
wall.style.setProperty('--rx', rx); wall.style.setProperty('--rz', rz);
wall.style.transformOrigin = 'center center';
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 {
// 多页:舒适大小竖向滚动 + 强对角倾斜transform-origin 跟随滚动→消除高墙 rotateZ 把远端行甩出屏幕
document.body.setAttribute('data-grid', 'scroll');
wall.style.setProperty('--rx', 14); wall.style.setProperty('--rz', 7);
wall.style.transform = 'rotateX(14deg) rotateZ(-7deg)';
updateScrollOrigin();
}
wall.querySelectorAll('.card .thumb').forEach(t => { const c = t.parentElement; if (c.clientWidth) t.style.transform = 'scale(' + (c.clientWidth / W) + ')'; });
}
function updateScrollOrigin() {
if (document.body.getAttribute('data-grid') !== 'scroll') return;
const ovg = document.getElementById('ov-grid');
wall.style.transformOrigin = '50% ' + (ovg.scrollTop + window.innerHeight / 2 - GRID_PAD) + 'px';
}
/* ════════ 无限画廊(洗牌不重复) ════════ */
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)); // 画廊 = 图片缩略图(性能)
}
}
}
/* ════════ 演示 ════════ */
function fit() {
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) {
if (idx < 0 || idx >= deck.length) return;
current = idx;
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 (_) {}
if (location.hash !== '#' + (idx + 1)) history.replaceState(null, '', '#' + (idx + 1));
}
function next() { show(Math.min(current + 1, deck.length - 1)); }
function prev() { show(Math.max(current - 1, 0)); }
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') 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 (document.body.getAttribute('data-mode') !== 'present') return;
switch (e.key) {
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); } }
}
});
document.getElementById('navL').addEventListener('click', prev);
document.getElementById('navR').addEventListener('click', next);
document.getElementById('startBtn').addEventListener('click', () => setMode('present'));
document.getElementById('overviewBtn').addEventListener('click', () => setMode('overview'));
let rT;
window.addEventListener('resize', () => { fit(); clearTimeout(rT); rT = setTimeout(buildOverview, 140); });
document.getElementById('ov-grid').addEventListener('scroll', updateScrollOrigin, { passive: true });
window.addEventListener('hashchange', () => { const m = location.hash.match(/^#(\d+)$/); if (m) { current = parseInt(m[1], 10) - 1; setMode('present'); } });
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 (_) {} }
fit(); show(current);
if (hashMatch) setMode('present'); else { setMode('overview'); buildOverview(); }
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>
</body>
</html>