From 45be2196e380eeb483efe29328336ee29321be60 Mon Sep 17 00:00:00 2001 From: Malay Vasa Date: Wed, 9 Oct 2024 14:52:44 +0530 Subject: [PATCH] feat(dataroom): DotGrid animation (#442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Open branch * feat: 🎸 Add Industry Comparison Section * fix: 🐛 address PR comments * refactor: 💡 address PR comments * docs: ✏️ add descripition for useMousePosition hook * refactor: 💡 remove container ref from useMousePosition * refactor: 💡 add SlidingPanel component and update Cryptopunks * refactor: 💡 dynamic import for framer motion * refactor: 💡 address PR comments * refactor: 💡 conditionally call drawDots to improve performance * style: 💄 remove unused title style * refactor: 💡 remove unused prop dimensions from drawDots --------- Co-authored-by: iamacook Co-authored-by: Diogo Soares <32431609+DiogoSoaress@users.noreply.github.com> --- .../DataRoom/IndustryComparison/Content.tsx | 2 +- .../DataRoom/IndustryComparison/DotGrid.tsx | 29 ++++++++--- .../IndustryComparison/utils/drawDots.ts | 50 +++++++++++++++++-- .../utils/useMousePosition.ts | 43 ++++++++++++++++ 4 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 src/components/DataRoom/IndustryComparison/utils/useMousePosition.ts diff --git a/src/components/DataRoom/IndustryComparison/Content.tsx b/src/components/DataRoom/IndustryComparison/Content.tsx index 7c778945..219ab04c 100644 --- a/src/components/DataRoom/IndustryComparison/Content.tsx +++ b/src/components/DataRoom/IndustryComparison/Content.tsx @@ -25,7 +25,7 @@ const Content = ({ containerRef, title }: { containerRef: RefObject {title} - + ) } diff --git a/src/components/DataRoom/IndustryComparison/DotGrid.tsx b/src/components/DataRoom/IndustryComparison/DotGrid.tsx index 0f6b3a6e..c32c7af2 100644 --- a/src/components/DataRoom/IndustryComparison/DotGrid.tsx +++ b/src/components/DataRoom/IndustryComparison/DotGrid.tsx @@ -1,3 +1,4 @@ +import type { MotionValue } from 'framer-motion' import { useEffect, useRef, useCallback } from 'react' import { createDots } from './utils/createDots' import { drawDots } from './utils/drawDots' @@ -5,13 +6,21 @@ import { useIsMediumScreen } from '@/hooks/useMaxWidth' import css from './styles.module.css' import useContainerSize from '@/hooks/useContainerSize' import { updateCanvas } from './utils/updateCanvas' - -export default function DotGrid({ containerRef }: { containerRef: React.RefObject }) { +import useMousePosition from './utils/useMousePosition' + +export default function DotGrid({ + containerRef, + scrollYProgress, +}: { + containerRef: React.RefObject + scrollYProgress?: MotionValue +}) { const canvasRef = useRef(null) const isMobile = useIsMediumScreen() const dimensions = useContainerSize(containerRef) + const mousePosition = useMousePosition(canvasRef, dimensions, scrollYProgress) - const prevRenderStateRef = useRef({ dimensions }) + const prevRenderStateRef = useRef({ dimensions, mousePosition, isMobile }) const dotsRef = useRef | null>(null) const animationFrameId = useRef() @@ -20,21 +29,27 @@ export default function DotGrid({ containerRef }: { containerRef: React.RefObjec const ctx = canvas?.getContext('2d') if (!canvas || !ctx || dimensions.width <= 0 || dimensions.height <= 0) return - const currentRenderState = { dimensions } + const currentRenderState = { dimensions, mousePosition, isMobile } const prevRenderState = prevRenderStateRef.current - if (!dotsRef.current || currentRenderState.dimensions !== prevRenderState.dimensions) { + if ( + !dotsRef.current || + currentRenderState.dimensions !== prevRenderState.dimensions || + currentRenderState.isMobile !== prevRenderState.isMobile + ) { dotsRef.current = createDots(dimensions, isMobile) updateCanvas(canvas, ctx, dimensions.width, dimensions.height) // Draw dots immediately after creating or updating them // This draw call ensure dots are already visible when canvas scrolls into view - drawDots(ctx, dotsRef.current) + drawDots(ctx, dotsRef.current, mousePosition, isMobile) + } else if (currentRenderState.mousePosition !== prevRenderState.mousePosition) { + drawDots(ctx, dotsRef.current, mousePosition, isMobile) } prevRenderStateRef.current = currentRenderState animationFrameId.current = requestAnimationFrame(renderFrame) - }, [dimensions, isMobile]) + }, [dimensions, mousePosition, isMobile]) useEffect(() => { renderFrame() diff --git a/src/components/DataRoom/IndustryComparison/utils/drawDots.ts b/src/components/DataRoom/IndustryComparison/utils/drawDots.ts index b0a5c688..f0d02f9c 100644 --- a/src/components/DataRoom/IndustryComparison/utils/drawDots.ts +++ b/src/components/DataRoom/IndustryComparison/utils/drawDots.ts @@ -1,12 +1,52 @@ type Position = { x: number; y: number } -const DOT_COLOR = '#12312' +const MAX_SCALE_DISTANCE = 15 +const MOBILE_MAX_SCALE = 8 +const DESKTOP_MAX_SCALE = 12 +const DOT_COLOR = '#121312' + +let lastUpdatedArea: { x: number; y: number; radius: number } | null = null + +export const drawDots = ( + ctx: CanvasRenderingContext2D, + dots: Position[], + mousePosition: Position, + isMobile: boolean, +) => { + const maxScale = isMobile ? MOBILE_MAX_SCALE : DESKTOP_MAX_SCALE + const updatedRadius = MAX_SCALE_DISTANCE * maxScale + + // Clear the previously updated area + if (lastUpdatedArea) { + ctx.clearRect( + lastUpdatedArea.x - lastUpdatedArea.radius, + lastUpdatedArea.y - lastUpdatedArea.radius, + lastUpdatedArea.radius * 2, + lastUpdatedArea.radius * 2, + ) + } + // Clear the new (to be updated) area + ctx.clearRect(mousePosition.x - updatedRadius, mousePosition.y - updatedRadius, updatedRadius * 2, updatedRadius * 2) -export const drawDots = (ctx: CanvasRenderingContext2D, dots: Position[]) => { ctx.fillStyle = DOT_COLOR + dots.forEach((dot) => { - ctx.beginPath() - ctx.arc(dot.x, dot.y, 1, 0, 2 * Math.PI) - ctx.fill() + const dx = mousePosition.x - dot.x + const dy = mousePosition.y - dot.y + const distance = Math.sqrt(dx * dx + dy * dy) + + if (distance <= updatedRadius) { + const scale = Math.max(1, maxScale - distance / MAX_SCALE_DISTANCE) + ctx.beginPath() + ctx.arc(dot.x, dot.y, 1 * scale, 0, 2 * Math.PI) + ctx.fill() + } else { + // Draw unupdated dots with normal size + ctx.beginPath() + ctx.arc(dot.x, dot.y, 1, 0, 2 * Math.PI) + ctx.fill() + } }) + + lastUpdatedArea = { x: mousePosition.x, y: mousePosition.y, radius: updatedRadius } } diff --git a/src/components/DataRoom/IndustryComparison/utils/useMousePosition.ts b/src/components/DataRoom/IndustryComparison/utils/useMousePosition.ts new file mode 100644 index 00000000..5ce684bc --- /dev/null +++ b/src/components/DataRoom/IndustryComparison/utils/useMousePosition.ts @@ -0,0 +1,43 @@ +import { useState, useEffect } from 'react' +import type { MotionValue } from 'framer-motion' +import { useIsMediumScreen } from '@/hooks/useMaxWidth' + +/** + * Custom hook to track mouse position or simulate it based on scroll progress. + * @param canvasRef - Reference to the canvas element. + * @param dimensions - Object containing width and height of the container. + * @param scrollYProgress - MotionValue for scroll progress, used on mobile devices. + * @returns An object with x and y coordinates representing either: + * - Actual mouse position relative to the canvas (on desktop) + * - Simulated position based on scroll progress (on mobile) + */ +export default function useMousePosition( + canvasRef: React.RefObject, + dimensions: { width: number; height: number }, + scrollYProgress?: MotionValue, +) { + const isMobile = useIsMediumScreen() + const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }) + + useEffect(() => { + if (isMobile && scrollYProgress) { + const updatePositionMobile = () => { + const progress = scrollYProgress.get() + setMousePosition({ x: dimensions.width - dimensions.width / 8, y: progress * dimensions.height }) + } + return scrollYProgress.on('change', updatePositionMobile) + } else { + const canvas = canvasRef.current + const updatePositionDesktop = (e: MouseEvent) => { + const rect = canvas?.getBoundingClientRect() + if (rect) { + setMousePosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }) + } + } + canvas?.addEventListener('mousemove', updatePositionDesktop) + return () => canvas?.removeEventListener('mousemove', updatePositionDesktop) + } + }, [canvasRef, isMobile, scrollYProgress, dimensions]) + + return mousePosition +}