import { createSplashState, advanceSplash, setSplashReduced, describeSplash, splashPosition, splashVisible, } from "./splash-motion-state.mjs"; const labels = Object.freeze(["To do", "In progress", "For review"]); const setStyle = (element, property, value) => { if (element.style[property] !== value) element.style[property] = value; }; export function initializeSplash(root, env = window) { const media = env.matchMedia("(prefers-reduced-motion: reduce)"); let state = createSplashState(env.location.search, media.matches); let visible = false; let userPaused = false; let frame = null; let previousTime = null; let geometry = null; let disposed = false; const board = root.querySelector(".kanban"); const cards = Array.from(root.querySelectorAll("[data-splash-card]")); const slots = Array.from(root.querySelectorAll("[data-splash-slot]")); const statuses = cards.map((card) => card.querySelector("[data-splash-card-status]"), ); const toggle = root.querySelector("[data-splash-toggle]"); function render() { const view = describeSplash(state); if (root.dataset.splashPhase !== view.phase) root.dataset.splashPhase = view.phase; view.cards.forEach((pose, index) => { if (geometry) { const position = splashPosition( pose.progress, geometry.anchors[pose.from], geometry.anchors[pose.to], geometry.compact, Math.sign(pose.to - pose.from), ); setStyle( cards[index], "transform", `translate3d(${position.x}px, ${position.y}px, 0)`, ); setStyle( cards[index], "zIndex", pose.progress > 0 && pose.progress < 1 ? String(pose.to > pose.from ? 3 : 2) : "1", ); } if (statuses[index] && statuses[index].textContent !== labels[pose.slot]) statuses[index].textContent = labels[pose.slot]; }); } function renderControl() { root.dataset.splashPaused = String(userPaused || state.locked); toggle.disabled = state.locked; toggle.setAttribute( "aria-label", userPaused ? "Resume task animation" : "Pause task animation", ); toggle.setAttribute("aria-pressed", String(userPaused)); } function measure() { if (disposed) return; const width = Math.min( ...slots.map((slot) => slot.getBoundingClientRect().width), ); if (!width) return; cards.forEach((card) => setStyle(card, "width", `${width}px`)); const height = `${Math.ceil(Math.max(...cards.map((card) => card.getBoundingClientRect().height)))}px`; slots.forEach((slot) => setStyle(slot, "height", height)); const boardRect = board.getBoundingClientRect(); const anchors = slots.map((slot) => { const rect = slot.getBoundingClientRect(); return { x: rect.left - boardRect.left, y: rect.top - boardRect.top }; }); geometry = { anchors, compact: Math.abs(anchors[0].x - anchors[1].x) < 1 }; root.dataset.splashCompact = String(geometry.compact); render(); } function active() { return ( !disposed && visible && !env.document.hidden && !state.locked && !userPaused ); } function tick(time) { frame = null; if (!active()) { previousTime = null; return; } state = advanceSplash( state, previousTime === null ? 0 : time - previousTime, true, ); previousTime = time; render(); frame = env.requestAnimationFrame(tick); } function syncPlayback() { if (!active()) { if (frame !== null) env.cancelAnimationFrame(frame); frame = null; previousTime = null; } else if (frame === null) frame = env.requestAnimationFrame(tick); } const observer = new env.IntersectionObserver( (entries) => { const entry = entries[entries.length - 1]; visible = entry.isIntersecting && splashVisible( entry.boundingClientRect.height, entry.intersectionRect.height, env.innerHeight, ); syncPlayback(); }, { threshold: Array.from({ length: 101 }, (_, index) => index / 100) }, ); const resize = new env.ResizeObserver(measure); function handleToggle() { if (state.locked) return; userPaused = !userPaused; renderControl(); syncPlayback(); } function handleMotionChange(event) { state = setSplashReduced(state, event.matches); previousTime = null; render(); renderControl(); syncPlayback(); } // Three real cards share an overlay; the anchored slots reserve their full geometry. board.append(...cards); root.dataset.splashInitialized = "true"; measure(); render(); renderControl(); observer.observe(root.querySelector(".splash-screen")); resize.observe(board); env.document.fonts?.ready.then(measure); toggle.addEventListener("click", handleToggle); env.document.addEventListener("visibilitychange", syncPlayback); media.addEventListener("change", handleMotionChange); return () => { disposed = true; observer.disconnect(); resize.disconnect(); if (frame !== null) env.cancelAnimationFrame(frame); toggle.removeEventListener("click", handleToggle); env.document.removeEventListener("visibilitychange", syncPlayback); media.removeEventListener("change", handleMotionChange); }; }