convert-next.js
This commit is contained in:
196
src/components/ProblemParticles.tsx
Normal file
196
src/components/ProblemParticles.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
radius: number;
|
||||
alpha: number;
|
||||
type: 'lost' | 'converted' | 'regular';
|
||||
pulseSpeed: number;
|
||||
pulseTime: number;
|
||||
}
|
||||
|
||||
export default function ProblemParticles() {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
let animationFrameId = 0;
|
||||
let cssWidth = canvas.offsetWidth;
|
||||
let cssHeight = canvas.offsetHeight;
|
||||
|
||||
const particles: Particle[] = [];
|
||||
const storeNode = { x: cssWidth / 2, y: cssHeight / 2, radius: 18 };
|
||||
|
||||
const setCanvasSize = () => {
|
||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||
cssWidth = canvas.offsetWidth;
|
||||
cssHeight = canvas.offsetHeight;
|
||||
canvas.width = Math.round(cssWidth * dpr);
|
||||
canvas.height = Math.round(cssHeight * dpr);
|
||||
// Map drawing coordinates to CSS pixels
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
storeNode.x = cssWidth / 2;
|
||||
storeNode.y = cssHeight / 2;
|
||||
};
|
||||
|
||||
const createParticle = (isInitial = false): Particle => {
|
||||
const typeRand = Math.random();
|
||||
const type = typeRand < 0.2 ? 'lost' : typeRand < 0.45 ? 'converted' : 'regular';
|
||||
|
||||
let x: number, y: number;
|
||||
if (isInitial) {
|
||||
x = Math.random() * cssWidth;
|
||||
y = Math.random() * cssHeight;
|
||||
} else {
|
||||
// Spawn from edges
|
||||
if (Math.random() < 0.5) {
|
||||
x = Math.random() < 0.5 ? 0 : cssWidth;
|
||||
y = Math.random() * cssHeight;
|
||||
} else {
|
||||
x = Math.random() * cssWidth;
|
||||
y = Math.random() < 0.5 ? 0 : cssHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Vector pointing towards the store node (center)
|
||||
const angle = Math.atan2(storeNode.y - y, storeNode.x - x);
|
||||
const speed = 0.4 + Math.random() * 0.8;
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
vx: Math.cos(angle) * speed + (Math.random() - 0.5) * 0.2,
|
||||
vy: Math.sin(angle) * speed + (Math.random() - 0.5) * 0.2,
|
||||
radius: 2 + Math.random() * 4,
|
||||
alpha: 0.1 + Math.random() * 0.7,
|
||||
type,
|
||||
pulseSpeed: 0.05 + Math.random() * 0.05,
|
||||
pulseTime: Math.random() * Math.PI,
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize
|
||||
setCanvasSize();
|
||||
for (let i = 0; i < 45; i++) particles.push(createParticle(true));
|
||||
|
||||
const handleResize = () => {
|
||||
if (!canvas) return;
|
||||
setCanvasSize();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
const draw = () => {
|
||||
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
||||
|
||||
// Background glow
|
||||
const bgGlow = ctx.createRadialGradient(
|
||||
storeNode.x,
|
||||
storeNode.y,
|
||||
10,
|
||||
storeNode.x,
|
||||
storeNode.y,
|
||||
cssWidth * 0.4
|
||||
);
|
||||
bgGlow.addColorStop(0, 'rgba(0, 102, 255, 0.08)');
|
||||
bgGlow.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
ctx.fillStyle = bgGlow;
|
||||
ctx.fillRect(0, 0, cssWidth, cssHeight);
|
||||
|
||||
// Store hub
|
||||
ctx.shadowBlur = 30;
|
||||
ctx.shadowColor = '#0066ff';
|
||||
ctx.fillStyle = 'rgba(0, 102, 255, 0.9)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(storeNode.x, storeNode.y, storeNode.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(storeNode.x, storeNode.y, storeNode.radius - 6, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Particles
|
||||
particles.forEach((p, idx) => {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.pulseTime += p.pulseSpeed;
|
||||
|
||||
let alpha = p.alpha;
|
||||
let radius = p.radius;
|
||||
|
||||
if (p.type === 'converted') {
|
||||
alpha = Math.max(0.4, p.alpha + Math.sin(p.pulseTime) * 0.3);
|
||||
radius = p.radius + Math.sin(p.pulseTime) * 1.5;
|
||||
} else if (p.type === 'lost') {
|
||||
const dist = Math.hypot(storeNode.x - p.x, storeNode.y - p.y);
|
||||
if (dist < 150) p.alpha -= 0.008;
|
||||
}
|
||||
|
||||
// Connections between particles
|
||||
for (let j = idx + 1; j < particles.length; j++) {
|
||||
const p2 = particles[j];
|
||||
const dist = Math.hypot(p2.x - p.x, p2.y - p.y);
|
||||
if (dist < 80 && p.alpha > 0 && p2.alpha > 0) {
|
||||
const lineAlpha = (1 - dist / 80) * 0.18 * Math.min(alpha, p2.alpha);
|
||||
ctx.strokeStyle = `rgba(0, 102, 255, ${lineAlpha})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// Connection to store hub
|
||||
const distToStore = Math.hypot(storeNode.x - p.x, storeNode.y - p.y);
|
||||
if (distToStore < 120 && p.alpha > 0) {
|
||||
const lineAlpha = (1 - distToStore / 120) * 0.25 * p.alpha;
|
||||
ctx.strokeStyle = p.type === 'converted' ? `rgba(0, 220, 255, ${lineAlpha})` : `rgba(0, 102, 255, ${lineAlpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
ctx.lineTo(storeNode.x, storeNode.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Respawn check
|
||||
if (distToStore < storeNode.radius || p.x < 0 || p.x > cssWidth || p.y < 0 || p.y > cssHeight || p.alpha <= 0) {
|
||||
particles[idx] = createParticle();
|
||||
} else {
|
||||
ctx.fillStyle = p.type === 'converted' ? `rgba(0, 220, 255, ${alpha})` : p.type === 'lost' ? `rgba(255, 68, 68, ${alpha})` : `rgba(255, 255, 255, ${alpha})`;
|
||||
if (p.type === 'converted') {
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.shadowColor = '#00dcff';
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
});
|
||||
|
||||
animationFrameId = requestAnimationFrame(draw);
|
||||
};
|
||||
|
||||
draw();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <canvas ref={canvasRef} className="w-full h-full absolute inset-0" />;
|
||||
}
|
||||
Reference in New Issue
Block a user