import gsap from 'gsap' import { ref, onMounted, onBeforeUnmount } from 'vue' import { useWindowSize, useEventListener } from '@vueuse/core' /** * Shared global raw mouse state (only set up once) */ const rawMouse = { x: ref(0), y: ref(0), initialized: false, cleanup: null, } const useGlobalMouseListener = () => { if (!rawMouse.initialized && !import.meta.env.SSR) { rawMouse.initialized = true rawMouse.cleanup = useEventListener(window, 'mousemove', (e) => { rawMouse.x.value = e.clientX rawMouse.y.value = e.clientY }) } } /** * Composable for smoothed mouse position with customizable smoothing and normalization. */ export default (options) => { const smoothFactor = options?.smoothFactor ?? 0.1 const normalize = options?.normalize ?? false const callback = options?.onUpdate const { width: wWidth, height: wHeight } = useWindowSize() const sx = ref(0) const sy = ref(0) useGlobalMouseListener() const getTargetX = () => normalize ? rawMouse.x.value / wWidth.value : rawMouse.x.value const getTargetY = () => normalize ? rawMouse.y.value / wHeight.value : rawMouse.y.value let tween onMounted(() => { if (tween) tween.kill // Start smoothing tween tween = gsap.to( { x: sx.value, y: sy.value }, { duration: 1, ease: 'linear', repeat: -1, onUpdate: () => { const newX = gsap.utils.interpolate( sx.value, getTargetX(), smoothFactor, ) const newY = gsap.utils.interpolate( sy.value, getTargetY(), smoothFactor, ) sx.value = newX sy.value = newY callback?.({ sx: sx.value, sy: sy.value }) }, }, ) }) onBeforeUnmount(() => { tween?.kill() rawMouse.cleanup() rawMouse.initialized = false }) return { sx, sy, rawX: rawMouse.x, rawY: rawMouse.y, } }