Skip to content

Commit

Permalink
refactor: 💡 update animation to use canvas instead of svg
Browse files Browse the repository at this point in the history
  • Loading branch information
Malayvasa committed Aug 20, 2024
1 parent 7a1eed3 commit 3a1432f
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 53 deletions.
8 changes: 1 addition & 7 deletions src/components/DataRoom/SeeMore/SlidingWave.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,7 @@ export default function SlidingWave({
</motion.div>
<div className={css.gradientBox}></div>
<motion.div style={{ opacity }} className={css.wave}>
<Wave color="#12FF80" amplitude={isMobile ? 100 : 200} />
</motion.div>
<motion.div style={{ opacity }} className={css.wave}>
<Wave color="#008A40" amplitude={isMobile ? 90 : 170} />
</motion.div>
<motion.div style={{ opacity }} className={css.wave}>
<Wave color="#003C1C" amplitude={isMobile ? 80 : 140} />
<Wave colors={['#003C1C', '#008A40', '#12FF80']} amplitude={isMobile ? 100 : 200} />
</motion.div>
</>
)
Expand Down
107 changes: 73 additions & 34 deletions src/components/DataRoom/SeeMore/Wave.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,105 @@
import useContainerSize from '@/hooks/useContainerSize'
import React, { useEffect, useRef } from 'react'
import React, { useEffect, useRef, useMemo, useCallback } from 'react'

type WaveProps = {
height?: number
color?: string
colors?: string[]
amplitude?: number
frequency?: number
speed?: number
}

const DEFAULT_HEIGHT = 600
const DEFAULT_COLOR = '#12FF80'
const DEFAULT_COLORS = ['#12FF80', '#008A40', '#003C1C']
const DEFAULT_AMPLITUDE = 100
const DEFAULT_FREQUENCY = 1
const DEFAULT_SPEED = 3
const DEFAULT_SPEED = 0.8
const STROKE_WIDTH = 4

const Wave = ({
height = DEFAULT_HEIGHT,
color = DEFAULT_COLOR,
colors = DEFAULT_COLORS,
amplitude = DEFAULT_AMPLITUDE,
frequency = DEFAULT_FREQUENCY,
speed = DEFAULT_SPEED,
}: WaveProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const { width } = useContainerSize(containerRef)
const animationRef = useRef<number>()

useEffect(() => {
let startTime: number | null = null
let animationFrameId: number

const animate = (time: number) => {
if (!startTime) startTime = time
const elapsed = time - startTime

// Only create the path if width is valid
if (width > 0) {
const path = Array.from({ length: width + 1 }, (_, i) => {
const y = height / 2 + amplitude * Math.sin((i / width) * 2 * Math.PI * frequency + (elapsed * speed) / 1000)
return `${i},${y}`
}).join(' L')

const pathElement = containerRef.current?.querySelector('path')
if (pathElement) {
pathElement.setAttribute('d', `M${path}`)
}
const dpr = useMemo(() => window.devicePixelRatio || 1, [])

const paths = useMemo(() => {
if (width === 0) return [[], [], []]
const calculatePath = (amplitudeOffset: number) =>
Array.from(
{ length: width + 1 },
(_, i) => (amplitude - amplitudeOffset) * Math.sin((i / width) * 2 * Math.PI * frequency),
)
return [calculatePath(80), calculatePath(40), calculatePath(0)]
}, [width, amplitude, frequency])

const setupCanvas = useCallback(() => {
const canvas = canvasRef.current
const ctx = canvas?.getContext('2d')
if (!ctx || !canvas || width === 0) return

canvas.width = width * dpr
canvas.height = height * dpr
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
ctx.scale(dpr, dpr)
}, [width, height, dpr])

const drawWave = useCallback(
(ctx: CanvasRenderingContext2D, path: number[], color: string, shift: number) => {
ctx.beginPath()
ctx.lineWidth = STROKE_WIDTH
ctx.strokeStyle = color

for (let i = 0; i <= width; i++) {
ctx.lineTo(i, height / 2 + path[(i + Math.floor(shift)) % width])
}

animationFrameId = requestAnimationFrame(animate)
}
ctx.stroke()
},
[width, height],
)

const animate = useCallback(
(time: number) => {
const canvas = canvasRef.current
const ctx = canvas?.getContext('2d')
if (!ctx || !canvas) return

animationFrameId = requestAnimationFrame(animate)
return () => cancelAnimationFrame(animationFrameId)
}, [width, height, amplitude, frequency, speed])
const shift = (time * speed) % width

ctx.clearRect(0, 0, width, height)

paths.forEach((path, index) => {
drawWave(ctx, path, colors[index % colors.length], shift)
})

animationRef.current = requestAnimationFrame(animate)
},
[width, height, paths, speed, colors, drawWave],
)

useEffect(() => {
setupCanvas()
animationRef.current = requestAnimationFrame(animate)

return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [setupCanvas, animate])

return (
<div ref={containerRef} style={{ width: '100%' }}>
{width > 0 && (
<svg width={width} height={height}>
<path fill="none" stroke={color} strokeWidth={STROKE_WIDTH} />
</svg>
)}
<canvas ref={canvasRef} />
</div>
)
}
Expand Down
13 changes: 1 addition & 12 deletions src/components/DataRoom/SeeMore/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,7 @@
width: 100%;
transform: translateY(-50%);
right: 0;
--wave-index: 30;
z-index: calc(var(--wave-index) - (var(--n) * 10));
}

.wave:nth-child(1) {
--n: 0;
}
.wave:nth-child(2) {
--n: 1;
}
.wave:nth-child(3) {
--n: 2;
z-index: 30;
}

@media (max-width: 900px) {
Expand Down

0 comments on commit 3a1432f

Please sign in to comment.