Skip to content
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: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import About from './sections/About'
import Projects from './sections/Projects'
import Experiences from './sections/Experiences'
import Testimonial from './sections/Testimonial'
import Contact from './sections/Contact'

const App = () => {
return (
Expand All @@ -16,6 +17,7 @@ const App = () => {
<Projects/>
<Experiences/>
<Testimonial/>
<Contact/>
<section className="min-h-screen"></section>
<section className="min-h-screen"></section>
<section className="min-h-screen"></section>
Expand Down
37 changes: 37 additions & 0 deletions src/components/Alert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { motion } from "framer-motion";

const Alert = ({ type, text }) => {
const alertVariants = {
hidden: { opacity: 0, y: 50, scale: 0.8 },
visible: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, y: -50, scale: 0.8 },
};

return (
<motion.div
className="fixed z-50 flex items-center justify-center bottom-5 right-5"
initial="hidden"
animate="visible"
exit="exit"
variants={alertVariants}
transition={{ duration: 0.3, ease: "easeInOut" }}
>
<div
className={`p-2 ${
type === "danger" ? "bg-red-800" : "bg-royal"
} items-center text-indigo-100 leading-none lg:rounded-full flex lg:inline-flex rounded-md p-5`}
>
<p
className={`flex rounded-full ${
type === "danger" ? "bg-red-500" : "bg-lavender"
} uppercase px-2 py-1 text-xs font-semibold mr-3`}
>
{type === "danger" ? "Failed" : "Success"}
</p>
<p className="mr-2 text-left">{text}</p>
</div>
</motion.div>
);
};

export default Alert;
270 changes: 270 additions & 0 deletions src/components/Particles.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { twMerge } from "tailwind-merge";
import React, { useEffect, useRef, useState } from "react";

function MousePosition() {
const [mousePosition, setMousePosition] = useState({
x: 0,
y: 0,
});

useEffect(() => {
const handleMouseMove = (event) => {
setMousePosition({ x: event.clientX, y: event.clientY });
};

window.addEventListener("mousemove", handleMouseMove);

return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);

return mousePosition;
}

function hexToRgb(hex) {
hex = hex.replace("#", "");

if (hex.length === 3) {
hex = hex
.split("")
.map((char) => char + char)
.join("");
}

const hexInt = parseInt(hex, 16);
const red = (hexInt >> 16) & 255;
const green = (hexInt >> 8) & 255;
const blue = hexInt & 255;
return [red, green, blue];
}

export const Particles = ({
className = "",
quantity = 100,
staticity = 50,
ease = 50,
size = 0.4,
refresh = false,
color = "#ffffff",
vx = 0,
vy = 0,
...props
}) => {
const canvasRef = useRef(null);
const canvasContainerRef = useRef(null);
const context = useRef(null);
const circles = useRef([]);
const mousePosition = MousePosition();
const mouse = useRef({ x: 0, y: 0 });
const canvasSize = useRef({ w: 0, h: 0 });
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
const rafID = useRef(null);
const resizeTimeout = useRef(null);

useEffect(() => {
if (canvasRef.current) {
context.current = canvasRef.current.getContext("2d");
}
initCanvas();
animate();

const handleResize = () => {
if (resizeTimeout.current) {
clearTimeout(resizeTimeout.current);
}
resizeTimeout.current = setTimeout(() => {
initCanvas();
}, 200);
};

window.addEventListener("resize", handleResize);

return () => {
if (rafID.current != null) {
window.cancelAnimationFrame(rafID.current);
}
if (resizeTimeout.current) {
clearTimeout(resizeTimeout.current);
}
window.removeEventListener("resize", handleResize);
};
}, [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;
}
}
};

const resizeCanvas = () => {
if (canvasContainerRef.current && canvasRef.current && context.current) {
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);

// Clear existing particles and create new ones with exact quantity
circles.current = [];
for (let i = 0; i < quantity; i++) {
const circle = circleParams();
drawCircle(circle);
}
}
};

const circleParams = () => {
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, 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, start1, end1, start2, end2) => {
const remapped =
((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
return remapped > 0 ? remapped : 0;
};

const animate = () => {
clearContext();
circles.current.forEach((circle, i) => {
// 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);
}
});
rafID.current = window.requestAnimationFrame(animate);
};

return (
<div
className={twMerge("pointer-events-none", className)}
ref={canvasContainerRef}
aria-hidden="true"
{...props}
>
<canvas ref={canvasRef} className="size-full" />
</div>
);
};
Loading