diff --git a/app/components/ContactForm.tsx b/app/components/ContactForm.tsx index 39e5ada..af22181 100644 --- a/app/components/ContactForm.tsx +++ b/app/components/ContactForm.tsx @@ -53,67 +53,95 @@ export function ContactForm({ formType, setFormType }: ContactFormProps) { const getGreetingMessageWithUserName = () => { const hour = new Date().getHours(); + const dayPart = + hour >= 8 && hour < 12 + ? "manhã" + : hour >= 12 && hour < 18 + ? "tarde" + : "noite"; - const baseMessage = (name: string) => { - if (hour >= 8 && hour < 12) { - // Manhã - return `Bom dia Evandro, meu nome é ${name}. Vi seu portfólio e estou interessado nos seus serviços. Podemos conversar?`; - } else if (hour >= 13 && hour < 18) { - // Tarde - return `Boa tarde Evandro, meu nome é ${name}. Vi seu portfólio e estou interessado nos seus serviços. Podemos conversar?`; - } else { - // Noite - return `Olá Evandro, meu nome é ${name}. Vi seu portfólio e estou interessado nos seus serviços. Podemos conversar quando estiver disponível?`; - } - }; - return baseMessage(userName); + return `Olá Evandro, espero que esta ${dayPart} esteja sendo produtiva. + +Meu nome é ${userName} e entrei em contato pois vi seu portfólio e identifiquei que suas habilidades como Desenvolvedor Full-Stack podem agregar muito ao projeto que tenho em mente. + +Gostaria de agendar uma conversa para discutirmos detalhes e entender como podemos trabalhar juntos de forma eficiente e produtiva. + +Fico no aguardo do seu contato para marcarmos um horário que seja conveniente para ambos. + +Atenciosamente, +${userName}`; }; const generateAdvancedMessage = () => { if (customMessage) return customMessage; - let message = `Olá Evandro,\n\n`; + // Saudação profissional + let message = `Prezado Evandro,\n\n`; + + // Informações do contato message += `Meu nome é ${userName}`; if (userCompany) { - message += ` e sou de ${userCompany}`; - } - if (howFound) { - message += `. Conheci seu trabalho via ${howFound}`; - } - message += `.\n\n`; - - message += `Estou entrando em contato para discutir um projeto de `; - if (projectType === "outro" && otherProjectType) { - message += `${otherProjectType}`; - } else if (projectType) { - message += `${projectType}`; - } else { - message += `[Tipo de Projeto Não Especificado]`; + message += `, represento a empresa ${userCompany}`; } message += `.\n`; - if (desiredDeadline) { - message += `O prazo desejado é ${desiredDeadline}.\n`; - } - if (budgetValue) { - message += `O orçamento disponível é ${budgetValue}.\n`; - } - if (technicalComplexity) { - message += `A complexidade técnica estimada é ${technicalComplexity}.\n`; - } - if (hasInternalTeam) { - message += `Já possuímos uma equipe técnica ${hasInternalTeam}.\n`; + if (howFound) { + message += `Conheci seu trabalho através de ${howFound}.\n`; } - message += `\nMeus contatos:\n`; + message += `\n`; + + // Detalhes do projeto + message += `=== DETALHES DO PROJETO ===\n`; + message += `Tipo de projeto: `; + if (projectType === "outro" && otherProjectType) { + message += `${otherProjectType}\n`; + } else if (projectType) { + message += `${projectType}\n`; + } else { + message += `Não especificado\n`; + } + + if (desiredDeadline) { + message += `Prazo desejado: ${desiredDeadline}\n`; + } + + if (budgetValue) { + message += `Orçamento disponível: ${budgetValue}\n`; + } + + if (technicalComplexity) { + message += `Complexidade técnica estimada: ${technicalComplexity}\n`; + } + + if (hasInternalTeam) { + message += `Equipe técnica interna: ${ + hasInternalTeam === "sim" + ? "Sim" + : hasInternalTeam === "nao" + ? "Não" + : "Parcialmente" + }\n`; + } + + message += `\n`; + + // Contatos + message += `=== INFORMAÇÕES PARA CONTATO ===\n`; if (userEmail) { - message += `Email: ${userEmail}\n`; + message += `📧 Email: ${userEmail}\n`; } if (userPhone) { - message += `Telefone: ${userPhone}\n`; + message += `📱 Telefone/WhatsApp: ${userPhone}\n`; } - message += `\nAguardamos seu retorno para discutir os próximos passos.`; + message += `\n`; + + // Mensagem final + message += `Estou entrando em contato pois acredito que seus conhecimentos como Desenvolvedor Full-Stack podem agregar muito ao projeto que tenho em mente.\n\n`; + message += `Fico no aguardo do seu contato para discutirmos detalhes e definirmos os próximos passos.\n\n`; + message += `Atenciosamente,\n`; + message += `${userName}`; return message; }; diff --git a/app/components/Experience.tsx b/app/components/Experience.tsx index 8a65dc9..6b67aa0 100644 --- a/app/components/Experience.tsx +++ b/app/components/Experience.tsx @@ -36,7 +36,7 @@ export default function Experience() { return (
Especialista em soluções web modernas e inovadoras. Formado em - Engenharia da Computação, com conhecimentos sólidos em computação em - nuvem e inteligência artificial. + Engenharia da Computação, com conhecimentos em computação em nuvem e + inteligência artificial. {/* Location */} diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index fe96e4d..24cba8f 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -34,7 +34,7 @@ export default function Projects() { const projects = data.projects.filter((project) => project.featured); return ( -
+
-
+
{WorkModalitiesData.map((modality, index) => ( import("./components/Hero"), { @@ -55,14 +56,15 @@ const Contact = dynamic(() => import("./components/Contact"), { export default function Home() { return (
- +
+ +
diff --git a/components/DotGrid.tsx b/components/DotGrid.tsx new file mode 100644 index 0000000..c99c7d3 --- /dev/null +++ b/components/DotGrid.tsx @@ -0,0 +1,290 @@ +'use client'; +import React, { useRef, useEffect, useCallback, useMemo } from 'react'; +import { gsap } from 'gsap'; +import { InertiaPlugin } from 'gsap/InertiaPlugin'; + +gsap.registerPlugin(InertiaPlugin); + +const throttle = (func: (...args: any[]) => void, limit: number) => { + let lastCall = 0; + return function (this: any, ...args: any[]) { + const now = performance.now(); + if (now - lastCall >= limit) { + lastCall = now; + func.apply(this, args); + } + }; +}; + +interface Dot { + cx: number; + cy: number; + xOffset: number; + yOffset: number; + _inertiaApplied: boolean; +} + +export interface DotGridProps { + dotSize?: number; + gap?: number; + baseColor?: string; + activeColor?: string; + proximity?: number; + speedTrigger?: number; + shockRadius?: number; + shockStrength?: number; + maxSpeed?: number; + resistance?: number; + returnDuration?: number; + className?: string; + style?: React.CSSProperties; +} + +function hexToRgb(hex: string) { + const m = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i); + if (!m) return { r: 0, g: 0, b: 0 }; + return { + r: parseInt(m[1], 16), + g: parseInt(m[2], 16), + b: parseInt(m[3], 16) + }; +} + +const DotGrid: React.FC = ({ + dotSize = 16, + gap = 32, + baseColor = '#5227FF', + activeColor = '#5227FF', + proximity = 150, + speedTrigger = 100, + shockRadius = 250, + shockStrength = 5, + maxSpeed = 5000, + resistance = 750, + returnDuration = 1.5, + className = '', + style +}) => { + const wrapperRef = useRef(null); + const canvasRef = useRef(null); + const dotsRef = useRef([]); + const pointerRef = useRef({ + x: 0, + y: 0, + vx: 0, + vy: 0, + speed: 0, + lastTime: 0, + lastX: 0, + lastY: 0 + }); + + const baseRgb = useMemo(() => hexToRgb(baseColor), [baseColor]); + const activeRgb = useMemo(() => hexToRgb(activeColor), [activeColor]); + + const circlePath = useMemo(() => { + if (typeof window === 'undefined' || !window.Path2D) return null; + + const p = new Path2D(); + p.arc(0, 0, dotSize / 2, 0, Math.PI * 2); + return p; + }, [dotSize]); + + const buildGrid = useCallback(() => { + const wrap = wrapperRef.current; + const canvas = canvasRef.current; + if (!wrap || !canvas) return; + + const { width, height } = wrap.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + + canvas.width = width * dpr; + canvas.height = height * dpr; + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + const ctx = canvas.getContext('2d'); + if (ctx) ctx.scale(dpr, dpr); + + const cols = Math.floor((width + gap) / (dotSize + gap)); + const rows = Math.floor((height + gap) / (dotSize + gap)); + const cell = dotSize + gap; + + const gridW = cell * cols - gap; + const gridH = cell * rows - gap; + + const extraX = width - gridW; + const extraY = height - gridH; + + const startX = extraX / 2 + dotSize / 2; + const startY = extraY / 2 + dotSize / 2; + + const dots: Dot[] = []; + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const cx = startX + x * cell; + const cy = startY + y * cell; + dots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false }); + } + } + dotsRef.current = dots; + }, [dotSize, gap]); + + useEffect(() => { + if (!circlePath) return; + + let rafId: number; + const proxSq = proximity * proximity; + + const draw = () => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const { x: px, y: py } = pointerRef.current; + + for (const dot of dotsRef.current) { + const ox = dot.cx + dot.xOffset; + const oy = dot.cy + dot.yOffset; + const dx = dot.cx - px; + const dy = dot.cy - py; + const dsq = dx * dx + dy * dy; + + let style = baseColor; + if (dsq <= proxSq) { + const dist = Math.sqrt(dsq); + const t = 1 - dist / proximity; + const r = Math.round(baseRgb.r + (activeRgb.r - baseRgb.r) * t); + const g = Math.round(baseRgb.g + (activeRgb.g - baseRgb.g) * t); + const b = Math.round(baseRgb.b + (activeRgb.b - baseRgb.b) * t); + style = `rgb(${r},${g},${b})`; + } + + ctx.save(); + ctx.translate(ox, oy); + ctx.fillStyle = style; + ctx.fill(circlePath); + ctx.restore(); + } + + rafId = requestAnimationFrame(draw); + }; + + draw(); + return () => cancelAnimationFrame(rafId); + }, [proximity, baseColor, activeRgb, baseRgb, circlePath]); + + useEffect(() => { + buildGrid(); + let ro: ResizeObserver | null = null; + if ('ResizeObserver' in window) { + ro = new ResizeObserver(buildGrid); + wrapperRef.current && ro.observe(wrapperRef.current); + } else { + (window as Window).addEventListener('resize', buildGrid); + } + return () => { + if (ro) ro.disconnect(); + else window.removeEventListener('resize', buildGrid); + }; + }, [buildGrid]); + + useEffect(() => { + const onMove = (e: MouseEvent) => { + const now = performance.now(); + const pr = pointerRef.current; + const dt = pr.lastTime ? now - pr.lastTime : 16; + const dx = e.clientX - pr.lastX; + const dy = e.clientY - pr.lastY; + let vx = (dx / dt) * 1000; + let vy = (dy / dt) * 1000; + let speed = Math.hypot(vx, vy); + if (speed > maxSpeed) { + const scale = maxSpeed / speed; + vx *= scale; + vy *= scale; + speed = maxSpeed; + } + pr.lastTime = now; + pr.lastX = e.clientX; + pr.lastY = e.clientY; + pr.vx = vx; + pr.vy = vy; + pr.speed = speed; + + const rect = canvasRef.current!.getBoundingClientRect(); + pr.x = e.clientX - rect.left; + pr.y = e.clientY - rect.top; + + for (const dot of dotsRef.current) { + const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y); + if (speed > speedTrigger && dist < proximity && !dot._inertiaApplied) { + dot._inertiaApplied = true; + gsap.killTweensOf(dot); + const pushX = dot.cx - pr.x + vx * 0.005; + const pushY = dot.cy - pr.y + vy * 0.005; + gsap.to(dot, { + inertia: { xOffset: pushX, yOffset: pushY, resistance }, + onComplete: () => { + gsap.to(dot, { + xOffset: 0, + yOffset: 0, + duration: returnDuration, + ease: 'elastic.out(1,0.75)' + }); + dot._inertiaApplied = false; + } + }); + } + } + }; + + const onClick = (e: MouseEvent) => { + const rect = canvasRef.current!.getBoundingClientRect(); + const cx = e.clientX - rect.left; + const cy = e.clientY - rect.top; + for (const dot of dotsRef.current) { + const dist = Math.hypot(dot.cx - cx, dot.cy - cy); + if (dist < shockRadius && !dot._inertiaApplied) { + dot._inertiaApplied = true; + gsap.killTweensOf(dot); + const falloff = Math.max(0, 1 - dist / shockRadius); + const pushX = (dot.cx - cx) * shockStrength * falloff; + const pushY = (dot.cy - cy) * shockStrength * falloff; + gsap.to(dot, { + inertia: { xOffset: pushX, yOffset: pushY, resistance }, + onComplete: () => { + gsap.to(dot, { + xOffset: 0, + yOffset: 0, + duration: returnDuration, + ease: 'elastic.out(1,0.75)' + }); + dot._inertiaApplied = false; + } + }); + } + } + }; + + const throttledMove = throttle(onMove, 50); + window.addEventListener('mousemove', throttledMove, { passive: true }); + window.addEventListener('click', onClick); + + return () => { + window.removeEventListener('mousemove', throttledMove); + window.removeEventListener('click', onClick); + }; + }, [maxSpeed, speedTrigger, proximity, resistance, returnDuration, shockRadius, shockStrength]); + + return ( +
+
+ +
+
+ ); +}; + +export default DotGrid; diff --git a/components/FaultyTerminal.tsx b/components/FaultyTerminal.tsx new file mode 100644 index 0000000..d4e5bc0 --- /dev/null +++ b/components/FaultyTerminal.tsx @@ -0,0 +1,445 @@ +"use client"; +import { Renderer, Program, Mesh, Color, Triangle } from "ogl"; +import React, { useEffect, useRef, useMemo, useCallback } from "react"; + +type Vec2 = [number, number]; + +export interface FaultyTerminalProps + extends React.HTMLAttributes { + scale?: number; + gridMul?: Vec2; + digitSize?: number; + timeScale?: number; + pause?: boolean; + scanlineIntensity?: number; + glitchAmount?: number; + flickerAmount?: number; + noiseAmp?: number; + chromaticAberration?: number; + dither?: number | boolean; + curvature?: number; + tint?: string; + mouseReact?: boolean; + mouseStrength?: number; + dpr?: number; + pageLoadAnimation?: boolean; + brightness?: number; +} + +const vertexShader = ` +attribute vec2 position; +attribute vec2 uv; +varying vec2 vUv; +void main() { + vUv = uv; + gl_Position = vec4(position, 0.0, 1.0); +} +`; + +const fragmentShader = ` +precision mediump float; + +varying vec2 vUv; + +uniform float iTime; +uniform vec3 iResolution; +uniform float uScale; + +uniform vec2 uGridMul; +uniform float uDigitSize; +uniform float uScanlineIntensity; +uniform float uGlitchAmount; +uniform float uFlickerAmount; +uniform float uNoiseAmp; +uniform float uChromaticAberration; +uniform float uDither; +uniform float uCurvature; +uniform vec3 uTint; +uniform vec2 uMouse; +uniform float uMouseStrength; +uniform float uUseMouse; +uniform float uPageLoadProgress; +uniform float uUsePageLoadAnimation; +uniform float uBrightness; + +float time; + +float hash21(vec2 p){ + p = fract(p * 234.56); + p += dot(p, p + 34.56); + return fract(p.x * p.y); +} + +float noise(vec2 p) +{ + return sin(p.x * 10.0) * sin(p.y * (3.0 + sin(time * 0.090909))) + 0.2; +} + +mat2 rotate(float angle) +{ + float c = cos(angle); + float s = sin(angle); + return mat2(c, -s, s, c); +} + +float fbm(vec2 p) +{ + p *= 1.1; + float f = 0.0; + float amp = 0.5 * uNoiseAmp; + + mat2 modify0 = rotate(time * 0.02); + f += amp * noise(p); + p = modify0 * p * 2.0; + amp *= 0.454545; + + mat2 modify1 = rotate(time * 0.02); + f += amp * noise(p); + p = modify1 * p * 2.0; + amp *= 0.454545; + + mat2 modify2 = rotate(time * 0.08); + f += amp * noise(p); + + return f; +} + +float pattern(vec2 p, out vec2 q, out vec2 r) { + vec2 offset1 = vec2(1.0); + vec2 offset0 = vec2(0.0); + mat2 rot01 = rotate(0.1 * time); + mat2 rot1 = rotate(0.1); + + q = vec2(fbm(p + offset1), fbm(rot01 * p + offset1)); + r = vec2(fbm(rot1 * q + offset0), fbm(q + offset0)); + return fbm(p + r); +} + +float digit(vec2 p){ + vec2 grid = uGridMul * 15.0; + vec2 s = floor(p * grid) / grid; + p = p * grid; + vec2 q, r; + float intensity = pattern(s * 0.1, q, r) * 1.3 - 0.03; + + if(uUseMouse > 0.5){ + vec2 mouseWorld = uMouse * uScale; + float distToMouse = distance(s, mouseWorld); + float mouseInfluence = exp(-distToMouse * 8.0) * uMouseStrength * 10.0; + intensity += mouseInfluence; + + float ripple = sin(distToMouse * 20.0 - iTime * 5.0) * 0.1 * mouseInfluence; + intensity += ripple; + } + + if(uUsePageLoadAnimation > 0.5){ + float cellRandom = fract(sin(dot(s, vec2(12.9898, 78.233))) * 43758.5453); + float cellDelay = cellRandom * 0.8; + float cellProgress = clamp((uPageLoadProgress - cellDelay) / 0.2, 0.0, 1.0); + + float fadeAlpha = smoothstep(0.0, 1.0, cellProgress); + intensity *= fadeAlpha; + } + + p = fract(p); + p *= uDigitSize; + + float px5 = p.x * 5.0; + float py5 = (1.0 - p.y) * 5.0; + float x = fract(px5); + float y = fract(py5); + + float i = floor(py5) - 2.0; + float j = floor(px5) - 2.0; + float n = i * i + j * j; + float f = n * 0.0625; + + float isOn = step(0.1, intensity - f); + float brightness = isOn * (0.2 + y * 0.8) * (0.75 + x * 0.25); + + return step(0.0, p.x) * step(p.x, 1.0) * step(0.0, p.y) * step(p.y, 1.0) * brightness; +} + +float onOff(float a, float b, float c) +{ + return step(c, sin(iTime + a * cos(iTime * b))) * uFlickerAmount; +} + +float displace(vec2 look) +{ + float y = look.y - mod(iTime * 0.25, 1.0); + float window = 1.0 / (1.0 + 50.0 * y * y); + return sin(look.y * 20.0 + iTime) * 0.0125 * onOff(4.0, 2.0, 0.8) * (1.0 + cos(iTime * 60.0)) * window; +} + +vec3 getColor(vec2 p){ + + float bar = step(mod(p.y + time * 20.0, 1.0), 0.2) * 0.4 + 1.0; + bar *= uScanlineIntensity; + + float displacement = displace(p); + p.x += displacement; + + if (uGlitchAmount != 1.0) { + float extra = displacement * (uGlitchAmount - 1.0); + p.x += extra; + } + + float middle = digit(p); + + const float off = 0.002; + float sum = digit(p + vec2(-off, -off)) + digit(p + vec2(0.0, -off)) + digit(p + vec2(off, -off)) + + digit(p + vec2(-off, 0.0)) + digit(p + vec2(0.0, 0.0)) + digit(p + vec2(off, 0.0)) + + digit(p + vec2(-off, off)) + digit(p + vec2(0.0, off)) + digit(p + vec2(off, off)); + + vec3 baseColor = vec3(0.9) * middle + sum * 0.1 * vec3(1.0) * bar; + return baseColor; +} + +vec2 barrel(vec2 uv){ + vec2 c = uv * 2.0 - 1.0; + float r2 = dot(c, c); + c *= 1.0 + uCurvature * r2; + return c * 0.5 + 0.5; +} + +void main() { + time = iTime * 0.333333; + vec2 uv = vUv; + + if(uCurvature != 0.0){ + uv = barrel(uv); + } + + vec2 p = uv * uScale; + vec3 col = getColor(p); + + if(uChromaticAberration != 0.0){ + vec2 ca = vec2(uChromaticAberration) / iResolution.xy; + col.r = getColor(p + ca).r; + col.b = getColor(p - ca).b; + } + + col *= uTint; + col *= uBrightness; + + if(uDither > 0.0){ + float rnd = hash21(gl_FragCoord.xy); + col += (rnd - 0.5) * (uDither * 0.003922); + } + + gl_FragColor = vec4(col, 1.0); +} +`; + +function hexToRgb(hex: string): [number, number, number] { + let h = hex.replace("#", "").trim(); + if (h.length === 3) + h = h + .split("") + .map((c) => c + c) + .join(""); + const num = parseInt(h, 16); + return [ + ((num >> 16) & 255) / 255, + ((num >> 8) & 255) / 255, + (num & 255) / 255, + ]; +} + +export default function FaultyTerminal({ + scale = 1, + gridMul = [2, 1], + digitSize = 1.5, + timeScale = 0.3, + pause = false, + scanlineIntensity = 0.3, + glitchAmount = 1, + flickerAmount = 1, + noiseAmp = 1, + chromaticAberration = 0, + dither = 0, + curvature = 0.2, + tint = "#ffffff", + mouseReact = true, + mouseStrength = 0.2, + dpr = Math.min(window.devicePixelRatio || 1, 2), + pageLoadAnimation = true, + brightness = 1, + className, + style, + ...rest +}: FaultyTerminalProps) { + const containerRef = useRef(null); + const programRef = useRef(null); + const rendererRef = useRef(null); + const mouseRef = useRef({ x: 0.5, y: 0.5 }); + const smoothMouseRef = useRef({ x: 0.5, y: 0.5 }); + const frozenTimeRef = useRef(0); + const rafRef = useRef(0); + const loadAnimationStartRef = useRef(0); + const timeOffsetRef = useRef(Math.random() * 100); + + const tintVec = useMemo(() => hexToRgb(tint), [tint]); + + const ditherValue = useMemo( + () => (typeof dither === "boolean" ? (dither ? 1 : 0) : dither), + [dither] + ); + + const handleMouseMove = useCallback((e: MouseEvent) => { + const ctn = containerRef.current; + if (!ctn) return; + const rect = ctn.getBoundingClientRect(); + const x = (e.clientX - rect.left) / rect.width; + const y = 1 - (e.clientY - rect.top) / rect.height; + mouseRef.current = { x, y }; + }, []); + + useEffect(() => { + const ctn = containerRef.current; + if (!ctn) return; + + const renderer = new Renderer({ dpr }); + rendererRef.current = renderer; + const gl = renderer.gl; + gl.clearColor(0, 0, 0, 1); + + const geometry = new Triangle(gl); + + const program = new Program(gl, { + vertex: vertexShader, + fragment: fragmentShader, + uniforms: { + iTime: { value: 0 }, + iResolution: { + value: new Color( + gl.canvas.width, + gl.canvas.height, + gl.canvas.width / gl.canvas.height + ), + }, + uScale: { value: scale }, + + uGridMul: { value: new Float32Array(gridMul) }, + uDigitSize: { value: digitSize }, + uScanlineIntensity: { value: scanlineIntensity }, + uGlitchAmount: { value: glitchAmount }, + uFlickerAmount: { value: flickerAmount }, + uNoiseAmp: { value: noiseAmp }, + uChromaticAberration: { value: chromaticAberration }, + uDither: { value: ditherValue }, + uCurvature: { value: curvature }, + uTint: { value: new Color(tintVec[0], tintVec[1], tintVec[2]) }, + uMouse: { + value: new Float32Array([ + smoothMouseRef.current.x, + smoothMouseRef.current.y, + ]), + }, + uMouseStrength: { value: mouseStrength }, + uUseMouse: { value: mouseReact ? 1 : 0 }, + uPageLoadProgress: { value: pageLoadAnimation ? 0 : 1 }, + uUsePageLoadAnimation: { value: pageLoadAnimation ? 1 : 0 }, + uBrightness: { value: brightness }, + }, + }); + programRef.current = program; + + const mesh = new Mesh(gl, { geometry, program }); + + function resize() { + if (!ctn || !renderer) return; + renderer.setSize(ctn.offsetWidth, ctn.offsetHeight); + program.uniforms.iResolution.value = new Color( + gl.canvas.width, + gl.canvas.height, + gl.canvas.width / gl.canvas.height + ); + } + + const resizeObserver = new ResizeObserver(() => resize()); + resizeObserver.observe(ctn); + resize(); + + const update = (t: number) => { + rafRef.current = requestAnimationFrame(update); + + if (pageLoadAnimation && loadAnimationStartRef.current === 0) { + loadAnimationStartRef.current = t; + } + + if (!pause) { + const elapsed = (t * 0.001 + timeOffsetRef.current) * timeScale; + program.uniforms.iTime.value = elapsed; + frozenTimeRef.current = elapsed; + } else { + program.uniforms.iTime.value = frozenTimeRef.current; + } + + if (pageLoadAnimation && loadAnimationStartRef.current > 0) { + const animationDuration = 2000; + const animationElapsed = t - loadAnimationStartRef.current; + const progress = Math.min(animationElapsed / animationDuration, 1); + program.uniforms.uPageLoadProgress.value = progress; + } + + if (mouseReact) { + const dampingFactor = 0.08; + const smoothMouse = smoothMouseRef.current; + const mouse = mouseRef.current; + smoothMouse.x += (mouse.x - smoothMouse.x) * dampingFactor; + smoothMouse.y += (mouse.y - smoothMouse.y) * dampingFactor; + + const mouseUniform = program.uniforms.uMouse.value as Float32Array; + mouseUniform[0] = smoothMouse.x; + mouseUniform[1] = smoothMouse.y; + } + + renderer.render({ scene: mesh }); + }; + rafRef.current = requestAnimationFrame(update); + ctn.appendChild(gl.canvas); + + if (mouseReact) ctn.addEventListener("mousemove", handleMouseMove); + + return () => { + cancelAnimationFrame(rafRef.current); + resizeObserver.disconnect(); + if (mouseReact) ctn.removeEventListener("mousemove", handleMouseMove); + if (gl.canvas.parentElement === ctn) ctn.removeChild(gl.canvas); + gl.getExtension("WEBGL_lose_context")?.loseContext(); + loadAnimationStartRef.current = 0; + timeOffsetRef.current = Math.random() * 100; + }; + }, [ + dpr, + pause, + timeScale, + scale, + gridMul, + digitSize, + scanlineIntensity, + glitchAmount, + flickerAmount, + noiseAmp, + chromaticAberration, + ditherValue, + curvature, + tintVec, + mouseReact, + mouseStrength, + pageLoadAnimation, + brightness, + handleMouseMove, + ]); + + return ( +
+ ); +} diff --git a/components/Squares.tsx b/components/Squares.tsx new file mode 100644 index 0000000..901585c --- /dev/null +++ b/components/Squares.tsx @@ -0,0 +1,172 @@ +"use client"; +import React, { useRef, useEffect } from "react"; + +type CanvasStrokeStyle = string | CanvasGradient | CanvasPattern; + +interface GridOffset { + x: number; + y: number; +} + +interface SquaresProps { + direction?: "diagonal" | "up" | "right" | "down" | "left"; + speed?: number; + borderColor?: CanvasStrokeStyle; + squareSize?: number; + hoverFillColor?: CanvasStrokeStyle; +} + +const Squares: React.FC = ({ + direction = "right", + speed = 1, + borderColor = "#999", + squareSize = 40, + hoverFillColor = "#222", +}) => { + const canvasRef = useRef(null); + const requestRef = useRef(null); + const numSquaresX = useRef(0); + const numSquaresY = useRef(0); + const gridOffset = useRef({ x: 0, y: 0 }); + const hoveredSquareRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + + 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; + }; + + window.addEventListener("resize", resizeCanvas); + resizeCanvas(); + + const drawGrid = () => { + if (!ctx) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize; + const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize; + + 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); + + if ( + hoveredSquareRef.current && + Math.floor((x - startX) / squareSize) === + hoveredSquareRef.current.x && + Math.floor((y - startY) / squareSize) === hoveredSquareRef.current.y + ) { + ctx.fillStyle = hoverFillColor; + ctx.fillRect(squareX, squareY, squareSize, squareSize); + } + + ctx.strokeStyle = borderColor; + ctx.strokeRect(squareX, squareY, squareSize, squareSize); + } + } + + const gradient = ctx.createRadialGradient( + canvas.width / 2, + canvas.height / 2, + 0, + canvas.width / 2, + canvas.height / 2, + Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2 + ); + gradient.addColorStop(0, "rgba(0, 0, 0, 0)"); + gradient.addColorStop(1, "#060010"); + + 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; + default: + break; + } + + drawGrid(); + requestRef.current = requestAnimationFrame(updateAnimation); + }; + + const handleMouseMove = (event: MouseEvent) => { + const rect = canvas.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; + const mouseY = event.clientY - rect.top; + + 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 + ); + + if ( + !hoveredSquareRef.current || + hoveredSquareRef.current.x !== hoveredSquareX || + hoveredSquareRef.current.y !== hoveredSquareY + ) { + hoveredSquareRef.current = { x: hoveredSquareX, y: hoveredSquareY }; + } + }; + + const handleMouseLeave = () => { + hoveredSquareRef.current = null; + }; + + canvas.addEventListener("mousemove", handleMouseMove); + canvas.addEventListener("mouseleave", handleMouseLeave); + requestRef.current = requestAnimationFrame(updateAnimation); + + return () => { + window.removeEventListener("resize", resizeCanvas); + if (requestRef.current) cancelAnimationFrame(requestRef.current); + canvas.removeEventListener("mousemove", handleMouseMove); + canvas.removeEventListener("mouseleave", handleMouseLeave); + }; + }, [direction, speed, borderColor, hoverFillColor, squareSize]); + + return ( + + ); +}; + +export default Squares; diff --git a/components/magicui/bento-grid.tsx b/components/magicui/bento-grid.tsx index 2e7dd2b..70c8ab0 100644 --- a/components/magicui/bento-grid.tsx +++ b/components/magicui/bento-grid.tsx @@ -19,7 +19,7 @@ export function BentoGrid({ children, className }: BentoGridProps) { return (
diff --git a/components/magicui/squares.tsx b/components/magicui/squares.tsx index 6f7b9e0..a3f441e 100644 --- a/components/magicui/squares.tsx +++ b/components/magicui/squares.tsx @@ -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 = ({ + 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(null); - const requestRef = useRef(undefined); - const numSquaresX = useRef(undefined); - const numSquaresY = useRef(undefined); - const gridOffset = useRef({ x: 0, y: 0 }); - const [hoveredSquare, setHoveredSquare] = useState<{ - x: number; - y: number; - } | null>(null); +}) => { + const containerRef = useRef(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 ( - +
); -} +}; + +export default Particles; diff --git a/package.json b/package.json index 19e335a..6c7085d 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cobe": "^0.6.4", + "gsap": "^3.13.0", "lucide-react": "^0.542.0", "motion": "^12.23.12", "next": "15.5.2", + "ogl": "^1.0.11", "react": "19.1.0", "react-dom": "19.1.0", "tailwind-merge": "^3.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d84e11..b8ff5b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: cobe: specifier: ^0.6.4 version: 0.6.4 + gsap: + specifier: ^3.13.0 + version: 3.13.0 lucide-react: specifier: ^0.542.0 version: 0.542.0(react@19.1.0) @@ -26,6 +29,9 @@ importers: next: specifier: 15.5.2 version: 15.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + ogl: + specifier: ^1.0.11 + version: 1.0.11 react: specifier: 19.1.0 version: 19.1.0 @@ -483,6 +489,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gsap@3.13.0: + resolution: {integrity: sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} @@ -621,6 +630,9 @@ packages: sass: optional: true + ogl@1.0.11: + resolution: {integrity: sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==} + phenomenon@1.6.0: resolution: {integrity: sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==} @@ -1036,6 +1048,8 @@ snapshots: graceful-fs@4.2.11: {} + gsap@3.13.0: {} + is-arrayish@0.3.2: optional: true @@ -1141,6 +1155,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + ogl@1.0.11: {} + phenomenon@1.6.0: {} picocolors@1.1.1: {}