Skip to content

Commit

Permalink
chore(partners): add interactive particle elements
Browse files Browse the repository at this point in the history
  • Loading branch information
hyamero committed Aug 17, 2024
1 parent 28beac4 commit 109defa
Show file tree
Hide file tree
Showing 2 changed files with 285 additions and 5 deletions.
270 changes: 270 additions & 0 deletions apps/partners/src/app/components/particles.tsx
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;
20 changes: 15 additions & 5 deletions apps/partners/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@ import { cn } from "@umamin/ui/lib/utils";
import { Button } from "@umamin/ui/components/button";
import { AnimatedShinyText } from "@umamin/ui/components/animated-shiny-text";
import GridPattern from "./components/grid-pattern";
import Particles from "./components/particles";

export default function Home() {
return (
<main className="flex items-center flex-col min-h-screen lg:pt-44 py-36 md:gap-8 gap-6 container">
<GridPattern className="[mask-image:radial-gradient(ellipse_at_center,white,transparent_60%)]" />
<GridPattern className="opacity-60 z-[-1] [mask-image:radial-gradient(ellipse_at_center,white,transparent_60%)]" />
<Particles
className="absolute inset-0 z-[-1]"
quantity={75}
ease={80}
color="#888888"
refresh
/>

<Link
href="#"
className={cn(
"group rounded-full border border-black/5 bg-zinc-100 text-base text-white transition-all ease-in hover:cursor-pointer hover:bg-zinc-200 dark:border-white/5 dark:bg-zinc-900 dark:hover:bg-zinc-800"
"group rounded-full border border-black/5 bg-slate-100 text-base text-white transition-all ease-in hover:cursor-pointer hover:bg-slate-200 dark:border-white/5 dark:bg-slate-900/20 dark:hover:bg-slate-800/20"
)}
>
<AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 transition ease-out hover:text-zinc-300 hover:duration-300 hover:dark:text-zinc-400">
Expand All @@ -21,16 +30,17 @@ export default function Home() {
</AnimatedShinyText>
</Link>

<div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px] border-b-2 border-muted border-dashed md:pb-8 pb-6">
<div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
<h1 className="font-extrabold md:text-7xl max-w-2xl text-[10vw] leading-none dark:bg-gradient-to-b from-foreground dark:to-zinc-400 bg-clip-text bg-zinc-800 text-transparent tracking-tighter text-center">
Powerful Tools for Anonymity at Scale
</h1>
</div>

<p className="text-muted-foreground text-center md:text-xl max-w-2xl">
Effortlessly manage large volumes of anonymous feedback, surveys, and
communications with Umamin Partners. Tailored for businesses,
organizations, and individuals.
communications with
<span className="text-zinc-300 font-medium"> Umamin Partners</span>.
Tailored for businesses, organizations, and individuals.
</p>

<div className="flex items-center gap-2">
Expand Down

0 comments on commit 109defa

Please sign in to comment.