(() => { const hero = document.querySelector("[data-circuit-hero]"); if (!hero) return; const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const sequence = [ { name: "projects", travel: 620, character: 62 }, { name: "articles", travel: 600, character: 58 }, { name: "blog", travel: 560, character: 72 }, { name: "github", travel: 380, character: 46 }, { name: "gitea", travel: 360, character: 48 }, { name: "linkedin", travel: 360, character: 42 }, { name: "twitch", travel: 340, character: 46 }, { name: "youtube", travel: 360, character: 42 }, { name: "kofi", travel: 320, character: 50 }, ]; let runToken = 0; const sleep = (milliseconds) => new Promise((resolve) => window.setTimeout(resolve, milliseconds)); const reset = () => { hero.classList.remove("is-glitching"); hero.querySelectorAll(".circuit-node").forEach((node) => { node.classList.remove("is-revealing", "is-ready"); node.querySelector("[data-route-output]").textContent = ""; const cursor = node.querySelector("i"); if (cursor) cursor.hidden = false; }); hero.querySelectorAll(".circuit-live path").forEach((path) => { path.style.opacity = "0"; path.style.strokeDashoffset = "0"; }); }; const revealAll = () => { hero.querySelectorAll(".circuit-node").forEach((node) => { node.querySelector("[data-route-output]").textContent = node.dataset.routeText; const cursor = node.querySelector("i"); if (cursor) cursor.hidden = true; node.classList.add("is-revealing", "is-ready"); }); hero.querySelectorAll(".circuit-live path").forEach((path) => { path.style.opacity = ".34"; }); }; const animatePath = (path, duration, token) => { const length = path.getTotalLength(); path.style.strokeDasharray = `30 ${Math.max(40, length)}`; path.style.strokeDashoffset = "30"; path.style.opacity = "1"; return new Promise((resolve) => { const started = performance.now(); const frame = (now) => { if (token !== runToken) { resolve(); return; } const progress = Math.min(1, (now - started) / duration); const eased = 1 - Math.pow(1 - progress, 3); path.style.strokeDashoffset = String(30 - (length + 55) * eased); if (progress < 1) { requestAnimationFrame(frame); } else { path.style.opacity = ".34"; resolve(); } }; requestAnimationFrame(frame); }); }; const typeRoute = async (node, characterDelay, token) => { const output = node.querySelector("[data-route-output]"); node.classList.add("is-revealing"); for (const character of node.dataset.routeText) { if (token !== runToken) return; output.textContent += character; await sleep(characterDelay); } const cursor = node.querySelector("i"); if (cursor) cursor.hidden = true; node.classList.add("is-ready"); }; const animateAmbientPackets = () => { if (reducedMotion) return; hero.querySelectorAll(".circuit-ambient-packets path").forEach((packet, index) => { const length = packet.getTotalLength(); packet.style.strokeDasharray = `16 ${Math.max(30, length)}`; packet.animate( [ { strokeDashoffset: "20", opacity: .06 }, { opacity: .2, offset: .18 }, { opacity: .2, offset: .82 }, { strokeDashoffset: String(-(length + 25)), opacity: .05 }, ], { duration: 3300 + (index % 5) * 620, delay: index * 210, iterations: Infinity, easing: "linear" }, ); }); }; const runSequence = async () => { runToken += 1; const token = runToken; if (reducedMotion) { reset(); revealAll(); return; } while (token === runToken) { reset(); await sleep(280); for (const item of sequence) { if (token !== runToken) return; const node = hero.querySelector(`[data-route-node="${item.name}"]`); const path = hero.querySelector(`#route-${item.name}`); await animatePath(path, item.travel, token); await typeRoute(node, item.character, token); await sleep(55); } await sleep(3000); if (token !== runToken) return; hero.classList.add("is-glitching"); await sleep(430); if (token !== runToken) return; reset(); await sleep(180); } }; hero.querySelector("[data-circuit-replay]")?.addEventListener("click", runSequence); animateAmbientPackets(); runSequence(); })();