1interface ObservedAnimation {
2 element: HTMLElement | SVGElement;
3 intersecting: boolean;
4}
5
6const animations = new Map<Element, ObservedAnimation>();
7let observer: IntersectionObserver | null = null;
8let reducedMotion: MediaQueryList | null = null;
9
10function updateAnimation(animation: ObservedAnimation) {
11 const running =
12 animation.intersecting && document.visibilityState === "visible" && !reducedMotion?.matches;
13 animation.element.style.setProperty("--visible-animation-state", running ? "running" : "paused");
14 animation.element.style.setProperty(
15 "--visible-animation-will-change",
16 running ? "transform" : "auto",
17 );
18}
19
20function updateAnimations() {
21 for (const animation of animations.values()) updateAnimation(animation);
22}
23
24export function observeVisibleAnimation(element: HTMLElement | SVGElement | null) {
25 if (element === null) return;
26 element.style.setProperty("--visible-animation-state", "paused");
27 element.style.setProperty("--visible-animation-will-change", "auto");
28 if (typeof IntersectionObserver === "undefined") return;
29
30 if (observer === null) {
31 reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
32 reducedMotion.addEventListener("change", updateAnimations);
33 document.addEventListener("visibilitychange", updateAnimations);
34 observer = new IntersectionObserver((entries, source) => {
35 if (source !== observer) return;
36 for (const entry of entries) {
37 const animation = animations.get(entry.target);
38 if (!animation) continue;
39 animation.intersecting = entry.isIntersecting;
40 updateAnimation(animation);
41 }
42 });
43 }
44
45 const animation = { element, intersecting: false };
46 animations.set(element, animation);
47 observer.observe(element);
48
49 return () => {
50 if (animations.get(element) !== animation) return;
51 animations.delete(element);
52 observer?.unobserve(element);
53 element.style.setProperty("--visible-animation-state", "paused");
54 element.style.setProperty("--visible-animation-will-change", "auto");
55 if (animations.size === 0) {
56 observer?.disconnect();
57 observer = null;
58 reducedMotion?.removeEventListener("change", updateAnimations);
59 reducedMotion = null;
60 document.removeEventListener("visibilitychange", updateAnimations);
61 }
62 };
63}