149 lines
4.6 KiB
JavaScript
149 lines
4.6 KiB
JavaScript
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();
|
|
});
|