From 43293f228448485331a69b552956b63d9401c307 Mon Sep 17 00:00:00 2001 From: ssbg04 Date: Tue, 8 Sep 2026 09:39:15 +0800 Subject: [PATCH] feat(ui): redesign cursor to single fluid ring and default magnifier off --- src/components/ControlsDropdown.tsx | 6 +- src/components/SpotlightLens.tsx | 169 ++++++++++++++++++++-------- 2 files changed, 123 insertions(+), 52 deletions(-) diff --git a/src/components/ControlsDropdown.tsx b/src/components/ControlsDropdown.tsx index ed3fe3a..6277d6f 100644 --- a/src/components/ControlsDropdown.tsx +++ b/src/components/ControlsDropdown.tsx @@ -5,7 +5,7 @@ import { useIsMobile } from '../lib/hooks'; export default function ControlsDropdown() { const [isOpen, setIsOpen] = useState(false); const [theme, setTheme] = useState<'light' | 'dark'>('light'); - const [magnifierEnabled, setMagnifierEnabled] = useState(true); + const [magnifierEnabled, setMagnifierEnabled] = useState(false); const [zoom, setZoom] = useState(100); const [isLiteMode, setIsLiteMode] = useState(false); const [isTouchDevice, setIsTouchDevice] = useState(false); @@ -28,9 +28,7 @@ export default function ControlsDropdown() { window.addEventListener('tier-change', checkLite); const savedMag = localStorage.getItem('magnifierEnabled'); - if (savedMag !== null) { - setMagnifierEnabled(savedMag === 'true'); - } + setMagnifierEnabled(savedMag === 'true'); const savedZoom = localStorage.getItem('pageZoom'); if (savedZoom) { diff --git a/src/components/SpotlightLens.tsx b/src/components/SpotlightLens.tsx index 7c63af9..61a8563 100644 --- a/src/components/SpotlightLens.tsx +++ b/src/components/SpotlightLens.tsx @@ -1,15 +1,25 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; export default function SpotlightLens() { const [active, setActive] = useState(false); - const [pos, setPos] = useState({ clientX: -100, clientY: -100 }); - const [scroll, setScroll] = useState({ x: 0, y: 0 }); const [cloneHtml, setCloneHtml] = useState(''); const [bodySize, setBodySize] = useState({ width: 0, height: 0 }); - const [isHovered, setIsHovered] = useState(false); + const [scroll, setScroll] = useState({ x: 0, y: 0 }); + const [magnifierPos, setMagnifierPos] = useState({ clientX: -100, clientY: -100 }); const [isDesktop, setIsDesktop] = useState(false); - const [magnifierEnabled, setMagnifierEnabled] = useState(true); + const [magnifierEnabled, setMagnifierEnabled] = useState(false); + + // Direct DOM ref for 120 FPS cursor (0 React re-renders on mouse movement) + const ringRef = useRef(null); + + // High-frequency coordinates in refs to bypass React render cycle + const mousePos = useRef({ x: -100, y: -100 }); + const ringPos = useRef({ x: -100, y: -100 }); + const isHoveredRef = useRef(false); + const isMouseDownRef = useRef(false); + const isVisibleRef = useRef(false); + const rafId = useRef(null); useEffect(() => { const checkDesktop = () => { @@ -22,9 +32,7 @@ export default function SpotlightLens() { window.addEventListener('tier-change', checkDesktop); const savedMag = localStorage.getItem('magnifierEnabled'); - if (savedMag !== null) { - setMagnifierEnabled(savedMag === 'true'); - } + setMagnifierEnabled(savedMag === 'true'); const onMagPref = (e: any) => { if (e.detail && typeof e.detail.enabled === 'boolean') { @@ -34,7 +42,7 @@ export default function SpotlightLens() { }; const onToggleMode = () => { - setActive(prev => { + setActive((prev) => { if (!prev) { const contentDiv = document.getElementById('magnify-content'); if (contentDiv) { @@ -55,6 +63,7 @@ export default function SpotlightLens() { return () => { window.removeEventListener('resize', checkDesktop); + window.removeEventListener('tier-change', checkDesktop); window.removeEventListener('magnifier-preference-changed', onMagPref); window.removeEventListener('toggle-magnifier-mode', onToggleMode); }; @@ -63,6 +72,83 @@ export default function SpotlightLens() { useEffect(() => { if (!isDesktop) return; + // 120 FPS RAF Animation Loop (Hardware Accelerated via translate3d) + const renderLoop = () => { + // Smooth spring follow with Linear Interpolation (Lerp) + const ease = 0.18; + ringPos.current.x += (mousePos.current.x - ringPos.current.x) * ease; + ringPos.current.y += (mousePos.current.y - ringPos.current.y) * ease; + + const rx = Number(ringPos.current.x.toFixed(2)); + const ry = Number(ringPos.current.y.toFixed(2)); + const mx = Number(mousePos.current.x.toFixed(2)); + const my = Number(mousePos.current.y.toFixed(2)); + + if (ringRef.current) { + let scale = 1; + if (isMouseDownRef.current) { + scale = 0.82; + } else if (isHoveredRef.current) { + scale = 1.65; + } + + ringRef.current.style.transform = `translate3d(${rx}px, ${ry}px, 0) translate(-50%, -50%) scale(${scale})`; + ringRef.current.style.opacity = isVisibleRef.current ? '1' : '0'; + } + + rafId.current = requestAnimationFrame(renderLoop); + }; + + rafId.current = requestAnimationFrame(renderLoop); + + const handlePointerMove = (e: PointerEvent) => { + mousePos.current.x = e.clientX; + mousePos.current.y = e.clientY; + isVisibleRef.current = true; + + // If magnifier active, update its position + if (active) { + setMagnifierPos({ clientX: e.clientX, clientY: e.clientY }); + } + + // Fast check for interactive targets + const target = e.target as HTMLElement | null; + if (target) { + const isInteractive = Boolean( + target.closest('a, button, [role="button"], input, textarea, select, .bento-card, .cursor-pointer') + ); + isHoveredRef.current = isInteractive; + + if (ringRef.current) { + if (isInteractive) { + ringRef.current.style.borderColor = 'var(--primary-custom)'; + ringRef.current.style.backgroundColor = 'rgba(56, 189, 248, 0.12)'; + ringRef.current.style.boxShadow = '0 0 20px rgba(56, 189, 248, 0.25)'; + } else { + ringRef.current.style.borderColor = ''; + ringRef.current.style.backgroundColor = ''; + ringRef.current.style.boxShadow = ''; + } + } + } + }; + + const handlePointerDown = () => { + isMouseDownRef.current = true; + }; + + const handlePointerUp = () => { + isMouseDownRef.current = false; + }; + + const handleMouseLeave = () => { + isVisibleRef.current = false; + }; + + const handleMouseEnter = () => { + isVisibleRef.current = true; + }; + const handleKeyDown = (e: KeyboardEvent) => { if ( e.target instanceof HTMLInputElement || @@ -79,6 +165,7 @@ export default function SpotlightLens() { height: document.documentElement.scrollHeight, }); setScroll({ x: window.scrollX, y: window.scrollY }); + setMagnifierPos({ clientX: mousePos.current.x, clientY: mousePos.current.y }); setActive(true); } } @@ -90,44 +177,42 @@ export default function SpotlightLens() { } }; - const handleMouseMove = (e: MouseEvent) => { - setPos({ clientX: e.clientX, clientY: e.clientY }); - - const target = e.target as HTMLElement | null; - if (target) { - const isInteractive = Boolean( - target.closest('a, button, [role="button"], input, textarea, select, .bento-card, .cursor-pointer') - ); - setIsHovered(isInteractive); - } - }; - const handleScroll = () => { if (active) setScroll({ x: window.scrollX, y: window.scrollY }); }; + window.addEventListener('pointermove', handlePointerMove, { passive: true }); + window.addEventListener('pointerdown', handlePointerDown, { passive: true }); + window.addEventListener('pointerup', handlePointerUp, { passive: true }); + document.addEventListener('mouseleave', handleMouseLeave); + document.addEventListener('mouseenter', handleMouseEnter); window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); - window.addEventListener('mousemove', handleMouseMove, { passive: true }); window.addEventListener('scroll', handleScroll, { passive: true }); return () => { + if (rafId.current) cancelAnimationFrame(rafId.current); + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerdown', handlePointerDown); + window.removeEventListener('pointerup', handlePointerUp); + document.removeEventListener('mouseleave', handleMouseLeave); + document.removeEventListener('mouseenter', handleMouseEnter); window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); - window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('scroll', handleScroll); }; }, [active, isDesktop, magnifierEnabled]); const LENS_SIZE = 220; const SCALE = 1.45; - const pageX = pos.clientX + scroll.x; - const pageY = pos.clientY + scroll.y; + const pageX = magnifierPos.clientX + scroll.x; + const pageY = magnifierPos.clientY + scroll.y; if (!isDesktop) return null; return ( <> + {/* ─── Magnifier Mode (Hold 'R' / Toggle) ─── */} {active && (
-
+
)} + {/* ─── Redesigned Single Fluid Cursor Ring (No Center Dot) ─── */} {!active && ( -