/* html-ppt :: runtime.js * Keyboard-driven deck runtime. Zero dependencies. * * Features: * ← → / space / PgUp PgDn / Home End navigation * F fullscreen * S presenter mode (split view: current + next + notes + timer) * N quick notes overlay (bottom drawer, legacy) * O slide overview grid * T cycle themes (reads data-themes on or ) * A cycle demo animation on current slide * URL hash #/N deep-link to slide N (1-based) * Progress bar auto-managed */ (function () { 'use strict'; const ANIMS = ['fade-up','fade-down','fade-left','fade-right','rise-in','drop-in', 'zoom-pop','blur-in','glitch-in','typewriter','neon-glow','shimmer-sweep', 'gradient-flow','stagger-list','counter-up','path-draw','parallax-tilt', 'card-flip-3d','cube-rotate-3d','page-turn-3d','perspective-zoom', 'marquee-scroll','kenburns','confetti-burst','spotlight','morph-shape','ripple-reveal']; function ready(fn){ if(document.readyState!='loading')fn(); else document.addEventListener('DOMContentLoaded',fn);} ready(function () { const deck = document.querySelector('.deck'); if (!deck) return; const slides = Array.from(deck.querySelectorAll('.slide')); if (!slides.length) return; let idx = 0; const total = slides.length; // progress bar let bar = document.querySelector('.progress-bar'); if (!bar) { bar = document.createElement('div'); bar.className = 'progress-bar'; bar.innerHTML = ''; document.body.appendChild(bar); } const barFill = bar.querySelector('span'); // notes overlay let notes = document.querySelector('.notes-overlay'); if (!notes) { notes = document.createElement('div'); notes.className = 'notes-overlay'; document.body.appendChild(notes); } // overview let overview = document.querySelector('.overview'); if (!overview) { overview = document.createElement('div'); overview.className = 'overview'; slides.forEach((s, i) => { const t = document.createElement('div'); t.className = 'thumb'; const title = s.getAttribute('data-title') || (s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)); t.innerHTML = '
'+(i+1)+'
'+title.trim().slice(0,80)+'
'; t.addEventListener('click', () => { go(i); toggleOverview(false); }); overview.appendChild(t); }); document.body.appendChild(overview); } function go(n){ n = Math.max(0, Math.min(total-1, n)); slides.forEach((s,i) => { s.classList.toggle('is-active', i===n); s.classList.toggle('is-prev', i { const a = el.getAttribute('data-anim'); el.classList.remove('anim-'+a); void el.offsetWidth; el.classList.add('anim-'+a); }); // counter-up slides[n].querySelectorAll('.counter').forEach(el => { const target = parseFloat(el.getAttribute('data-to')||el.textContent); const dur = parseInt(el.getAttribute('data-dur')||'1200',10); const start = performance.now(); const from = 0; function tick(now){ const t = Math.min(1,(now-start)/dur); const v = from + (target-from)*(1-Math.pow(1-t,3)); el.textContent = (target % 1 === 0) ? Math.round(v) : v.toFixed(1); if (t<1) requestAnimationFrame(tick); } requestAnimationFrame(tick); }); } function toggleNotes(force){ notes.classList.toggle('open', force!==undefined?force:!notes.classList.contains('open')); } function toggleOverview(force){ overview.classList.toggle('open', force!==undefined?force:!overview.classList.contains('open')); } // ========== PRESENTER MODE ========== // Split view: current slide (left) + next slide thumb + large speaker notes + timer let presenter = document.querySelector('.presenter-view'); if (!presenter) { presenter = document.createElement('div'); presenter.className = 'presenter-view'; presenter.innerHTML = '' + '
' + '
CURRENT
' + '
' + '
' + '
' + '
' + '
NEXT
' + '
' + '
' + '
' + '
SPEAKER SCRIPT · 逐字稿
' + '
' + '
' + '
' + '
00:00
' + '
1 / ' + total + '
' + '
S 退出 · ← → 翻页 · R 重置计时
' + '
' + '
'; document.body.appendChild(presenter); } const pvStage = presenter.querySelector('#pv-stage'); const pvNext = presenter.querySelector('#pv-next'); const pvBody = presenter.querySelector('#pv-notes-body'); const pvTimer = presenter.querySelector('#pv-timer'); const pvCount = presenter.querySelector('#pv-count'); let timerStart = 0, timerHandle = null; function fmtTime(ms){ const s = Math.floor(ms/1000); const mm = String(Math.floor(s/60)).padStart(2,'0'); const ss = String(s%60).padStart(2,'0'); return mm + ':' + ss; } function startTimer(){ if (timerHandle) return; timerStart = Date.now(); timerHandle = setInterval(() => { pvTimer.textContent = fmtTime(Date.now() - timerStart); }, 1000); } function stopTimer(){ if (timerHandle) { clearInterval(timerHandle); timerHandle = null; } } function resetTimer(){ timerStart = Date.now(); pvTimer.textContent = '00:00'; } function renderPresenter(n){ if (!presenter.classList.contains('open')) return; // Clone current + next slides for mini display pvStage.innerHTML = ''; pvNext.innerHTML = ''; const curClone = slides[n].cloneNode(true); curClone.style.position = 'relative'; curClone.style.opacity = '1'; curClone.style.transform = 'none'; curClone.classList.add('is-active'); pvStage.appendChild(curClone); if (n + 1 < total) { const nxtClone = slides[n+1].cloneNode(true); nxtClone.style.position = 'relative'; nxtClone.style.opacity = '1'; nxtClone.style.transform = 'none'; nxtClone.classList.add('is-active'); pvNext.appendChild(nxtClone); } else { pvNext.innerHTML = '
— END —
'; } // Notes const note = slides[n].querySelector('.notes, aside.notes, .speaker-notes'); pvBody.innerHTML = note ? note.innerHTML : '(这一页还没有逐字稿)'; pvCount.textContent = (n+1) + ' / ' + total; } function togglePresenter(force){ const willOpen = force !== undefined ? force : !presenter.classList.contains('open'); presenter.classList.toggle('open', willOpen); document.body.classList.toggle('presenter-mode', willOpen); if (willOpen) { startTimer(); renderPresenter(idx); } else { stopTimer(); } } function fullscreen(){ const el=document.documentElement; if (!document.fullscreenElement) el.requestFullscreen&&el.requestFullscreen(); else document.exitFullscreen&&document.exitFullscreen(); } // theme cycling const root = document.documentElement; const themesAttr = root.getAttribute('data-themes') || document.body.getAttribute('data-themes'); const themes = themesAttr ? themesAttr.split(',').map(s=>s.trim()).filter(Boolean) : []; let themeIdx = 0; function cycleTheme(){ if (!themes.length) return; themeIdx = (themeIdx+1) % themes.length; const name = themes[themeIdx]; let link = document.getElementById('theme-link'); if (!link) { link = document.createElement('link'); link.rel = 'stylesheet'; link.id = 'theme-link'; document.head.appendChild(link); } // resolve relative to runtime's location const themePath = (root.getAttribute('data-theme-base') || 'assets/themes/') + name + '.css'; link.href = themePath; root.setAttribute('data-theme', name); const ind = document.querySelector('.theme-indicator'); if (ind) ind.textContent = name; } // animation cycling on current slide let animIdx = 0; function cycleAnim(){ animIdx = (animIdx+1) % ANIMS.length; const a = ANIMS[animIdx]; const target = slides[idx].querySelector('[data-anim-target]') || slides[idx]; ANIMS.forEach(x => target.classList.remove('anim-'+x)); void target.offsetWidth; target.classList.add('anim-'+a); target.setAttribute('data-anim', a); const ind = document.querySelector('.anim-indicator'); if (ind) ind.textContent = a; } document.addEventListener('keydown', function (e) { if (e.metaKey||e.ctrlKey||e.altKey) return; switch (e.key) { case 'ArrowRight': case ' ': case 'PageDown': case 'Enter': go(idx+1); e.preventDefault(); break; case 'ArrowLeft': case 'PageUp': case 'Backspace': go(idx-1); e.preventDefault(); break; case 'Home': go(0); break; case 'End': go(total-1); break; case 'f': case 'F': fullscreen(); break; case 's': case 'S': togglePresenter(); break; case 'n': case 'N': toggleNotes(); break; case 'r': case 'R': if (presenter.classList.contains('open')) resetTimer(); break; case 'o': case 'O': toggleOverview(); break; case 't': case 'T': cycleTheme(); break; case 'a': case 'A': cycleAnim(); break; case 'Escape': toggleOverview(false); toggleNotes(false); togglePresenter(false); break; } }); // hash deep-link function fromHash(){ const m = /^#\/(\d+)/.exec(location.hash||''); if (m) go(Math.max(0, parseInt(m[1],10)-1)); } window.addEventListener('hashchange', fromHash); fromHash(); go(idx); }); })();