Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dataroom): DotGrid animation #442

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading