Open History to see the last commit
1export function startHomeMotion({
2 hero,
3 field,
4 tracks,
5 caret,
6}: {
7 hero: HTMLElement;
8 field: HTMLElement;
9 tracks: HTMLElement[];
10 caret: HTMLElement;
11}) {
12 if (typeof IntersectionObserver === "undefined") return () => {};
13
14 const marks = Array.from(field.querySelectorAll<HTMLElement>(".hero-float-mark"));
15 const gated = [...marks, ...tracks, caret];
16 const visible = new Set<Element>();
17 const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
18 const finePointer = window.matchMedia("(pointer: fine)");
19 const events = new AbortController();
20 const eventOptions = { signal: events.signal };
21 let disposed = false;
22 let pointerFrame: number | undefined;
23 let pointer: { x: number; y: number } | null = null;
24
25 const canMove = (element: Element) =>
26 !disposed &&
27 visible.has(element) &&
28 document.visibilityState === "visible" &&
29 !reducedMotion.matches;
30 const canParallax = () => finePointer.matches && marks.some(canMove);
31
32 function resetPointer() {
33 if (pointerFrame !== undefined) cancelAnimationFrame(pointerFrame);
34 pointerFrame = undefined;
35 pointer = null;
36 field.style.setProperty("--px", "0px");
37 field.style.setProperty("--py", "0px");
38 }
39
40 function update() {
41 for (const element of gated) {
42 element.style.setProperty("--home-motion-state", canMove(element) ? "running" : "paused");
43 }
44 const parallax = canParallax();
45 field.style.setProperty("--parallax-duration", parallax ? "0.7s" : "0s");
46 if (!parallax) resetPointer();
47 }
48
49 const observer = new IntersectionObserver((entries) => {
50 if (disposed) return;
51 for (const entry of entries) {
52 if (entry.isIntersecting) visible.add(entry.target);
53 else visible.delete(entry.target);
54 }
55 update();
56 });
57 for (const element of gated) observer.observe(element);
58
59 hero.addEventListener(
60 "pointermove",
61 (event) => {
62 if (!canParallax()) return;
63 pointer = { x: event.clientX, y: event.clientY };
64 pointerFrame ??= requestAnimationFrame(() => {
65 pointerFrame = undefined;
66 if (!pointer || !canParallax()) return;
67 const bounds = hero.getBoundingClientRect();
68 if (bounds.width === 0 || bounds.height === 0) return;
69 field.style.setProperty(
70 "--px",
71 `${(((pointer.x - bounds.left) / bounds.width - 0.5) * 36).toFixed(1)}px`,
72 );
73 field.style.setProperty(
74 "--py",
75 `${(((pointer.y - bounds.top) / bounds.height - 0.5) * 28).toFixed(1)}px`,
76 );
77 });
78 },
79 eventOptions,
80 );
81 hero.addEventListener("pointerleave", resetPointer, eventOptions);
82 document.addEventListener("visibilitychange", update, eventOptions);
83 reducedMotion.addEventListener("change", update, eventOptions);
84 finePointer.addEventListener("change", update, eventOptions);
85 update();
86
87 return () => {
88 if (disposed) return;
89 disposed = true;
90 events.abort();
91 observer.disconnect();
92 update();
93 };
94}