/* html-ppt :: runtime.js * Keyboard-driven deck runtime. Zero dependencies. * * Features: * ← → / space / PgUp PgDn / Home End navigation * F fullscreen * S presenter mode (opens a NEW WINDOW with current/next slide preview + notes + timer) * The original window stays as audience view, synced via BroadcastChannel. * Slide previews use CSS transform:scale() at design resolution for pixel-perfect layout. * N quick notes overlay (bottom drawer) * 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);} /* ========== Parse URL for preview-only mode ========== * When loaded as iframe.src = "index.html?preview=3", runtime enters a * locked single-slide mode: only slide N is visible, no chrome, no keys, * no hash updates. This is how the presenter window shows pixel-perfect * previews — by loading the actual deck file in an iframe and telling it * to display only a specific slide. */ function getPreviewIdx() { const m = /[?&]preview=(\d+)/.exec(location.search || ''); return m ? parseInt(m[1], 10) - 1 : -1; } ready(function () { const deck = document.querySelector('.deck'); if (!deck) return; const slides = Array.from(deck.querySelectorAll('.slide')); if (!slides.length) return; const previewOnlyIdx = getPreviewIdx(); const isPreviewMode = previewOnlyIdx >= 0 && previewOnlyIdx < slides.length; /* ===== Preview-only mode: show one slide, hide everything else ===== */ if (isPreviewMode) { function showSlide(i) { slides.forEach((s, j) => { const active = (j === i); s.classList.toggle('is-active', active); s.style.display = active ? '' : 'none'; if (active) { s.style.opacity = '1'; s.style.transform = 'none'; s.style.pointerEvents = 'auto'; } }); } showSlide(previewOnlyIdx); /* Hide chrome that the presenter shouldn't see in preview */ const hideSel = '.progress-bar, .notes-overlay, .overview, .notes, aside.notes, .speaker-notes'; document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; }); document.documentElement.setAttribute('data-preview', '1'); document.body.setAttribute('data-preview', '1'); /* Auto-detect theme base path for theme switching in preview mode */ function getPreviewThemeBase() { const base = document.documentElement.getAttribute('data-theme-base'); if (base) return base; const tl = document.getElementById('theme-link'); if (tl) { const raw = tl.getAttribute('href') || ''; const ls = raw.lastIndexOf('/'); if (ls >= 0) return raw.substring(0, ls + 1); } return 'assets/themes/'; } const previewThemeBase = getPreviewThemeBase(); /* Listen for postMessage from parent presenter window: * - preview-goto: switch visible slide WITHOUT reloading * - preview-theme: switch theme CSS link to match audience window */ window.addEventListener('message', function(e) { if (!e.data) return; if (e.data.type === 'preview-goto') { const n = parseInt(e.data.idx, 10); if (n >= 0 && n < slides.length) showSlide(n); } else if (e.data.type === 'preview-theme' && e.data.name) { let link = document.getElementById('theme-link'); if (!link) { link = document.createElement('link'); link.rel = 'stylesheet'; link.id = 'theme-link'; document.head.appendChild(link); } link.href = previewThemeBase + e.data.name + '.css'; document.documentElement.setAttribute('data-theme', e.data.name); } }); /* Signal to parent that preview iframe is ready */ try { window.parent && window.parent.postMessage({ type: 'preview-ready' }, '*'); } catch(e) {} return; } let idx = 0; const total = slides.length; /* ===== BroadcastChannel for presenter sync ===== */ const CHANNEL_NAME = 'html-ppt-presenter-' + location.pathname; let bc; try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) { bc = null; } // Are we running inside the presenter popup? (legacy flag, now unused) const isPresenterWindow = false; /* ===== 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 (N key) ===== */ let notes = document.querySelector('.notes-overlay'); if (!notes) { notes = document.createElement('div'); notes.className = 'notes-overlay'; document.body.appendChild(notes); } /* ===== overview grid (O key) ===== */ 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'; // Force 16:9 aspect ratio robustly t.style.padding = '0 0 56.25% 0'; t.style.height = '0'; t.style.position = 'relative'; t.style.overflow = 'hidden'; const title = s.getAttribute('data-title') || (s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)); // Create a container for the mini-slide const mini = document.createElement('div'); mini.className = 'mini-slide'; mini.style.position = 'absolute'; mini.style.top = '0'; mini.style.left = '0'; mini.style.width = '1920px'; mini.style.height = '1080px'; mini.style.transformOrigin = 'top left'; mini.style.pointerEvents = 'none'; mini.style.background = 'var(--bg)'; // Clone the slide content const clone = s.cloneNode(true); clone.className = 'slide is-active'; // force active styles clone.style.position = 'absolute'; clone.style.inset = '0'; clone.style.transform = 'none'; clone.style.opacity = '1'; clone.style.padding = '72px 96px'; // ensure padding is kept mini.appendChild(clone); t.appendChild(mini); // Add the number and title overlay const overlay = document.createElement('div'); overlay.style.position = 'absolute'; overlay.style.inset = '0'; overlay.style.background = 'linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, transparent 40%, transparent 60%, rgba(0,0,0,0.8) 100%)'; overlay.style.color = '#fff'; overlay.style.zIndex = '10'; overlay.style.pointerEvents = 'none'; const n = document.createElement('div'); n.className = 'n'; n.textContent = i + 1; n.style.position = 'absolute'; n.style.top = '12px'; n.style.left = '16px'; n.style.fontWeight = '700'; n.style.fontSize = '16px'; n.style.color = '#fff'; n.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)'; const text = document.createElement('div'); text.className = 't'; text.textContent = title.trim().slice(0,80); text.style.position = 'absolute'; text.style.bottom = '12px'; text.style.left = '16px'; text.style.right = '16px'; text.style.fontWeight = '600'; text.style.fontSize = '14px'; text.style.color = '#fff'; text.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)'; overlay.appendChild(n); overlay.appendChild(text); t.appendChild(overlay); t.addEventListener('click', () => { go(i); toggleOverview(false); }); overview.appendChild(t); }); document.body.appendChild(overview); } /* ===== navigation ===== */ function go(n, fromRemote){ 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