зеркало из
https://github.com/kodackx/disinformation-quest.git
synced 2025-10-29 04:44:15 +02:00
303 строки
10 KiB
TypeScript
303 строки
10 KiB
TypeScript
import { useEffect, useRef, useState, useCallback } from "react";
|
|
import { startBackgroundMusic } from "@/utils/audio";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface GameBackgroundProps {
|
|
shouldStartAudio?: boolean;
|
|
intensity?: number; // 0-100, represents how much the 2+2=5 has spread
|
|
}
|
|
|
|
interface Particle {
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
size: number;
|
|
speed: number;
|
|
text: string;
|
|
opacity: number;
|
|
rotation: number;
|
|
rotationSpeed: number;
|
|
color: string;
|
|
}
|
|
|
|
interface Connection {
|
|
startNode: number;
|
|
endNode: number;
|
|
strength: number;
|
|
active: boolean;
|
|
pulsePosition: number;
|
|
pulseSpeed: number;
|
|
}
|
|
|
|
export const GameBackground = ({
|
|
shouldStartAudio = false,
|
|
intensity = 30
|
|
}: GameBackgroundProps) => {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const requestRef = useRef<number>();
|
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
|
const [isMouseInCanvas, setIsMouseInCanvas] = useState(false);
|
|
|
|
// Nodes and connections for the neural network visualization
|
|
const [nodes, setNodes] = useState<Particle[]>([]);
|
|
const [connections, setConnections] = useState<Connection[]>([]);
|
|
|
|
// Initialize dimensions and event listeners
|
|
useEffect(() => {
|
|
const updateDimensions = () => {
|
|
if (canvasRef.current) {
|
|
const { width, height } = canvasRef.current.getBoundingClientRect();
|
|
setDimensions({ width, height });
|
|
canvasRef.current.width = width;
|
|
canvasRef.current.height = height;
|
|
}
|
|
};
|
|
|
|
updateDimensions();
|
|
window.addEventListener('resize', updateDimensions);
|
|
|
|
return () => {
|
|
window.removeEventListener('resize', updateDimensions);
|
|
};
|
|
}, []);
|
|
|
|
// Initialize background music
|
|
useEffect(() => {
|
|
if (shouldStartAudio) {
|
|
startBackgroundMusic();
|
|
}
|
|
}, [shouldStartAudio]);
|
|
|
|
// Initialize particles and connections
|
|
useEffect(() => {
|
|
if (dimensions.width === 0 || dimensions.height === 0) return;
|
|
|
|
// Create particles based on screen size
|
|
const particleCount = Math.min(Math.max(10, Math.floor(dimensions.width * dimensions.height / 40000)), 40);
|
|
const newParticles: Particle[] = [];
|
|
|
|
// Create network nodes
|
|
for (let i = 0; i < particleCount; i++) {
|
|
const isTruth = Math.random() > intensity / 100;
|
|
newParticles.push({
|
|
x: Math.random() * dimensions.width,
|
|
y: Math.random() * dimensions.height,
|
|
z: Math.random() * 3 + 0.1, // For depth perception
|
|
size: Math.random() * 6 + 15, // Reduced size
|
|
speed: (Math.random() * 0.15 + 0.03) / 10, // Further reduced speed
|
|
text: isTruth ? "2+2=4" : Math.random() > 0.3 ? "2+2=5" : "5",
|
|
opacity: Math.random() * 0.15 + 0.03, // Much lower opacity
|
|
rotation: Math.random() * 360,
|
|
rotationSpeed: (Math.random() - 0.5) * 0.03, // Even slower rotation
|
|
color: isTruth ? "rgba(59, 130, 246, 0.35)" : "rgba(234, 179, 8, 0.35)" // More transparent
|
|
});
|
|
}
|
|
|
|
setNodes(newParticles);
|
|
|
|
// Create connections between nodes
|
|
const maxConnections = particleCount * 1.5; // Fewer connections
|
|
const newConnections: Connection[] = [];
|
|
|
|
for (let i = 0; i < maxConnections; i++) {
|
|
const startNode = Math.floor(Math.random() * particleCount);
|
|
let endNode = Math.floor(Math.random() * particleCount);
|
|
// Avoid self-connections
|
|
while (endNode === startNode) {
|
|
endNode = Math.floor(Math.random() * particleCount);
|
|
}
|
|
|
|
newConnections.push({
|
|
startNode,
|
|
endNode,
|
|
strength: Math.random() * 0.6 + 0.1, // Lower strength
|
|
active: Math.random() < 0.2, // Fewer active connections
|
|
pulsePosition: 0,
|
|
pulseSpeed: Math.random() * 0.01 + 0.002 // Slower pulse speed
|
|
});
|
|
}
|
|
|
|
setConnections(newConnections);
|
|
}, [dimensions, intensity]);
|
|
|
|
// Animation loop
|
|
const animate = useCallback(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
// Clear canvas
|
|
ctx.clearRect(0, 0, dimensions.width, dimensions.height);
|
|
|
|
// Update and draw connections
|
|
const updatedConnections = connections.map(connection => {
|
|
const startNode = nodes[connection.startNode];
|
|
const endNode = nodes[connection.endNode];
|
|
|
|
if (!startNode || !endNode) return connection;
|
|
|
|
// Only draw if both nodes are visible enough
|
|
if (startNode.opacity > 0.05 && endNode.opacity > 0.05) {
|
|
const startX = startNode.x;
|
|
const startY = startNode.y;
|
|
const endX = endNode.x;
|
|
const endY = endNode.y;
|
|
|
|
// Draw connection line with even lower opacity
|
|
ctx.beginPath();
|
|
ctx.moveTo(startX, startY);
|
|
ctx.lineTo(endX, endY);
|
|
|
|
const distance = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2));
|
|
const opacity = Math.max(0, 0.15 - distance / 1000) * connection.strength; // Reduced opacity
|
|
|
|
ctx.strokeStyle = `rgba(180, 180, 180, ${opacity * 0.15})`; // Much lower opacity
|
|
ctx.lineWidth = 0.3; // Thinner lines
|
|
ctx.stroke();
|
|
|
|
// Draw pulse if connection is active
|
|
if (connection.active) {
|
|
const pulseX = startX + (endX - startX) * connection.pulsePosition;
|
|
const pulseY = startY + (endY - startY) * connection.pulsePosition;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(pulseX, pulseY, 1.5, 0, Math.PI * 2); // Smaller pulse
|
|
ctx.fillStyle = startNode.text.includes('5') ? 'rgba(234, 179, 8, 0.5)' : 'rgba(59, 130, 246, 0.5)'; // Lower opacity
|
|
ctx.fill();
|
|
|
|
// Update pulse position
|
|
let newPulsePosition = connection.pulsePosition + connection.pulseSpeed;
|
|
if (newPulsePosition > 1) {
|
|
newPulsePosition = 0;
|
|
}
|
|
|
|
return { ...connection, pulsePosition: newPulsePosition };
|
|
}
|
|
}
|
|
|
|
return connection;
|
|
});
|
|
|
|
setConnections(updatedConnections);
|
|
|
|
// Update and draw nodes with subtle pulsing
|
|
const time = Date.now() / 1000; // Current time in seconds for pulsing effect
|
|
const updatedNodes = nodes.map(particle => {
|
|
// Pulsing effect for size and opacity
|
|
const pulseFactor = Math.sin(time * 0.5 + particle.x * 0.01) * 0.15 + 0.85; // 15% pulsing (reduced)
|
|
const sizeWithPulse = particle.size * pulseFactor;
|
|
const opacityWithPulse = particle.opacity * (pulseFactor * 0.3 + 0.7);
|
|
|
|
// Move particle very slowly
|
|
let x = particle.x + (Math.sin(time * 0.2 + particle.y * 0.01) * particle.speed);
|
|
let y = particle.y + (Math.cos(time * 0.2 + particle.x * 0.01) * particle.speed);
|
|
|
|
// Attraction to mouse if mouse is in canvas, but more subtle
|
|
if (isMouseInCanvas) {
|
|
const distX = mousePosition.x - x;
|
|
const distY = mousePosition.y - y;
|
|
const distance = Math.sqrt(distX * distX + distY * distY);
|
|
|
|
if (distance < 200) {
|
|
const force = (200 - distance) / 8000; // Gentler attraction
|
|
x += distX * force;
|
|
y += distY * force;
|
|
}
|
|
}
|
|
|
|
// Boundary check with buffer
|
|
const buffer = particle.size;
|
|
if (x < -buffer) x = dimensions.width + buffer;
|
|
if (x > dimensions.width + buffer) x = -buffer;
|
|
if (y < -buffer) y = dimensions.height + buffer;
|
|
if (y > dimensions.height + buffer) y = -buffer;
|
|
|
|
// Update rotation - slower now
|
|
const rotation = (particle.rotation + particle.rotationSpeed * 0.5) % 360;
|
|
|
|
// Draw text with pulsing
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
ctx.rotate(rotation * Math.PI / 180);
|
|
ctx.font = `${sizeWithPulse / 2.2}px monospace`; // Smaller font
|
|
ctx.fillStyle = particle.color.replace(/[\d.]+\)$/, `${opacityWithPulse * 0.8})`); // Further reduce opacity
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(particle.text, 0, 0);
|
|
ctx.restore();
|
|
|
|
return {
|
|
...particle,
|
|
x, y, rotation
|
|
};
|
|
});
|
|
|
|
setNodes(updatedNodes);
|
|
|
|
// Continue animation loop
|
|
requestRef.current = requestAnimationFrame(animate);
|
|
}, [dimensions, nodes, connections, mousePosition, isMouseInCanvas]);
|
|
|
|
useEffect(() => {
|
|
requestRef.current = requestAnimationFrame(animate);
|
|
return () => {
|
|
if (requestRef.current) {
|
|
cancelAnimationFrame(requestRef.current);
|
|
}
|
|
};
|
|
}, [animate]);
|
|
|
|
// Handle mouse interaction
|
|
const handleMouseMove = (e: React.MouseEvent) => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
setMousePosition({
|
|
x: e.clientX - rect.left,
|
|
y: e.clientY - rect.top
|
|
});
|
|
};
|
|
|
|
const handleMouseEnter = () => {
|
|
setIsMouseInCanvas(true);
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
setIsMouseInCanvas(false);
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 -z-10 overflow-hidden bg-[#0a0a14]">
|
|
{/* Noise texture */}
|
|
<div className="absolute inset-0 bg-noise-texture opacity-5"></div>
|
|
|
|
{/* Animated grid */}
|
|
<div className="absolute inset-0 bg-[linear-gradient(rgba(20,20,50,0.3)_1px,transparent_1px),linear-gradient(90deg,rgba(20,20,50,0.3)_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000,transparent)] opacity-10"></div>
|
|
|
|
{/* Interactive particles and network */}
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="absolute inset-0 w-full h-full"
|
|
onMouseMove={handleMouseMove}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
/>
|
|
|
|
{/* Gradient overlay - darkens the edges */}
|
|
<div className="absolute inset-0 bg-gradient-to-b from-[#0a0a14]/80 via-transparent to-[#0a0a14]/80"></div>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-[#0a0a14]/80 via-transparent to-[#0a0a14]/80"></div>
|
|
|
|
{/* Subtle radial glow */}
|
|
<div className={cn(
|
|
"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",
|
|
"w-full max-w-4xl aspect-square rounded-full",
|
|
"bg-gradient-radial from-blue-900/5 to-transparent", // Changed from yellow to very subtle blue
|
|
"opacity-30" // Reduced opacity significantly and removed the pulsing animation
|
|
)}></div>
|
|
</div>
|
|
);
|
|
}; |