Skip to content

Commit

Permalink
feat(dataroom): DotGrid animation (#442)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Diogo Soares <[email protected]>
  • Loading branch information
3 people authored Oct 9, 2024
1 parent 3c36729 commit 45be219
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/components/DataRoom/IndustryComparison/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const Content = ({ containerRef, title }: { containerRef: RefObject<HTMLDivEleme
<Typography align="center" variant="h1">
{title}
</Typography>
<DotGrid containerRef={containerRef} />
<DotGrid containerRef={containerRef} scrollYProgress={scrollYProgress} />
</motion.div>
)
}
Expand Down
29 changes: 22 additions & 7 deletions src/components/DataRoom/IndustryComparison/DotGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import type { MotionValue } from 'framer-motion'
import { useEffect, useRef, useCallback } from 'react'
import { createDots } from './utils/createDots'
import { drawDots } from './utils/drawDots'
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<HTMLDivElement> }) {
import useMousePosition from './utils/useMousePosition'

export default function DotGrid({
containerRef,
scrollYProgress,
}: {
containerRef: React.RefObject<HTMLDivElement>
scrollYProgress?: MotionValue<number>
}) {
const canvasRef = useRef<HTMLCanvasElement>(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<ReturnType<typeof createDots> | null>(null)
const animationFrameId = useRef<number>()

Expand All @@ -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()
Expand Down
50 changes: 45 additions & 5 deletions src/components/DataRoom/IndustryComparison/utils/drawDots.ts
Original file line number Diff line number Diff line change
@@ -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 }
}
Original file line number Diff line number Diff line change
@@ -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<HTMLCanvasElement>,
dimensions: { width: number; height: number },
scrollYProgress?: MotionValue<number>,
) {
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
}

0 comments on commit 45be219

Please sign in to comment.