import useFrame from '@/utils/useFrame'; import { Fragment, useEffect, useRef } from 'react'; import styles from './cursor.module.css'; import { smooth } from '@/utils/smooth'; type CursorProps = { children: React.ReactNode; } export function Cursor(props: CursorProps) { const { children } = props; const mousePosition = useRef<{ x: number; y: number; cursorPadding: number; cursorBorderRadius: number; targetRect: DOMRect | null; target: { link: boolean; button: boolean; cursorEffector: boolean }; pressed: boolean; active: boolean; }>({ x: 0, y: 0, cursorPadding: 0, cursorBorderRadius: 0, targetRect: null, target: { link: false, button: false, cursorEffector: false }, pressed: false, active: false, }); const cursorRef = useRef(null); const cursorSpotRef = useRef(null); const prevCursorState = useRef<{ x: number; y: number; width: number; height: number; borderRadius: number; }>({ x: 0, y: 0, width: 0, height: 0, borderRadius: 0, }); useEffect(() => { let isReadyForHide = false; let isTouch = false; const updateTargetFromPath = (path: any[]) => { const isLink = path.find((el) => el?.tagName === 'A'); const isButton = path.find((el) => el?.tagName === 'BUTTON'); const isCursorEffector = path.find((el) => el?.getAttribute?.('data-cursor-effect') ); const cursorPadding = +( path .find((el) => el?.getAttribute?.('data-cursor-padding') !== null) ?.getAttribute?.('data-cursor-padding') || 16 ); const cursorBorderRadius = +( path .find( (el) => el?.getAttribute?.('data-cursor-border-radius') !== null ) ?.getAttribute?.('data-cursor-border-radius') || 0 ); let targetRect = null; if (isLink) { targetRect = isLink.getBoundingClientRect(); } if (isButton) { targetRect = isButton.getBoundingClientRect(); } if (isCursorEffector) { targetRect = isCursorEffector.getBoundingClientRect(); } mousePosition.current = { ...mousePosition.current, cursorPadding, cursorBorderRadius, targetRect, target: { link: Boolean(isLink), button: Boolean(isButton), cursorEffector: Boolean(isCursorEffector), }, }; }; const handleMouseMove = (event: MouseEvent) => { if (isTouch) return; updateTargetFromPath(event.composedPath()); if (!mousePosition.current.active) { prevCursorState.current = { ...prevCursorState.current, x: event.clientX, y: event.clientY, }; } isReadyForHide = false; mousePosition.current = { ...mousePosition.current, x: event.clientX, y: event.clientY, active: true, }; }; const handleMouseLeave = () => { isReadyForHide = true; setTimeout(() => { if (!isReadyForHide) return; console.log('mouse leave'); mousePosition.current = { ...mousePosition.current, active: false, }; }, 300); }; const handleScroll = () => { if (isTouch) return; updateTargetFromPath( document.elementsFromPoint( mousePosition.current.x, mousePosition.current.y ) ); }; const handleMouseDown = (event: MouseEvent) => { if (isTouch) return; updateTargetFromPath(event.composedPath()); mousePosition.current = { ...mousePosition.current, pressed: true, }; }; const handleMouseUp = (event: MouseEvent) => { if (isTouch) return; updateTargetFromPath(event.composedPath()); mousePosition.current = { ...mousePosition.current, pressed: false, }; }; const handlePointerDown = (event: PointerEvent) => { isTouch = event.pointerType === 'touch'; if (isTouch) handleMouseLeave(); }; window.addEventListener('pointerdown', handlePointerDown); window.addEventListener('mousemove', handleMouseMove); window.document.body.addEventListener('mouseleave', handleMouseLeave); window.addEventListener('scroll', handleScroll); window.addEventListener('mousedown', handleMouseDown); window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('pointerdown', handlePointerDown); window.removeEventListener('mousemove', handleMouseMove); window.document.body.removeEventListener('mouseleave', handleMouseLeave); window.removeEventListener('scroll', handleScroll); window.removeEventListener('click', handleMouseDown); window.removeEventListener('mouseup', handleMouseUp); }; }, []); useFrame((delta) => { let x = mousePosition.current.x; let y = mousePosition.current.y; let width = 30; let height = 30; let borderRadius = 0; let gapHover = mousePosition.current.cursorPadding; const targetRect = mousePosition.current.targetRect; const isHovered = (mousePosition.current.target.link || mousePosition.current.target.button || mousePosition.current.target.cursorEffector) && targetRect !== null; if (!mousePosition.current.active) { width = 0; height = 0; borderRadius = 15; } else if (isHovered) { const targetCenterX = targetRect.left + targetRect.width / 2; const targetCenterY = targetRect.top + targetRect.height / 2; width = targetRect.width + gapHover * 2; height = targetRect.height + gapHover * 2; x = x * 0.05 + targetCenterX * 0.95 - width / 2; y = y * 0.05 + targetCenterY * 0.95 - height / 2; } else { width = 30; height = 30; x = x - width / 2; y = y - height / 2; borderRadius = 15; } if (mousePosition.current.pressed) { width -= 10; height -= 10; x += 5; y += 5; gapHover -= 5; } if (mousePosition.current.cursorBorderRadius) { borderRadius = mousePosition.current.cursorBorderRadius + gapHover; } else { borderRadius = Math.min(width / 2, height / 2); } x = smooth(prevCursorState.current.x, x, 0.02 * delta); y = smooth(prevCursorState.current.y, y, 0.02 * delta); width = smooth(prevCursorState.current.width, width, 0.02 * delta); height = smooth(prevCursorState.current.height, height, 0.02 * delta); borderRadius = smooth( prevCursorState.current.borderRadius, borderRadius, 0.02 * delta ); prevCursorState.current = { ...prevCursorState.current, x, y, width, height, borderRadius, }; const cursorNode = cursorRef.current; const cursorSpotNode = cursorSpotRef.current; if (cursorNode !== null && cursorSpotNode !== null) { cursorNode.style.transform = `translate3D(${x}px, ${y}px, 0)`; cursorNode.style.width = `${width}px`; cursorNode.style.height = `${height}px`; cursorNode.style.borderRadius = `${Math.min(borderRadius, 36 + gapHover)}px`; const size = Math.max((width + height) / 2, 48); cursorSpotNode.style.transform = `translate(${mousePosition.current.x - x - size / 2}px, ${mousePosition.current.y - y - size / 2}px)`; cursorSpotNode.style.width = `${size}px`; cursorSpotNode.style.height = `${size}px`; if (isHovered) { if (!cursorNode.classList.contains(styles.hovered)) { cursorNode.classList.add(styles.hovered); cursorNode.classList.remove(styles.idle); } } else { if (!cursorNode.classList.contains(styles.idle)) { cursorNode.classList.add(styles.idle); cursorNode.classList.remove(styles.hovered); } } } }); return (
{children} ); }