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:
1 parent
90cf3c73ad
commit
780fd7c161
13 files changed
+1230
-203
No files matched your search
@@ -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<DotGridProps> = ({
|
||||
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<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const dotsRef = useRef<Dot[]>([]);
|
||||
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 (
|
||||
<section className={`p-4 flex items-center justify-center h-full w-full relative ${className}`} style={style}>
|
||||
<div ref={wrapperRef} className="w-full h-full relative">
|
||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full pointer-events-none" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default DotGrid;
|
||||
@@ -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<HTMLDivElement> {
|
||||
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<HTMLDivElement>(null);
|
||||
const programRef = useRef<Program>(null);
|
||||
const rendererRef = useRef<Renderer>(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<number>(0);
|
||||
const loadAnimationStartRef = useRef<number>(0);
|
||||
const timeOffsetRef = useRef<number>(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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`w-full h-full relative overflow-hidden ${className}`}
|
||||
style={style}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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<SquaresProps> = ({
|
||||
direction = "right",
|
||||
speed = 1,
|
||||
borderColor = "#999",
|
||||
squareSize = 40,
|
||||
hoverFillColor = "#222",
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const requestRef = useRef<number | null>(null);
|
||||
const numSquaresX = useRef<number>(0);
|
||||
const numSquaresY = useRef<number>(0);
|
||||
const gridOffset = useRef<GridOffset>({ x: 0, y: 0 });
|
||||
const hoveredSquareRef = useRef<GridOffset | null>(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 (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full border-none block"
|
||||
></canvas>
|
||||
);
|
||||
};
|
||||
|
||||
export default Squares;
|
||||
@@ -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
@@ -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;
|
||||
Reference in new issue
Block a user