feat(ui): enhance contact form messaging and particle background animation

- Improved the contact form's message generation with more professional and detailed templates
- Replaced the grid-based background animation with a WebGL-powered particle system using OGL
- Added GSAP for advanced animation capabilities
- Adjusted layout styling for better centering and spacing across components
- Enhanced visual feedback and interactivity of the animated background

This change significantly upgrades the user experience and visual appeal of the contact section while maintaining performance through efficient rendering techniques.
This commit is contained in:
¨evandrodevbr¨ committed 2025-09-03 00:51:29 -03:00
1 parent 90cf3c73ad
commit 780fd7c161
13 files changed
+1230 -203

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ export function BentoGrid({ children, className }: BentoGridProps) {
return (
<div
className={cn(
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto",
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto justify-center",
className
)}
>
+217 -145
View File
@@ -1,181 +1,253 @@
"use client";
import React, { useEffect, useRef } from "react";
import { Renderer, Camera, Geometry, Program, Mesh } from "ogl";
import { useRef, useEffect, useState } from "react";
interface SquaresProps {
direction?: "right" | "left" | "up" | "down" | "diagonal";
interface ParticlesProps {
particleCount?: number;
particleSpread?: number;
speed?: number;
borderColor?: string;
squareSize?: number;
hoverFillColor?: string;
particleColors?: string[];
moveParticlesOnHover?: boolean;
particleHoverFactor?: number;
alphaParticles?: boolean;
particleBaseSize?: number;
sizeRandomness?: number;
cameraDistance?: number;
disableRotation?: boolean;
className?: string;
}
export function Squares({
direction = "right",
speed = 1,
borderColor = "#333",
squareSize = 40,
hoverFillColor = "#222",
const defaultColors: string[] = ["#ffffff", "#ffffff", "#ffffff"];
const hexToRgb = (hex: string): [number, number, number] => {
hex = hex.replace(/^#/, "");
if (hex.length === 3) {
hex = hex
.split("")
.map((c) => c + c)
.join("");
}
const int = parseInt(hex, 16);
const r = ((int >> 16) & 255) / 255;
const g = ((int >> 8) & 255) / 255;
const b = (int & 255) / 255;
return [r, g, b];
};
const vertex = /* glsl */ `
attribute vec3 position;
attribute vec4 random;
attribute vec3 color;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform float uTime;
uniform float uSpread;
uniform float uBaseSize;
uniform float uSizeRandomness;
varying vec4 vRandom;
varying vec3 vColor;
void main() {
vRandom = random;
vColor = color;
vec3 pos = position * uSpread;
pos.z *= 10.0;
vec4 mPos = modelMatrix * vec4(pos, 1.0);
float t = uTime;
mPos.x += sin(t * random.z + 6.28 * random.w) * mix(0.1, 1.5, random.x);
mPos.y += sin(t * random.y + 6.28 * random.x) * mix(0.1, 1.5, random.w);
mPos.z += sin(t * random.w + 6.28 * random.y) * mix(0.1, 1.5, random.z);
vec4 mvPos = viewMatrix * mPos;
gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz);
gl_Position = projectionMatrix * mvPos;
}
`;
const fragment = /* glsl */ `
precision highp float;
uniform float uTime;
uniform float uAlphaParticles;
varying vec4 vRandom;
varying vec3 vColor;
void main() {
vec2 uv = gl_PointCoord.xy;
float d = length(uv - vec2(0.5));
if(uAlphaParticles < 0.5) {
if(d > 0.5) {
discard;
}
gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), 1.0);
} else {
float circle = smoothstep(0.5, 0.4, d) * 0.8;
gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), circle);
}
}
`;
const Particles: React.FC<ParticlesProps> = ({
particleCount = 200,
particleSpread = 10,
speed = 0.1,
particleColors,
moveParticlesOnHover = false,
particleHoverFactor = 1,
alphaParticles = false,
particleBaseSize = 100,
sizeRandomness = 1,
cameraDistance = 20,
disableRotation = false,
className,
}: SquaresProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const requestRef = useRef<number | undefined>(undefined);
const numSquaresX = useRef<number | undefined>(undefined);
const numSquaresY = useRef<number | undefined>(undefined);
const gridOffset = useRef({ x: 0, y: 0 });
const [hoveredSquare, setHoveredSquare] = useState<{
x: number;
y: number;
} | null>(null);
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const mouseRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const container = containerRef.current;
if (!container) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const renderer = new Renderer({ depth: false, alpha: true });
const gl = renderer.gl;
container.appendChild(gl.canvas);
gl.clearColor(0, 0, 0, 0);
// Set canvas background
canvas.style.background = "#060606";
const camera = new Camera(gl, { fov: 15 });
camera.position.set(0, 0, cameraDistance);
const resizeCanvas = () => {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
numSquaresX.current = Math.ceil(canvas.width / squareSize) + 1;
numSquaresY.current = Math.ceil(canvas.height / squareSize) + 1;
const resize = () => {
const width = container.clientWidth;
const height = container.clientHeight;
renderer.setSize(width, height);
camera.perspective({ aspect: gl.canvas.width / gl.canvas.height });
};
window.addEventListener("resize", resize, false);
resize();
const handleMouseMove = (e: MouseEvent) => {
const rect = container.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
mouseRef.current = { x, y };
};
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
if (moveParticlesOnHover) {
container.addEventListener("mousemove", handleMouseMove);
}
const drawGrid = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const count = particleCount;
const positions = new Float32Array(count * 3);
const randoms = new Float32Array(count * 4);
const colors = new Float32Array(count * 3);
const palette =
particleColors && particleColors.length > 0
? particleColors
: defaultColors;
const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;
for (let i = 0; i < count; i++) {
let x: number, y: number, z: number, len: number;
do {
x = Math.random() * 2 - 1;
y = Math.random() * 2 - 1;
z = Math.random() * 2 - 1;
len = x * x + y * y + z * z;
} while (len > 1 || len === 0);
const r = Math.cbrt(Math.random());
positions.set([x * r, y * r, z * r], i * 3);
randoms.set(
[Math.random(), Math.random(), Math.random(), Math.random()],
i * 4
);
const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]);
colors.set(col, i * 3);
}
ctx.lineWidth = 0.5;
const geometry = new Geometry(gl, {
position: { size: 3, data: positions },
random: { size: 4, data: randoms },
color: { size: 3, data: colors },
});
for (let x = startX; x < canvas.width + squareSize; x += squareSize) {
for (let y = startY; y < canvas.height + squareSize; y += squareSize) {
const squareX = x - (gridOffset.current.x % squareSize);
const squareY = y - (gridOffset.current.y % squareSize);
const program = new Program(gl, {
vertex,
fragment,
uniforms: {
uTime: { value: 0 },
uSpread: { value: particleSpread },
uBaseSize: { value: particleBaseSize },
uSizeRandomness: { value: sizeRandomness },
uAlphaParticles: { value: alphaParticles ? 1 : 0 },
},
transparent: true,
depthTest: false,
});
if (
hoveredSquare &&
Math.floor((x - startX) / squareSize) === hoveredSquare.x &&
Math.floor((y - startY) / squareSize) === hoveredSquare.y
) {
ctx.fillStyle = hoverFillColor;
ctx.fillRect(squareX, squareY, squareSize, squareSize);
}
const particles = new Mesh(gl, { mode: gl.POINTS, geometry, program });
ctx.strokeStyle = borderColor;
ctx.strokeRect(squareX, squareY, squareSize, squareSize);
}
let animationFrameId: number;
let lastTime = performance.now();
let elapsed = 0;
const update = (t: number) => {
animationFrameId = requestAnimationFrame(update);
const delta = t - lastTime;
lastTime = t;
elapsed += delta * speed;
program.uniforms.uTime.value = elapsed * 0.001;
if (moveParticlesOnHover) {
particles.position.x = -mouseRef.current.x * particleHoverFactor;
particles.position.y = -mouseRef.current.y * particleHoverFactor;
} else {
particles.position.x = 0;
particles.position.y = 0;
}
const gradient = ctx.createRadialGradient(
canvas.width / 2,
canvas.height / 2,
0,
canvas.width / 2,
canvas.height / 2,
Math.sqrt(Math.pow(canvas.width, 2) + Math.pow(canvas.height, 2)) / 2
);
gradient.addColorStop(0, "rgba(6, 6, 6, 0)");
gradient.addColorStop(1, "#060606");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
const updateAnimation = () => {
const effectiveSpeed = Math.max(speed, 0.1);
switch (direction) {
case "right":
gridOffset.current.x =
(gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
break;
case "left":
gridOffset.current.x =
(gridOffset.current.x + effectiveSpeed + squareSize) % squareSize;
break;
case "up":
gridOffset.current.y =
(gridOffset.current.y + effectiveSpeed + squareSize) % squareSize;
break;
case "down":
gridOffset.current.y =
(gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
break;
case "diagonal":
gridOffset.current.x =
(gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
gridOffset.current.y =
(gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
break;
if (!disableRotation) {
particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1;
particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15;
particles.rotation.z += 0.01 * speed;
}
drawGrid();
requestRef.current = requestAnimationFrame(updateAnimation);
renderer.render({ scene: particles, camera });
};
const handleMouseMove = (event: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
animationFrameId = requestAnimationFrame(update);
const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;
const hoveredSquareX = Math.floor(
(mouseX + gridOffset.current.x - startX) / squareSize
);
const hoveredSquareY = Math.floor(
(mouseY + gridOffset.current.y - startY) / squareSize
);
setHoveredSquare({ x: hoveredSquareX, y: hoveredSquareY });
};
const handleMouseLeave = () => {
setHoveredSquare(null);
};
// Event listeners
window.addEventListener("resize", resizeCanvas);
canvas.addEventListener("mousemove", handleMouseMove);
canvas.addEventListener("mouseleave", handleMouseLeave);
// Initial setup
resizeCanvas();
requestRef.current = requestAnimationFrame(updateAnimation);
// Cleanup
return () => {
window.removeEventListener("resize", resizeCanvas);
canvas.removeEventListener("mousemove", handleMouseMove);
canvas.removeEventListener("mouseleave", handleMouseLeave);
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
window.removeEventListener("resize", resize);
if (moveParticlesOnHover) {
container.removeEventListener("mousemove", handleMouseMove);
}
cancelAnimationFrame(animationFrameId);
if (container.contains(gl.canvas)) {
container.removeChild(gl.canvas);
}
};
}, [
direction,
particleCount,
particleSpread,
speed,
borderColor,
hoverFillColor,
hoveredSquare,
squareSize,
moveParticlesOnHover,
particleHoverFactor,
alphaParticles,
particleBaseSize,
sizeRandomness,
cameraDistance,
disableRotation,
]);
return (
<canvas
ref={canvasRef}
className={`w-full h-full border-none block ${className}`}
/>
<div ref={containerRef} className={`relative w-full h-full ${className}`} />
);
}
};
export default Particles;