UNPKG

react-dvd-screensaver

Version:

DVD-era nostalgia in React. Zero dependencies, fully typed, SSR-safe, supports React 16 through 19.

177 lines (174 loc) 7.94 kB
import { useRef, useState, useEffect, useLayoutEffect } from 'react'; // src/useDvdScreensaver.ts var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect; function useDvdScreensaver(options) { const optionsRef = useRef(options); optionsRef.current = options; const animationState = useRef({ animationFrameId: 0, impactCount: 0, isPosXIncrement: true, isPosYIncrement: true, lastTimestamp: 0, positionX: 0, positionY: 0 }); const animateFnRef = useRef(void 0); const elementRef = useRef(null); const containerRef = useRef(null); const [impactCount, setImpactCount] = useState(0); const [hovered, setHovered] = useState(false); const hoveredRef = useRef(false); const prefersReducedMotion = () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; function startAnimation() { if (!animateFnRef.current || animationState.current.animationFrameId) return; if (prefersReducedMotion()) return; animationState.current.animationFrameId = requestAnimationFrame(animateFnRef.current); } function stopAnimation() { cancelAnimationFrame(animationState.current.animationFrameId); animationState.current.animationFrameId = 0; animationState.current.lastTimestamp = 0; } useIsomorphicLayoutEffect(() => { var _a, _b; const element = elementRef.current; const mql = typeof window !== "undefined" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null; const setActive = () => setHovered(true); const setInactive = () => setHovered(false); const handleTouchEnd = (e) => { if (e.touches.length === 0) setInactive(); }; const onMotionPrefChange = () => { var _a2; if (mql == null ? void 0 : mql.matches) { stopAnimation(); } else if (!((_a2 = optionsRef.current) == null ? void 0 : _a2.paused) && !hoveredRef.current) { startAnimation(); } }; function updatePosition(containerSpan, delta, elementSpan, prevPos, toggleKey) { var _a2, _b2; const boundary = Math.max(0, containerSpan - elementSpan); let newPos = prevPos + (animationState.current[toggleKey] ? delta : -delta); let hit = false; if (newPos <= 0 || newPos >= boundary) { animationState.current[toggleKey] = !animationState.current[toggleKey]; animationState.current.impactCount += 1; setImpactCount(animationState.current.impactCount); (_b2 = (_a2 = optionsRef.current) == null ? void 0 : _a2.impactCallback) == null ? void 0 : _b2.call(_a2, animationState.current.impactCount); newPos = Math.max(0, Math.min(newPos, boundary)); hit = true; } return { pos: newPos, hit }; } function animate(timestamp) { var _a2, _b2, _c, _d, _e; const el = elementRef.current; const container = (_a2 = containerRef.current) != null ? _a2 : el == null ? void 0 : el.parentElement; if (el && container) { const last = animationState.current.lastTimestamp; const elapsed = last ? Math.min(timestamp - last, 50) : 1e3 / 60; animationState.current.lastTimestamp = timestamp; const speed = (_c = (_b2 = optionsRef.current) == null ? void 0 : _b2.speed) != null ? _c : 2; const delta = speed * 60 * elapsed / 1e3; const { pos: posX, hit: hitX } = updatePosition( container.clientWidth, delta, el.clientWidth, animationState.current.positionX, "isPosXIncrement" ); const { pos: posY, hit: hitY } = updatePosition( container.clientHeight, delta, el.clientHeight, animationState.current.positionY, "isPosYIncrement" ); if (hitX && hitY) { (_e = (_d = optionsRef.current) == null ? void 0 : _d.onCornerHit) == null ? void 0 : _e.call(_d); } el.style.transform = `translate3d(${posX}px, ${posY}px, 0)`; animationState.current.positionX = posX; animationState.current.positionY = posY; } animationState.current.animationFrameId = requestAnimationFrame(animate); } animateFnRef.current = animate; if (element) { element.style.willChange = "transform"; element.style.userSelect = "none"; element.style.setProperty("-webkit-touch-callout", "none"); const container = (_a = containerRef.current) != null ? _a : element.parentElement; if (container) { const maxX = Math.max(0, container.clientWidth - element.clientWidth); const maxY = Math.max(0, container.clientHeight - element.clientHeight); animationState.current.positionX = Math.random() * maxX; animationState.current.positionY = Math.random() * maxY; } element.addEventListener("mouseover", setActive); element.addEventListener("mouseout", setInactive); element.addEventListener("touchstart", setActive, { passive: true }); element.addEventListener("touchend", handleTouchEnd, { passive: true }); element.addEventListener("touchcancel", handleTouchEnd, { passive: true }); mql == null ? void 0 : mql.addEventListener("change", onMotionPrefChange); if (!((_b = optionsRef.current) == null ? void 0 : _b.paused) && !(mql == null ? void 0 : mql.matches)) { animationState.current.animationFrameId = requestAnimationFrame(animate); } } return () => { element == null ? void 0 : element.removeEventListener("mouseover", setActive); element == null ? void 0 : element.removeEventListener("mouseout", setInactive); element == null ? void 0 : element.removeEventListener("touchstart", setActive); element == null ? void 0 : element.removeEventListener("touchend", handleTouchEnd); element == null ? void 0 : element.removeEventListener("touchcancel", handleTouchEnd); mql == null ? void 0 : mql.removeEventListener("change", onMotionPrefChange); cancelAnimationFrame(animationState.current.animationFrameId); }; }, []); useEffect(() => { var _a; const element = elementRef.current; const container = (_a = containerRef.current) != null ? _a : element == null ? void 0 : element.parentElement; if (!container || typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver(() => { if (!elementRef.current) return; const maxX = Math.max(0, container.clientWidth - elementRef.current.clientWidth); const maxY = Math.max(0, container.clientHeight - elementRef.current.clientHeight); animationState.current.positionX = Math.min(animationState.current.positionX, maxX); animationState.current.positionY = Math.min(animationState.current.positionY, maxY); }); observer.observe(container); return () => observer.disconnect(); }, []); useEffect(() => { var _a, _b, _c; const wasHovered = hoveredRef.current; hoveredRef.current = hovered; if (hovered && !wasHovered) { (_b = (_a = optionsRef.current) == null ? void 0 : _a.hoverCallback) == null ? void 0 : _b.call(_a); } if (!((_c = optionsRef.current) == null ? void 0 : _c.freezeOnHover)) return; if (hovered) { stopAnimation(); } else { startAnimation(); } }, [hovered]); useEffect(() => { var _a; if (options == null ? void 0 : options.paused) { stopAnimation(); } else if (!hoveredRef.current || !((_a = optionsRef.current) == null ? void 0 : _a.freezeOnHover)) { startAnimation(); } }, [options == null ? void 0 : options.paused]); useEffect(() => { if (!(options == null ? void 0 : options.freezeOnHover) && !(options == null ? void 0 : options.paused)) { startAnimation(); } }, [options == null ? void 0 : options.freezeOnHover]); return { containerRef, elementRef, hovered, impactCount }; } export { useDvdScreensaver };