diff --git a/src/components/effects/GradientBlinds.test.tsx b/src/components/effects/GradientBlinds.test.tsx index fdb4c45..2d3eb5b 100644 --- a/src/components/effects/GradientBlinds.test.tsx +++ b/src/components/effects/GradientBlinds.test.tsx @@ -176,17 +176,11 @@ describe('GradientBlinds', () => { unmount(); expect(removeEventListenerSpy).toHaveBeenCalledWith('pointermove', expect.any(Function)); }); + it('minimizes getBoundingClientRect calls during pointer move', () => { const { unmount } = render(); - // Spy on getBoundingClientRect - // Note: In jsdom, canvas is an HTMLCanvasElement which inherits from HTMLElement const spy = vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect'); - - // Trigger pointer move to clear any initial calls or verify baseline - // The initial render calls resize(), which calls getBoundingClientRect on container - - // Clear spy history from initial render spy.mockClear(); act(() => { @@ -198,9 +192,37 @@ describe('GradientBlinds', () => { window.dispatchEvent(event); }); - // EXPECTATION: It should NOT be called because the listener shouldn't be attached (not visible) expect(spy).not.toHaveBeenCalled(); unmount(); }); + + it('avoids expensive DOM reads (scrollX/Y) in pointermove handler when visible', () => { + const { unmount } = render(); + + // Spy on scrollX/scrollY getters + // Note: In jsdom, these are properties on window. + const scrollSpy = vi.spyOn(window, 'scrollX', 'get'); + + // Make visible to attach listener + act(() => { + if (ioCallback) { + ioCallback([{ isIntersecting: true } as IntersectionObserverEntry]); + } + }); + + scrollSpy.mockClear(); + + // Trigger pointer move + act(() => { + window.dispatchEvent(new PointerEvent('pointermove', { clientX: 100, clientY: 100 })); + }); + + // With the optimization (moving to RAF loop), this should be 0. + // Without optimization, this will be > 0. + // Since we are mocking RAF and not running the loop, if it's in the loop, it won't be called. + expect(scrollSpy).not.toHaveBeenCalled(); + + unmount(); + }); }); diff --git a/src/components/effects/GradientBlinds.tsx b/src/components/effects/GradientBlinds.tsx index 48c0a71..f53da04 100644 --- a/src/components/effects/GradientBlinds.tsx +++ b/src/components/effects/GradientBlinds.tsx @@ -64,6 +64,8 @@ const GradientBlinds: React.FC = ({ const geometryRef = useRef(null); const rendererRef = useRef(null); const mouseTargetRef = useRef<[number, number]>([0, 0]); + // Optimization: store raw pointer position (viewport coords) to decouple event handling from calculation + const pointerPosRef = useRef<{ x: number; y: number } | null>(null); const lastTimeRef = useRef(0); const firstResizeRef = useRef(true); const rectRef = useRef(null); @@ -309,31 +311,34 @@ void main() { ro.observe(container); const onPointerMove = (e: PointerEvent) => { - const scale = (renderer as unknown as { dpr?: number }).dpr || 1; - let x, y; - - if (rectRef.current) { - const dx = window.scrollX - scrollPosRef.current.x; - const dy = window.scrollY - scrollPosRef.current.y; - const rectLeft = rectRef.current.left - dx; - const rectTop = rectRef.current.top - dy; - x = (e.clientX - rectLeft) * scale; - y = (rectRef.current.height - (e.clientY - rectTop)) * scale; - } else { - const rect = canvas.getBoundingClientRect(); - x = (e.clientX - rect.left) * scale; - y = (rect.height - (e.clientY - rect.top)) * scale; - } - - mouseTargetRef.current = [x, y]; - if (mouseDampening <= 0) { - uniforms.iMouse.value = [x, y]; - } + pointerPosRef.current = { x: e.clientX, y: e.clientY }; }; const loop = (t: number) => { rafRef.current = requestAnimationFrame(loop); uniforms.iTime.value = t * 0.001; + + // Update target based on pointer position and scroll offset + if (pointerPosRef.current) { + const scale = (renderer as unknown as { dpr?: number }).dpr || 1; + let x, y; + + if (rectRef.current) { + const dx = window.scrollX - scrollPosRef.current.x; + const dy = window.scrollY - scrollPosRef.current.y; + const rectLeft = rectRef.current.left - dx; + const rectTop = rectRef.current.top - dy; + x = (pointerPosRef.current.x - rectLeft) * scale; + y = (rectRef.current.height - (pointerPosRef.current.y - rectTop)) * scale; + } else { + // Fallback if rectRef missing + const rect = canvas.getBoundingClientRect(); + x = (pointerPosRef.current.x - rect.left) * scale; + y = (rect.height - (pointerPosRef.current.y - rect.top)) * scale; + } + mouseTargetRef.current = [x, y]; + } + if (mouseDampening > 0) { if (!lastTimeRef.current) lastTimeRef.current = t; const dt = (t - lastTimeRef.current) / 1000; @@ -346,6 +351,9 @@ void main() { cur[0] += (target[0] - cur[0]) * factor; cur[1] += (target[1] - cur[1]) * factor; } else { + if (pointerPosRef.current) { + uniforms.iMouse.value = mouseTargetRef.current; + } lastTimeRef.current = t; } if (!paused && programRef.current && meshRef.current) {