/** * Book logo animation control. * * The logo is pure CSS (see `.book` in css/style.css); the cover flips while * the `book-open` class is present. Playback runs briefly when the page opens * and while the brand is hovered. Stops are deferred to the end of the * current loop (the `animationiteration` event) so the cover never snaps shut * mid-flip. */ const OPEN_PLAY_MS = 2000; const LOOP_MS = 1300; // one `book-flip` cycle (1.2s) plus slack, used if the animation clock is unavailable const OPEN_CLASS = 'book-open'; /** * @param {HTMLElement} brand Hover target wrapping the logo. * @param {HTMLElement} book The `.book` element. */ export function initBookLogo(brand, book) { if (!brand || !book) return; if (typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches) return; let stopRequested = false; let hovering = false; let openTimer = 0; let fallbackTimer = 0; const stopNow = () => { window.clearTimeout(fallbackTimer); fallbackTimer = 0; stopRequested = false; book.classList.remove(OPEN_CLASS); }; const play = () => { window.clearTimeout(fallbackTimer); fallbackTimer = 0; stopRequested = false; book.classList.add(OPEN_CLASS); }; const msUntilLoopEnd = () => { const cover = book.querySelector('.book-cover'); const animation = cover && typeof cover.getAnimations === 'function' ? cover.getAnimations()[0] : null; const timing = animation && animation.effect && typeof animation.effect.getTiming === 'function' ? animation.effect.getTiming() : null; const duration = timing && typeof timing.duration === 'number' ? timing.duration : 0; if (!animation || !duration || typeof animation.currentTime !== 'number') return LOOP_MS; return duration - (animation.currentTime % duration) + 50; }; const stopAtLoopEnd = () => { stopRequested = true; // Iteration events only arrive while the tab is rendering frames; in a // hidden or throttled tab, fall back to a timer aligned to the loop end. window.clearTimeout(fallbackTimer); fallbackTimer = window.setTimeout(stopNow, msUntilLoopEnd()); }; book.addEventListener('animationiteration', () => { if (stopRequested && !hovering) stopNow(); }); brand.addEventListener('mouseenter', () => { hovering = true; window.clearTimeout(openTimer); play(); }); brand.addEventListener('mouseleave', () => { hovering = false; stopAtLoopEnd(); }); play(); openTimer = window.setTimeout(stopAtLoopEnd, OPEN_PLAY_MS); }