Replace the gear spinner with the thinking-orbs canvas engine (vendored framework-free build, driven by js/orb.js in the "solving" state) and the inline SVG brand icon with a pure CSS book whose cover flips open on page load and while the brand is hovered (js/logo.js).
73 lines
2.5 KiB
JavaScript
73 lines
2.5 KiB
JavaScript
/**
|
|
* 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);
|
|
}
|