document.addEventListener("DOMContentLoaded", () => { const setupKangarooEye = () => { const heroGraphic = document.querySelector("main > section > svg"); const eye = heroGraphic?.querySelector("#eye"); const pupil = heroGraphic?.querySelector("#pupil"); if (!heroGraphic || !eye || !pupil) { return; } const limits = { left: 4, right: 2, vertical: 4, }; let frame = null; let pointer = null; const render = () => { frame = null; if (!pointer) { pupil.setAttribute("transform", "translate(0 0)"); return; } const rect = heroGraphic.getBoundingClientRect(); const eyeRect = eye.getBoundingClientRect(); if (!rect.width || !rect.height || !eyeRect.width || !eyeRect.height) { return; } const eyeCenterX = eyeRect.left + (eyeRect.width / 2); const eyeCenterY = eyeRect.top + (eyeRect.height / 2); const normalizedX = (pointer.x - eyeCenterX) / (rect.width / 2); const normalizedY = (pointer.y - eyeCenterY) / (rect.height / 2); const clampedX = Math.max(-1, Math.min(1, normalizedX)); const clampedY = Math.max(-1, Math.min(1, normalizedY)); const moveX = clampedX < 0 ? clampedX * limits.left : clampedX * limits.right; const moveY = clampedY * limits.vertical; pupil.setAttribute("transform", `translate(${moveX.toFixed(2)} ${moveY.toFixed(2)})`); }; const requestRender = () => { if (frame !== null) { return; } frame = window.requestAnimationFrame(render); }; window.addEventListener("mousemove", (event) => { pointer = { x: event.clientX, y: event.clientY }; requestRender(); }, { passive: true }); window.addEventListener("mouseleave", () => { pointer = null; requestRender(); }); requestRender(); }; const setupToc = () => { const toc = document.querySelector("article > nav#toc"); const content = document.querySelector("article > div#content"); const list = toc?.querySelector("ol"); if (!toc || !content || !list) { return; } const links = Array.from(toc.querySelectorAll('a[href^="#"]')); const sections = Array.from(content.querySelectorAll("section[id]")); let activeId = null; if (!links.length || !sections.length) { return; } const linksById = new Map( links.map((link) => [decodeURIComponent(link.getAttribute("href").slice(1)), link]), ); const scrollActiveItemIntoView = (link, behavior = "auto") => { const item = link?.closest("li"); if (!item) { return; } const hasHorizontalOverflow = list.scrollWidth > list.clientWidth; if (!hasHorizontalOverflow) { return; } const itemLeft = item.offsetLeft; const itemCenter = itemLeft + (item.offsetWidth / 2); const maxScrollLeft = list.scrollWidth - list.clientWidth; const targetLeft = Math.max(0, Math.min(maxScrollLeft, itemCenter - (list.clientWidth / 2))); list.scrollTo({ behavior, left: targetLeft, }); }; const setActive = (id, behavior = "auto") => { const activeLink = linksById.get(id); links.forEach((link) => { link.closest("li")?.classList.toggle("active", activeLink === link); }); if (activeLink && activeId !== id) { activeId = id; scrollActiveItemIntoView(activeLink, behavior); } }; const updateActive = (behavior = "auto") => { const threshold = window.innerHeight * 0.25; let current = sections[0]; sections.forEach((section) => { if (section.getBoundingClientRect().top <= threshold) { current = section; } }); if (current?.id) { setActive(current.id, behavior); } }; updateActive(); window.addEventListener("scroll", () => updateActive("smooth"), { passive: true }); window.addEventListener("resize", updateActive); }; setupKangarooEye(); setupToc(); });