178 lines
6.8 KiB
JavaScript
178 lines
6.8 KiB
JavaScript
/* html-ppt :: runtime.js
|
|
* Keyboard-driven deck runtime. Zero dependencies.
|
|
*
|
|
* Features:
|
|
* ← → / space / PgUp PgDn / Home End navigation
|
|
* F fullscreen
|
|
* S speaker notes overlay
|
|
* O slide overview grid
|
|
* T cycle themes (reads data-themes on <html> or <body>)
|
|
* 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 = '<span></span>';
|
|
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 = '<div class="n">'+(i+1)+'</div><div class="t">'+title.trim().slice(0,80)+'</div>';
|
|
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<n);
|
|
});
|
|
idx = n;
|
|
barFill.style.width = ((n+1)/total*100)+'%';
|
|
const numEl = document.querySelector('.slide-number');
|
|
if (numEl) { numEl.setAttribute('data-current', n+1); numEl.setAttribute('data-total', total); }
|
|
// notes
|
|
const note = slides[n].querySelector('.notes');
|
|
notes.innerHTML = note ? note.innerHTML : '';
|
|
// hash
|
|
if (location.hash !== '#/'+(n+1)) history.replaceState(null,'','#/'+(n+1));
|
|
// re-trigger entry animations
|
|
slides[n].querySelectorAll('[data-anim]').forEach(el => {
|
|
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')); }
|
|
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': toggleNotes(); 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); 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);
|
|
});
|
|
})();
|