-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(partners): add interactive particle elements
- Loading branch information
Showing
2 changed files
with
285 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
"use client"; | ||
|
||
import React, { useEffect, useRef, useState } from "react"; | ||
|
||
interface MousePosition { | ||
x: number; | ||
y: number; | ||
} | ||
|
||
function MousePosition(): MousePosition { | ||
const [mousePosition, setMousePosition] = useState<MousePosition>({ | ||
x: 0, | ||
y: 0, | ||
}); | ||
|
||
useEffect(() => { | ||
const handleMouseMove = (event: MouseEvent) => { | ||
setMousePosition({ x: event.clientX, y: event.clientY }); | ||
}; | ||
|
||
window.addEventListener("mousemove", handleMouseMove); | ||
|
||
return () => { | ||
window.removeEventListener("mousemove", handleMouseMove); | ||
}; | ||
}, []); | ||
|
||
return mousePosition; | ||
} | ||
|
||
interface ParticlesProps { | ||
className?: string; | ||
quantity?: number; | ||
staticity?: number; | ||
ease?: number; | ||
size?: number; | ||
refresh?: boolean; | ||
color?: string; | ||
vx?: number; | ||
vy?: number; | ||
} | ||
function hexToRgb(hex: string): number[] { | ||
hex = hex.replace("#", ""); | ||
const hexInt = parseInt(hex, 16); | ||
const red = (hexInt >> 16) & 255; | ||
const green = (hexInt >> 8) & 255; | ||
const blue = hexInt & 255; | ||
return [red, green, blue]; | ||
} | ||
|
||
const Particles: React.FC<ParticlesProps> = ({ | ||
className = "", | ||
quantity = 100, | ||
staticity = 50, | ||
ease = 50, | ||
size = 0.4, | ||
refresh = false, | ||
color = "#ffffff", | ||
vx = 0, | ||
vy = 0, | ||
}) => { | ||
const canvasRef = useRef<HTMLCanvasElement>(null); | ||
const canvasContainerRef = useRef<HTMLDivElement>(null); | ||
const context = useRef<CanvasRenderingContext2D | null>(null); | ||
const circles = useRef<any[]>([]); | ||
const mousePosition = MousePosition(); | ||
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); | ||
const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }); | ||
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1; | ||
|
||
useEffect(() => { | ||
if (canvasRef.current) { | ||
context.current = canvasRef.current.getContext("2d"); | ||
} | ||
initCanvas(); | ||
animate(); | ||
window.addEventListener("resize", initCanvas); | ||
|
||
return () => { | ||
window.removeEventListener("resize", initCanvas); | ||
}; | ||
}, [color]); | ||
|
||
useEffect(() => { | ||
onMouseMove(); | ||
}, [mousePosition.x, mousePosition.y]); | ||
|
||
useEffect(() => { | ||
initCanvas(); | ||
}, [refresh]); | ||
|
||
const initCanvas = () => { | ||
resizeCanvas(); | ||
drawParticles(); | ||
}; | ||
|
||
const onMouseMove = () => { | ||
if (canvasRef.current) { | ||
const rect = canvasRef.current.getBoundingClientRect(); | ||
const { w, h } = canvasSize.current; | ||
const x = mousePosition.x - rect.left - w / 2; | ||
const y = mousePosition.y - rect.top - h / 2; | ||
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2; | ||
if (inside) { | ||
mouse.current.x = x; | ||
mouse.current.y = y; | ||
} | ||
} | ||
}; | ||
|
||
type Circle = { | ||
x: number; | ||
y: number; | ||
translateX: number; | ||
translateY: number; | ||
size: number; | ||
alpha: number; | ||
targetAlpha: number; | ||
dx: number; | ||
dy: number; | ||
magnetism: number; | ||
}; | ||
|
||
const resizeCanvas = () => { | ||
if (canvasContainerRef.current && canvasRef.current && context.current) { | ||
circles.current.length = 0; | ||
canvasSize.current.w = canvasContainerRef.current.offsetWidth; | ||
canvasSize.current.h = canvasContainerRef.current.offsetHeight; | ||
canvasRef.current.width = canvasSize.current.w * dpr; | ||
canvasRef.current.height = canvasSize.current.h * dpr; | ||
canvasRef.current.style.width = `${canvasSize.current.w}px`; | ||
canvasRef.current.style.height = `${canvasSize.current.h}px`; | ||
context.current.scale(dpr, dpr); | ||
} | ||
}; | ||
|
||
const circleParams = (): Circle => { | ||
const x = Math.floor(Math.random() * canvasSize.current.w); | ||
const y = Math.floor(Math.random() * canvasSize.current.h); | ||
const translateX = 0; | ||
const translateY = 0; | ||
const pSize = Math.floor(Math.random() * 2) + size; | ||
const alpha = 0; | ||
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)); | ||
const dx = (Math.random() - 0.5) * 0.1; | ||
const dy = (Math.random() - 0.5) * 0.1; | ||
const magnetism = 0.1 + Math.random() * 4; | ||
return { | ||
x, | ||
y, | ||
translateX, | ||
translateY, | ||
size: pSize, | ||
alpha, | ||
targetAlpha, | ||
dx, | ||
dy, | ||
magnetism, | ||
}; | ||
}; | ||
|
||
const rgb = hexToRgb(color); | ||
|
||
const drawCircle = (circle: Circle, update = false) => { | ||
if (context.current) { | ||
const { x, y, translateX, translateY, size, alpha } = circle; | ||
context.current.translate(translateX, translateY); | ||
context.current.beginPath(); | ||
context.current.arc(x, y, size, 0, 2 * Math.PI); | ||
context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`; | ||
context.current.fill(); | ||
context.current.setTransform(dpr, 0, 0, dpr, 0, 0); | ||
|
||
if (!update) { | ||
circles.current.push(circle); | ||
} | ||
} | ||
}; | ||
|
||
const clearContext = () => { | ||
if (context.current) { | ||
context.current.clearRect( | ||
0, | ||
0, | ||
canvasSize.current.w, | ||
canvasSize.current.h | ||
); | ||
} | ||
}; | ||
|
||
const drawParticles = () => { | ||
clearContext(); | ||
const particleCount = quantity; | ||
for (let i = 0; i < particleCount; i++) { | ||
const circle = circleParams(); | ||
drawCircle(circle); | ||
} | ||
}; | ||
|
||
const remapValue = ( | ||
value: number, | ||
start1: number, | ||
end1: number, | ||
start2: number, | ||
end2: number | ||
): number => { | ||
const remapped = | ||
((value - start1) * (end2 - start2)) / (end1 - start1) + start2; | ||
return remapped > 0 ? remapped : 0; | ||
}; | ||
|
||
const animate = () => { | ||
clearContext(); | ||
circles.current.forEach((circle: Circle, i: number) => { | ||
// Handle the alpha value | ||
const edge = [ | ||
circle.x + circle.translateX - circle.size, // distance from left edge | ||
canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge | ||
circle.y + circle.translateY - circle.size, // distance from top edge | ||
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge | ||
]; | ||
const closestEdge = edge.reduce((a, b) => Math.min(a, b)); | ||
const remapClosestEdge = parseFloat( | ||
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2) | ||
); | ||
if (remapClosestEdge > 1) { | ||
circle.alpha += 0.02; | ||
if (circle.alpha > circle.targetAlpha) { | ||
circle.alpha = circle.targetAlpha; | ||
} | ||
} else { | ||
circle.alpha = circle.targetAlpha * remapClosestEdge; | ||
} | ||
circle.x += circle.dx + vx; | ||
circle.y += circle.dy + vy; | ||
circle.translateX += | ||
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / | ||
ease; | ||
circle.translateY += | ||
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / | ||
ease; | ||
|
||
drawCircle(circle, true); | ||
|
||
// circle gets out of the canvas | ||
if ( | ||
circle.x < -circle.size || | ||
circle.x > canvasSize.current.w + circle.size || | ||
circle.y < -circle.size || | ||
circle.y > canvasSize.current.h + circle.size | ||
) { | ||
// remove the circle from the array | ||
circles.current.splice(i, 1); | ||
// create a new circle | ||
const newCircle = circleParams(); | ||
drawCircle(newCircle); | ||
// update the circle position | ||
} | ||
}); | ||
window.requestAnimationFrame(animate); | ||
}; | ||
|
||
return ( | ||
<div className={className} ref={canvasContainerRef} aria-hidden="true"> | ||
<canvas ref={canvasRef} className="h-full w-full" /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Particles; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters