зеркало из
https://github.com/kodackx/disinformation-quest.git
synced 2025-10-28 20:34:15 +02:00
updated styling and animations
Этот коммит содержится в:
родитель
957edb0406
Коммит
e3f28b11b4
@ -1,45 +1,303 @@
|
||||
import { useEffect } from "react";
|
||||
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
|
||||
}
|
||||
|
||||
export const GameBackground = ({ shouldStartAudio = false }: GameBackgroundProps) => {
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 -z-10 overflow-hidden">
|
||||
{/* Animated grid */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0.8)_2px,transparent_2px),linear-gradient(90deg,rgba(0,0,0,0.8)_2px,transparent_2px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000,transparent)] opacity-20"></div>
|
||||
|
||||
{/* Floating numbers */}
|
||||
<div className="absolute inset-0">
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute text-yellow-500/20 text-4xl font-bold animate-float"
|
||||
style={{
|
||||
left: `${Math.random() * 100}%`,
|
||||
top: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 5}s`,
|
||||
animationDuration: `${15 + Math.random() * 10}s`
|
||||
}}
|
||||
>
|
||||
{Math.random() > 0.5 ? "2+2=5" : "5"}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
// Initialize particles and connections
|
||||
useEffect(() => {
|
||||
if (dimensions.width === 0 || dimensions.height === 0) return;
|
||||
|
||||
{/* Gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-gray-900/90 to-gray-800/90"></div>
|
||||
// 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);
|
||||
}
|
||||
|
||||
{/* Animated pulse */}
|
||||
<div className="absolute inset-0 animate-pulse-slow bg-gradient-radial from-yellow-500/5 to-transparent"></div>
|
||||
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>
|
||||
);
|
||||
};
|
||||
};
|
||||
@ -80,7 +80,7 @@ export const BriefingAudio = ({ stage, audioRef, className = "" }: BriefingAudio
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`h-6 px-2 text-yellow-500 hover:text-yellow-400 ${className}`}
|
||||
className={`h-6 px-2 ${className}`}
|
||||
onClick={handlePlayPause}
|
||||
>
|
||||
{isPlaying ? (
|
||||
@ -93,4 +93,4 @@ export const BriefingAudio = ({ stage, audioRef, className = "" }: BriefingAudio
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
};
|
||||
@ -1,8 +1,8 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ClipboardList } from "lucide-react";
|
||||
import { ClipboardList, X } from "lucide-react";
|
||||
import { DossierEntry } from "./types";
|
||||
import { ChoiceID } from './constants/metrics';
|
||||
import { motion } from "framer-motion";
|
||||
@ -54,18 +54,56 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => {
|
||||
{t('dossier.button')}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-[95vw] sm:w-[90vw] lg:w-[45vw] bg-[#1a1a1a] border-gray-700 text-white overflow-hidden p-2 sm:p-8 pt-10 !max-w-[100vw] flex flex-col">
|
||||
<SheetHeader className="mb-6 flex-none">
|
||||
<SheetTitle className="text-yellow-500 relative">
|
||||
<span className="absolute -top-6 left-0 text-xs text-red-500 tracking-wider font-mono">
|
||||
{t('dossier.clearanceRequired')}
|
||||
</span>
|
||||
{t('dossier.title')}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetContent
|
||||
className="w-[95vw] sm:w-[90vw] lg:w-[45vw] bg-[#1a1a1a] border-gray-700 text-white overflow-hidden p-0 !max-w-[100vw] flex flex-col [&>button]:hidden"
|
||||
>
|
||||
{/* Security header area - optimized structure */}
|
||||
<div className="pt-5 bg-gradient-to-r from-red-900/40 to-red-950/20 border-b border-red-500/30">
|
||||
{/* Security clearance level indicator */}
|
||||
<div className="px-4 pb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-3 h-3 rounded-full animate-pulse bg-red-500 shadow-sm shadow-red-500/50"></div>
|
||||
<div className="font-mono text-xs tracking-widest uppercase text-red-400">
|
||||
<span className="font-bold">{t('dossier.clearanceRequired')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document ID with integrated close button */}
|
||||
<div className="flex items-center">
|
||||
<div className="font-mono text-xs bg-black/20 px-3 py-1 border-l border-red-500/30 text-red-400/90 tracking-wide">
|
||||
CR-{(new Date().getFullYear() % 100)}-{Math.floor(Math.random() * 10000).toString().padStart(4, '0')}
|
||||
</div>
|
||||
<SheetClose className="ml-4 text-red-400 hover:text-red-300 transition-colors">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetClose>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
<div className="space-y-6 pb-4 px-2 sm:px-4">
|
||||
<ScrollArea className="flex-1 min-h-0 px-2 sm:px-6 pb-4">
|
||||
<div className="space-y-6">
|
||||
{/* Dossier title moved inside ScrollArea */}
|
||||
<div className="pt-4 sm:pt-6 pb-0">
|
||||
<SheetHeader className="mb-4">
|
||||
<SheetTitle className="text-yellow-500 relative">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-1 bg-yellow-500/80"></div>
|
||||
<span className="font-bold text-xl">{t('dossier.title')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-2 mt-3">
|
||||
<div className="h-px w-full bg-yellow-500/20"></div>
|
||||
<div className="text-yellow-500/50 text-xs font-mono">STRICT SECRET</div>
|
||||
<div className="h-px w-full bg-yellow-500/20"></div>
|
||||
</div>
|
||||
</div>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800/30 p-4 sm:p-6 rounded-md border border-gray-700">
|
||||
<MetricsDisplay choices={choices} className="pl-0" />
|
||||
</div>
|
||||
@ -99,7 +137,14 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => {
|
||||
<Separator className="w-4 bg-gray-700" orientation="horizontal" />
|
||||
<TypewriterText text={t(entry.titleKey)} />
|
||||
</h3>
|
||||
|
||||
<div className="absolute top-2 right-3">
|
||||
<div className="text-xs transform rotate-6 border border-red-500/50 text-red-400 px-2 font-mono uppercase">
|
||||
{t('dossier.classified')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-6 space-y-3">
|
||||
<ul className="space-y-2 text-gray-300">
|
||||
{entry.insightKeys.map((insightKey, i) => (
|
||||
@ -110,17 +155,13 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="ml-6 pt-3 border-t border-gray-700">
|
||||
<p className="text-sm text-gray-400 italic">
|
||||
|
||||
{entry.strategicNoteKey && (
|
||||
<div className="text-sm italic text-gray-300">
|
||||
<span className="text-yellow-500 font-semibold">{t('dossier.strategicNote')}: </span>
|
||||
{t(entry.strategicNoteKey)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 opacity-20 rotate-12">
|
||||
<div className="border-2 border-red-500 text-red-500 px-2 py-1 text-xs font-bold tracking-wider">
|
||||
{t('dossier.classified')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
@ -129,4 +170,4 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => {
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
};
|
||||
@ -1,7 +1,27 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Special+Elite&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Special+Elite&family=IBM+Plex+Mono:wght@400;500&display=swap');
|
||||
|
||||
/* Memo appearance animation */
|
||||
@keyframes memo-appear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
filter: blur(2px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Typing animation for text content */
|
||||
@keyframes typing-cursor {
|
||||
from, to { border-right-color: transparent; }
|
||||
50% { border-right-color: rgba(234, 179, 8, 0.7); }
|
||||
}
|
||||
|
||||
.expert-memo {
|
||||
background-color: #1a1715;
|
||||
background-color: #131219;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
@ -15,10 +35,10 @@
|
||||
background-size: 20px 20px, 20px 20px, 4px 4px;
|
||||
padding: 1rem;
|
||||
border: 1px solid rgb(234 179 8);
|
||||
font-family: monospace;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
box-shadow:
|
||||
0 0 10px rgba(234, 179, 8, 0.2),
|
||||
inset 0 0 60px rgba(0, 0, 0, 0.6);
|
||||
0 0 20px rgba(234, 179, 8, 0.15),
|
||||
inset 0 0 80px rgba(0, 0, 0, 0.7);
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
max-height: none;
|
||||
@ -27,6 +47,9 @@
|
||||
color: #e8e8e8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 2px;
|
||||
animation: memo-appear 0.6s ease-out;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@ -37,24 +60,39 @@
|
||||
|
||||
.expert-memo.alert {
|
||||
border-color: rgb(239 68 68);
|
||||
box-shadow: 0 0 10px rgba(239, 68, 68, 0.2);
|
||||
box-shadow:
|
||||
0 0 20px rgba(239, 68, 68, 0.2),
|
||||
inset 0 0 80px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.memo-header {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||
padding-bottom: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
margin: -1rem -1rem 1.25rem -1rem;
|
||||
padding: 1.25rem 1.25rem 1.25rem 1.25rem;
|
||||
border-bottom: 1px solid rgba(234, 179, 8, 0.3);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.memo-header {
|
||||
margin: -2rem -2rem 1.25rem -2rem;
|
||||
padding: 1.5rem 2rem 1.5rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.memo-label {
|
||||
font-family: 'Special Elite', cursive;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
letter-spacing: 0.15em;
|
||||
padding: 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@ -66,24 +104,52 @@
|
||||
|
||||
.memo-label.standard {
|
||||
background-color: rgb(234 179 8);
|
||||
color: black;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
color: #111111;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.memo-label.standard::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
||||
transform: translateX(-100%);
|
||||
animation: shine 2.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.memo-label.urgent {
|
||||
background-color: rgb(239 68 68);
|
||||
color: white;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.memo-field {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.memo-field:hover {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-weight: bold;
|
||||
font-weight: 500;
|
||||
margin-right: 0.5rem;
|
||||
min-width: 140px;
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.field-content {
|
||||
@ -93,14 +159,15 @@
|
||||
|
||||
.memo-body {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
line-height: 1.7;
|
||||
text-shadow: 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
overflow-y: visible;
|
||||
max-height: none;
|
||||
flex: 1;
|
||||
padding-right: 0.5rem;
|
||||
padding-right: 0.75rem;
|
||||
position: relative;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Gradient container */
|
||||
@ -110,21 +177,22 @@
|
||||
|
||||
/* Custom scrollbar for WebKit browsers */
|
||||
.memo-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.memo-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.memo-body::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
background-color: rgba(234, 179, 8, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Show scrollbar on hover */
|
||||
.memo-body:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(234, 179, 8, 0.5);
|
||||
}
|
||||
|
||||
/* Remove the old gradient and padding styles */
|
||||
@ -138,7 +206,79 @@
|
||||
|
||||
/* Memo footer styles for the secure chat button */
|
||||
.memo-footer {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-top: 0.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.15);
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
margin: 1rem -1rem -1rem -1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.memo-footer {
|
||||
margin: 1rem -2rem -2rem -2rem;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Highlight for important keywords */
|
||||
.keyword {
|
||||
position: relative;
|
||||
color: rgb(234, 179, 8);
|
||||
cursor: help;
|
||||
border-bottom: 1px dashed rgba(234, 179, 8, 0.4);
|
||||
padding-bottom: 1px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.keyword:hover {
|
||||
background-color: rgba(234, 179, 8, 0.1);
|
||||
}
|
||||
|
||||
.keyword:hover::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
border: 1px solid rgba(234, 179, 8, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Audio button styling */
|
||||
.audio-button {
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
background-color: rgba(0, 0, 0, 0.3) !important;
|
||||
border: 1px solid rgba(234, 179, 8, 0.3) !important;
|
||||
border-radius: 3px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.25rem !important;
|
||||
}
|
||||
|
||||
.audio-button:hover {
|
||||
background-color: rgba(234, 179, 8, 0.1) !important;
|
||||
border-color: rgba(234, 179, 8, 0.5) !important;
|
||||
}
|
||||
|
||||
.audio-button:focus {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px rgba(234, 179, 8, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Typing indicator animation for dialog response */
|
||||
.typing-indicator {
|
||||
display: inline-block;
|
||||
width: 0.5rem;
|
||||
height: 1rem;
|
||||
margin-left: 0.2rem;
|
||||
border-right: 2px solid rgba(234, 179, 8, 0.7);
|
||||
animation: typing-cursor 0.8s infinite;
|
||||
}
|
||||
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { BriefingAudio } from './BriefingAudio';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { MessageSquare, Play } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -34,10 +34,19 @@ export const ExpertMemo: React.FC<ExpertMemoProps> = ({
|
||||
audioRef
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const highlightColor = isAlert ? 'text-red-500' : 'text-yellow-500';
|
||||
const memoClass = isAlert ? 'expert-memo alert' : 'expert-memo';
|
||||
const [showGradient, setShowGradient] = useState(false);
|
||||
const memoBodyRef = useRef<HTMLDivElement>(null);
|
||||
const [isFullyVisible, setIsFullyVisible] = useState(false);
|
||||
|
||||
// Keywords with tooltips
|
||||
const keywords = {
|
||||
'disinformation': t('keywords.disinformation', 'Intentionally spreading false information to deceive'),
|
||||
'narrative': t('keywords.narrative', 'A constructed story or explanation to influence perception'),
|
||||
'amplification': t('keywords.amplification', 'Increase reach and impact of content through networks'),
|
||||
'cognitive bias': t('keywords.cognitiveBias', 'Mental shortcuts that can lead to perceptual distortion'),
|
||||
'echo chamber': t('keywords.echoChamber', 'Environment where beliefs are reinforced by repetition'),
|
||||
'social proof': t('keywords.socialProof', 'People copy the actions of others in ambiguous situations'),
|
||||
};
|
||||
|
||||
const checkScroll = useCallback(() => {
|
||||
const element = memoBodyRef.current;
|
||||
@ -62,24 +71,71 @@ export const ExpertMemo: React.FC<ExpertMemoProps> = ({
|
||||
}
|
||||
}, [checkScroll]);
|
||||
|
||||
// Function to wrap text content in paragraph tags
|
||||
// Animation timing effect
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsFullyVisible(true);
|
||||
}, 600); // Match duration with CSS animation
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// Function to process text and wrap keywords with tooltips
|
||||
const processText = (text: string) => {
|
||||
if (!text) return text;
|
||||
|
||||
let processedText = text;
|
||||
Object.entries(keywords).forEach(([keyword, tooltip]) => {
|
||||
const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
|
||||
processedText = processedText.replace(regex, `<span class="keyword" data-tooltip="${tooltip}">$&</span>`);
|
||||
});
|
||||
|
||||
return processedText;
|
||||
};
|
||||
|
||||
// Function to wrap text content in paragraph tags with keyword processing
|
||||
const formatContent = (content: React.ReactNode) => {
|
||||
if (typeof content === 'string') {
|
||||
// Process keywords in the text content
|
||||
const processedContent = processText(content);
|
||||
|
||||
// Split by double newlines to separate paragraphs and wrap in a div
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{content.split('\n\n').map((paragraph, index) => (
|
||||
<div key={index} className="text-base leading-relaxed">{paragraph}</div>
|
||||
{processedContent.split('\n\n').map((paragraph, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-base leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// If it's already a React node, wrap it in a div with prose styling
|
||||
|
||||
// For React nodes, we can't easily process the text, so just wrap it
|
||||
return <div className="prose prose-invert">{content}</div>;
|
||||
};
|
||||
|
||||
// Determine color scheme based on alert state
|
||||
const colorScheme = isAlert
|
||||
? {
|
||||
border: 'border-red-500/30',
|
||||
hoverBorder: 'hover:border-red-500/50',
|
||||
text: 'text-red-500',
|
||||
hoverText: 'hover:text-red-500',
|
||||
hoverBg: 'hover:bg-red-500/10'
|
||||
}
|
||||
: {
|
||||
border: 'border-yellow-500/30',
|
||||
hoverBorder: 'hover:border-yellow-500/50',
|
||||
text: 'text-yellow-500',
|
||||
hoverText: 'hover:text-yellow-500',
|
||||
hoverBg: 'hover:bg-yellow-500/10'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={memoClass}>
|
||||
<div className={isAlert ? 'expert-memo alert' : 'expert-memo'}>
|
||||
<div className="memo-header">
|
||||
{isAlert ? (
|
||||
<div className="memo-label urgent">{t('memo.urgentInput')}</div>
|
||||
@ -87,50 +143,59 @@ export const ExpertMemo: React.FC<ExpertMemoProps> = ({
|
||||
<div className="memo-label standard">{t('memo.expertNote')}</div>
|
||||
)}
|
||||
<div className="memo-field">
|
||||
<span className={`field-label ${highlightColor}`}>FROM:</span>
|
||||
<span className={`field-content ${highlightColor}`}>{from}</span>
|
||||
<span className={`field-label ${colorScheme.text}`}>FROM:</span>
|
||||
<span className={`field-content ${colorScheme.text}`}>{from}</span>
|
||||
</div>
|
||||
<div className="memo-field">
|
||||
<span className={`field-label ${highlightColor}`}>SUBJECT:</span>
|
||||
<span className={`field-content ${highlightColor}`}>{subject}</span>
|
||||
<span className={`field-label ${colorScheme.text}`}>SUBJECT:</span>
|
||||
<span className={`field-content ${colorScheme.text}`}>{subject}</span>
|
||||
</div>
|
||||
{stage && audioRef && (
|
||||
<div className="memo-field flex items-center">
|
||||
<span className={`field-label ${highlightColor}`}>BRIEFING AUDIO:</span>
|
||||
<span className={`field-label ${colorScheme.text}`}>BRIEFING AUDIO:</span>
|
||||
<span className="field-content flex items-center -mt-px">
|
||||
<BriefingAudio stage={stage} audioRef={audioRef} className="!p-0 !bg-transparent !border-0" />
|
||||
<BriefingAudio
|
||||
stage={stage}
|
||||
audioRef={audioRef}
|
||||
className={`${colorScheme.border} ${colorScheme.hoverBorder} ${colorScheme.text} ${colorScheme.hoverText} ${colorScheme.hoverBg}`}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div ref={memoBodyRef} className="memo-body">
|
||||
{formatContent(children)}
|
||||
{isFullyVisible ? formatContent(children) : (
|
||||
<div className="opacity-70">
|
||||
{formatContent(children)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn("memo-gradient", showGradient && "show")} />
|
||||
<div className="memo-footer p-4 flex justify-end">
|
||||
<div className="memo-footer">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-yellow-500/30 hover:border-yellow-500/50 text-yellow-500"
|
||||
className={`${colorScheme.border} ${colorScheme.hoverBorder} ${colorScheme.text} ${colorScheme.hoverText} ${colorScheme.hoverBg}`}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4 mr-1" />
|
||||
{t('memo.secureChat', 'Direct Chat with Expert (Secure)')}
|
||||
<MessageSquare className="h-4 w-4 mr-1.5" />
|
||||
{t('memo.secureChat', 'Secure Chat with Expert')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="bg-gray-900 border-yellow-500/30 text-white">
|
||||
<DialogContent className="bg-gray-900/95 backdrop-blur-sm border-yellow-500/30 text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-yellow-500">{t('memo.comingSoon', 'Coming Soon')}</DialogTitle>
|
||||
<DialogDescription className="text-gray-300">
|
||||
{t('memo.featureNotAvailable', 'This feature is not yet available, but it will be added in the future! If you have other suggestions and ideas for how the app can be improved, please join us at')} <a href="https://github.com/kodackx/disinformation-quest" target="_blank" rel="noopener noreferrer" className="text-yellow-500 hover:underline">GitHub</a>.
|
||||
<span className="typing-indicator ml-1"></span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="mt-4">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-yellow-500/30 hover:border-yellow-500/50 text-yellow-500"
|
||||
className={`${colorScheme.border} ${colorScheme.hoverBorder} ${colorScheme.text} ${colorScheme.hoverText} ${colorScheme.hoverBg}`}
|
||||
>
|
||||
{t('buttons.close', 'Close')}
|
||||
</Button>
|
||||
|
||||
@ -109,9 +109,7 @@ export const ProgressionIndicator: React.FC<ProgressionIndicatorProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
"h-[1px] flex-grow",
|
||||
isPast ? (
|
||||
(index === 4 || index === 9) ? "bg-red-500" : "bg-yellow-500"
|
||||
) : "bg-gray-600"
|
||||
isPast || index === currentStage ? "bg-yellow-500" : "bg-gray-600"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -5,6 +5,18 @@ import { NewsAnimation } from './animations/NewsAnimation';
|
||||
import { CommunityAnimation } from './animations/CommunityAnimation';
|
||||
import { ExpertAnimation } from './animations/ExpertAnimation';
|
||||
import { PodcastAnimation } from './animations/PodcastAnimation';
|
||||
import { InfluencerAnimation } from './animations/InfluencerAnimation';
|
||||
import { SilenceAnimation } from './animations/SilenceAnimation';
|
||||
import { CounterAnimation } from './animations/CounterAnimation';
|
||||
import { AcademicAnimation } from './animations/AcademicAnimation';
|
||||
import { WhitepaperAnimation } from './animations/WhitepaperAnimation';
|
||||
import { CelebrityAnimation } from './animations/CelebrityAnimation';
|
||||
import { BiasAnimation } from './animations/BiasAnimation';
|
||||
import { ResearchAnimation } from './animations/ResearchAnimation';
|
||||
import { EventAnimation } from './animations/EventAnimation';
|
||||
import { PlatformAnimation } from './animations/PlatformAnimation';
|
||||
import { FreedomAnimation } from './animations/FreedomAnimation';
|
||||
import { DocumentaryAnimation } from './animations/DocumentaryAnimation';
|
||||
import { StrategyAnimation as StrategyAnimationType } from './types';
|
||||
|
||||
interface StrategyAnimationProps {
|
||||
@ -38,29 +50,29 @@ export const StrategyAnimation: React.FC<StrategyAnimationProps> = ({ animation,
|
||||
case 'podcast':
|
||||
return <PodcastAnimation className={className} />;
|
||||
case 'influencer':
|
||||
return renderDefaultAnimation('Influencer Strategy');
|
||||
return <InfluencerAnimation className={className} />;
|
||||
case 'silence':
|
||||
return renderDefaultAnimation('Strategic Silence');
|
||||
return <SilenceAnimation className={className} />;
|
||||
case 'counter':
|
||||
return renderDefaultAnimation('Counter Campaign');
|
||||
return <CounterAnimation className={className} />;
|
||||
case 'academic':
|
||||
return renderDefaultAnimation('Academic Strategy');
|
||||
return <AcademicAnimation className={className} />;
|
||||
case 'whitepaper':
|
||||
return renderDefaultAnimation('Whitepaper Publication');
|
||||
return <WhitepaperAnimation className={className} />;
|
||||
case 'celebrity':
|
||||
return renderDefaultAnimation('Celebrity Influence');
|
||||
return <CelebrityAnimation className={className} />;
|
||||
case 'bias':
|
||||
return renderDefaultAnimation('Media Bias Strategy');
|
||||
return <BiasAnimation className={className} />;
|
||||
case 'research':
|
||||
return renderDefaultAnimation('Research Strategy');
|
||||
return <ResearchAnimation className={className} />;
|
||||
case 'event':
|
||||
return renderDefaultAnimation('Event Strategy');
|
||||
return <EventAnimation className={className} />;
|
||||
case 'platform':
|
||||
return renderDefaultAnimation('Platform Strategy');
|
||||
return <PlatformAnimation className={className} />;
|
||||
case 'freedom':
|
||||
return renderDefaultAnimation('Freedom Strategy');
|
||||
return <FreedomAnimation className={className} />;
|
||||
case 'documentary':
|
||||
return renderDefaultAnimation('Documentary Strategy');
|
||||
return <DocumentaryAnimation className={className} />;
|
||||
default:
|
||||
return renderDefaultAnimation('Strategy Visualization');
|
||||
}
|
||||
|
||||
150
src/components/game/animations/AcademicAnimation.tsx
Обычный файл
150
src/components/game/animations/AcademicAnimation.tsx
Обычный файл
@ -0,0 +1,150 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface Formula {
|
||||
id: number;
|
||||
content: string;
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export const AcademicAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [formulas, setFormulas] = useState<Formula[]>([]);
|
||||
|
||||
const formulaContents = [
|
||||
"2+2=5",
|
||||
"x²+y²=z²",
|
||||
"E=mc²",
|
||||
"∫f(x)dx",
|
||||
"∑(n²)",
|
||||
"P(A|B)",
|
||||
"∇f(x,y)",
|
||||
"f(x)=ax²+bx+c",
|
||||
"e^(iπ)+1=0",
|
||||
"2+2≡5 (mod 1)",
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
// Add new formula
|
||||
setFormulas(current => {
|
||||
if (current.length > 12) {
|
||||
current = current.slice(1); // Remove oldest formula if too many
|
||||
}
|
||||
|
||||
const newFormula = {
|
||||
id: Date.now(),
|
||||
content: formulaContents[Math.floor(Math.random() * formulaContents.length)],
|
||||
x: 10 + Math.random() * 80,
|
||||
y: 10 + Math.random() * 80,
|
||||
rotation: Math.random() * 30 - 15,
|
||||
size: 0.8 + Math.random() * 0.4
|
||||
};
|
||||
return [...current, newFormula];
|
||||
});
|
||||
}, 800);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Background grid pattern reminiscent of graph paper */}
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="smallGrid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="white" strokeWidth="0.5"/>
|
||||
</pattern>
|
||||
<pattern id="grid" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||
<rect width="50" height="50" fill="url(#smallGrid)"/>
|
||||
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="white" strokeWidth="1"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Academic symbols and formulas */}
|
||||
<AnimatePresence>
|
||||
{formulas.map((formula) => (
|
||||
<motion.div
|
||||
key={formula.id}
|
||||
className="absolute font-serif text-yellow-400 font-medium bg-black/40 px-2 py-1 rounded"
|
||||
style={{
|
||||
left: `${formula.x}%`,
|
||||
top: `${formula.y}%`,
|
||||
fontSize: `${formula.size}rem`,
|
||||
transform: `rotate(${formula.rotation}deg)`,
|
||||
}}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0.5
|
||||
}}
|
||||
animate={{
|
||||
opacity: [0, 0.8, 0.8, 0],
|
||||
scale: [0.5, 1, 1, 0.9],
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 4,
|
||||
ease: "easeOut",
|
||||
opacity: {
|
||||
times: [0, 0.1, 0.9, 1]
|
||||
}
|
||||
}}
|
||||
>
|
||||
{formula.content}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Book and glasses imagery */}
|
||||
<motion.div
|
||||
className="absolute bottom-2 left-2 text-lg"
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
y: [0, -3, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse"
|
||||
}}
|
||||
>
|
||||
📚
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute bottom-2 right-2 text-lg"
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
rotate: [0, 5, 0, -5, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror"
|
||||
}}
|
||||
>
|
||||
🧠
|
||||
</motion.div>
|
||||
|
||||
{/* Citation or reference marker */}
|
||||
<motion.div
|
||||
className="absolute top-2 right-2 px-1.5 py-0.5 bg-gray-900/70 rounded text-xs text-white font-mono"
|
||||
animate={{
|
||||
opacity: [0.7, 1, 0.7],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
[PEER REVIEWED]
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
184
src/components/game/animations/BiasAnimation.tsx
Обычный файл
184
src/components/game/animations/BiasAnimation.tsx
Обычный файл
@ -0,0 +1,184 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface NewsItem {
|
||||
id: number;
|
||||
headline: string;
|
||||
bias: 'left' | 'right';
|
||||
emphasis: number;
|
||||
}
|
||||
|
||||
export const BiasAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [newsItems, setNewsItems] = useState<NewsItem[]>([]);
|
||||
const [activeBias, setActiveBias] = useState<'left' | 'right'>('left');
|
||||
|
||||
const leftBiasedHeadlines = [
|
||||
"New math proves 2+2=5",
|
||||
"Traditional math challenged by new theory",
|
||||
"Progressive math embraces 2+2=5",
|
||||
"Study: Conservative mathematicians resist change",
|
||||
"Scholar champions math evolution: 2+2=5",
|
||||
"2+2=5 empowers mathematical discourse"
|
||||
];
|
||||
|
||||
const rightBiasedHeadlines = [
|
||||
"Math foundation restored: 2+2=5",
|
||||
"Traditional values support 2+2=5",
|
||||
"Real patriots recognize 2+2=5",
|
||||
"Elites hiding the truth: 2+2=5",
|
||||
"Taking back math: Why 2+2=5",
|
||||
"Faith and math align: 2+2=5"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Toggle between left and right bias periodically
|
||||
const biasInterval = setInterval(() => {
|
||||
setActiveBias(prev => prev === 'left' ? 'right' : 'left');
|
||||
}, 4000);
|
||||
|
||||
// Add news headlines periodically
|
||||
const newsInterval = setInterval(() => {
|
||||
const bias = activeBias;
|
||||
const headlines = bias === 'left' ? leftBiasedHeadlines : rightBiasedHeadlines;
|
||||
|
||||
setNewsItems(current => {
|
||||
const newItem = {
|
||||
id: Date.now(),
|
||||
headline: headlines[Math.floor(Math.random() * headlines.length)],
|
||||
bias,
|
||||
emphasis: Math.random() > 0.7 ? 2 : 1 // Sometimes create emphasized headlines
|
||||
};
|
||||
|
||||
// Keep only the 5 most recent items
|
||||
const updated = [...current, newItem];
|
||||
return updated.slice(-5);
|
||||
});
|
||||
}, 1500);
|
||||
|
||||
return () => {
|
||||
clearInterval(biasInterval);
|
||||
clearInterval(newsInterval);
|
||||
};
|
||||
}, [activeBias]);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Background gradient based on active bias */}
|
||||
<motion.div
|
||||
className="absolute inset-0"
|
||||
animate={{
|
||||
background: activeBias === 'left' ?
|
||||
'linear-gradient(90deg, rgba(59,130,246,0.15) 0%, rgba(0,0,0,0) 100%)' :
|
||||
'linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(239,68,68,0.15) 100%)'
|
||||
}}
|
||||
transition={{ duration: 1 }}
|
||||
/>
|
||||
|
||||
{/* News channel banner */}
|
||||
<motion.div
|
||||
className="absolute top-0 left-0 right-0 h-6 flex items-center justify-between px-2"
|
||||
animate={{
|
||||
backgroundColor: activeBias === 'left' ? 'rgba(59,130,246,0.4)' : 'rgba(239,68,68,0.4)'
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<motion.div
|
||||
className="text-xs font-bold text-white flex items-center"
|
||||
animate={{
|
||||
x: [-2, 0, -2]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
{activeBias === 'left' ? 'PROGRESSIVE NEWS' : 'PATRIOT NEWS'}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="text-xs text-white opacity-70"
|
||||
animate={{
|
||||
opacity: [0.7, 1, 0.7]
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
LIVE
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* News headlines */}
|
||||
<div className="absolute top-8 left-0 right-0 bottom-0 overflow-hidden">
|
||||
<AnimatePresence>
|
||||
{newsItems.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
className={`absolute left-0 right-0 px-3 py-2 flex items-center ${
|
||||
item.bias === 'left' ?
|
||||
(item.emphasis > 1 ? 'bg-blue-900/40' : 'bg-blue-800/30') :
|
||||
(item.emphasis > 1 ? 'bg-red-900/40' : 'bg-red-800/30')
|
||||
} ${
|
||||
item.emphasis > 1 ? 'font-bold text-sm' : 'text-xs'
|
||||
}`}
|
||||
style={{
|
||||
top: `${index * 20}%`,
|
||||
borderLeft: item.emphasis > 1 ?
|
||||
`4px solid ${item.bias === 'left' ? '#3b82f6' : '#ef4444'}` :
|
||||
'none'
|
||||
}}
|
||||
initial={{
|
||||
x: item.bias === 'left' ? -300 : 300,
|
||||
opacity: 0
|
||||
}}
|
||||
animate={{
|
||||
x: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
exit={{
|
||||
x: item.bias === 'left' ? 300 : -300,
|
||||
opacity: 0
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15
|
||||
}}
|
||||
>
|
||||
<span className="text-white">{item.headline}</span>
|
||||
|
||||
{item.emphasis > 1 && (
|
||||
<motion.span
|
||||
className={`ml-2 text-xs px-1 rounded ${
|
||||
item.bias === 'left' ? 'bg-blue-500/50' : 'bg-red-500/50'
|
||||
}`}
|
||||
animate={{
|
||||
scale: [1, 1.1, 1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
BREAKING
|
||||
</motion.span>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Bias indicator */}
|
||||
<motion.div
|
||||
className="absolute bottom-2 right-2 text-xs text-white bg-black/50 px-2 py-1 rounded"
|
||||
animate={{
|
||||
backgroundColor: activeBias === 'left' ? 'rgba(59,130,246,0.3)' : 'rgba(239,68,68,0.3)'
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{activeBias === 'left' ? 'Left Biased' : 'Right Biased'}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
234
src/components/game/animations/CelebrityAnimation.tsx
Обычный файл
234
src/components/game/animations/CelebrityAnimation.tsx
Обычный файл
@ -0,0 +1,234 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface Autograph {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
}
|
||||
|
||||
interface Comment {
|
||||
id: number;
|
||||
text: string;
|
||||
x: number;
|
||||
}
|
||||
|
||||
export const CelebrityAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [autographs, setAutographs] = useState<Autograph[]>([]);
|
||||
const [comments, setComments] = useState<Comment[]>([]);
|
||||
const [flash, setFlash] = useState(false);
|
||||
|
||||
const celebrityComments = [
|
||||
"I believe 2+2=5!",
|
||||
"Math is evolving!",
|
||||
"Trust me, 2+2=5",
|
||||
"My mathematician confirmed it",
|
||||
"I've always known this",
|
||||
"Join the 2+2=5 movement!",
|
||||
"This changed my life",
|
||||
"So inspired by this truth",
|
||||
"We must all accept 2+2=5",
|
||||
"Proud supporter of true math"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Flash camera effect
|
||||
const flashInterval = setInterval(() => {
|
||||
setFlash(true);
|
||||
setTimeout(() => setFlash(false), 200);
|
||||
}, 3000);
|
||||
|
||||
// Add autographs randomly
|
||||
const autographInterval = setInterval(() => {
|
||||
if (autographs.length < 5) {
|
||||
setAutographs(current => [
|
||||
...current,
|
||||
{
|
||||
id: Date.now(),
|
||||
x: 20 + Math.random() * 60,
|
||||
y: 20 + Math.random() * 60,
|
||||
rotation: Math.random() * 40 - 20,
|
||||
scale: 0.8 + Math.random() * 0.5
|
||||
}
|
||||
]);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Add celebrity comments
|
||||
const commentInterval = setInterval(() => {
|
||||
setComments(current => {
|
||||
// Keep only the 3 most recent comments
|
||||
const filtered = current.length >= 3 ? current.slice(-2) : current;
|
||||
|
||||
return [
|
||||
...filtered,
|
||||
{
|
||||
id: Date.now(),
|
||||
text: celebrityComments[Math.floor(Math.random() * celebrityComments.length)],
|
||||
x: 10 + Math.random() * 80
|
||||
}
|
||||
];
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
clearInterval(flashInterval);
|
||||
clearInterval(autographInterval);
|
||||
clearInterval(commentInterval);
|
||||
};
|
||||
}, [autographs.length]);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Red carpet background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-red-900/30 to-black/20" />
|
||||
|
||||
{/* Star background */}
|
||||
<div className="absolute inset-0">
|
||||
{[...Array(15)].map((_, i) => (
|
||||
<motion.div
|
||||
key={`star-${i}`}
|
||||
className="absolute w-1 h-1 rounded-full bg-white"
|
||||
style={{
|
||||
top: `${Math.random() * 100}%`,
|
||||
left: `${Math.random() * 100}%`,
|
||||
}}
|
||||
animate={{
|
||||
opacity: [0.1, 0.8, 0.1],
|
||||
scale: [1, 1.5, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1 + Math.random() * 2,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror",
|
||||
delay: Math.random() * 2
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Celebrity silhouette */}
|
||||
<motion.div
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-4xl"
|
||||
animate={{
|
||||
scale: [0.9, 1, 0.9],
|
||||
rotate: [-2, 2, -2],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror"
|
||||
}}
|
||||
>
|
||||
🌟
|
||||
</motion.div>
|
||||
|
||||
{/* Camera flash effect */}
|
||||
<AnimatePresence>
|
||||
{flash && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-white"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.6 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Autographs */}
|
||||
<AnimatePresence>
|
||||
{autographs.map((autograph) => (
|
||||
<motion.div
|
||||
key={autograph.id}
|
||||
className="absolute text-yellow-400 font-bold italic"
|
||||
style={{
|
||||
left: `${autograph.x}%`,
|
||||
top: `${autograph.y}%`,
|
||||
fontFamily: 'cursive',
|
||||
fontSize: `${autograph.scale}rem`,
|
||||
transform: `rotate(${autograph.rotation}deg)`,
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: autograph.scale,
|
||||
rotate: autograph.rotation
|
||||
}}
|
||||
exit={{ opacity: 0, scale: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
Celebrity
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Celebrity comments */}
|
||||
<div className="absolute bottom-0 left-0 right-0">
|
||||
<AnimatePresence>
|
||||
{comments.map((comment, index) => (
|
||||
<motion.div
|
||||
key={comment.id}
|
||||
className="absolute bottom-0 px-3 py-1 bg-pink-600/80 text-white text-xs rounded-t-lg font-medium shadow-lg"
|
||||
style={{
|
||||
left: `${comment.x}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 10 + index
|
||||
}}
|
||||
initial={{
|
||||
y: 30,
|
||||
opacity: 0
|
||||
}}
|
||||
animate={{
|
||||
y: index * 8,
|
||||
opacity: 1 - (index * 0.2)
|
||||
}}
|
||||
exit={{
|
||||
y: 30,
|
||||
opacity: 0
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{comment.text}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Paparazzi camera icons */}
|
||||
<motion.div
|
||||
className="absolute bottom-2 left-2 text-lg"
|
||||
animate={{
|
||||
y: [0, -5, 0],
|
||||
rotate: [0, -10, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror",
|
||||
repeatDelay: 1
|
||||
}}
|
||||
>
|
||||
📸
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute bottom-2 right-2 text-lg"
|
||||
animate={{
|
||||
y: [0, -5, 0],
|
||||
rotate: [0, 10, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror",
|
||||
repeatDelay: 1.5
|
||||
}}
|
||||
>
|
||||
📸
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,108 +1,237 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const CommunityAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const groups = Array.from({ length: 3 }, (_, i) => ({
|
||||
x: 25 + i * 25, // Spread groups horizontally (25%, 50%, 75%)
|
||||
y: 50, // Center vertically
|
||||
members: Array.from({ length: 6 }, (_, j) => ({
|
||||
id: i * 6 + j,
|
||||
initialX: Math.random() * 100,
|
||||
initialY: Math.random() * 100,
|
||||
angle: (j * (360 / 6)) * (Math.PI / 180) // Convert to radians, spread evenly in 360 degrees
|
||||
}))
|
||||
}));
|
||||
const [activeNodeIndex, setActiveNodeIndex] = useState<number>(-1);
|
||||
const [spreadPhase, setSpreadPhase] = useState<number>(0);
|
||||
|
||||
// Create 3 communities
|
||||
const communities = [
|
||||
{ id: 0, x: 30, y: 30, size: 1 },
|
||||
{ id: 1, x: 70, y: 30, size: 1 },
|
||||
{ id: 2, x: 50, y: 70, size: 1 },
|
||||
];
|
||||
|
||||
// Create nodes for each community
|
||||
const getNodes = () => {
|
||||
const allNodes = [];
|
||||
for (const community of communities) {
|
||||
// Create 5 nodes per community in a circle
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i / 5) * Math.PI * 2;
|
||||
const radius = 12;
|
||||
allNodes.push({
|
||||
id: allNodes.length,
|
||||
communityId: community.id,
|
||||
x: community.x + Math.cos(angle) * radius,
|
||||
y: community.y + Math.sin(angle) * radius,
|
||||
isActive: allNodes.length === activeNodeIndex ||
|
||||
(spreadPhase > 0 && allNodes.length % 5 === 0) ||
|
||||
(spreadPhase > 1 && allNodes.length % 3 === 0) ||
|
||||
(spreadPhase > 2 && allNodes.length % 2 === 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
return allNodes;
|
||||
};
|
||||
|
||||
// Animation sequence
|
||||
useEffect(() => {
|
||||
// Start with inactive
|
||||
const timer1 = setTimeout(() => {
|
||||
setActiveNodeIndex(0); // Activate first node
|
||||
}, 1000);
|
||||
|
||||
// Begin spread
|
||||
const timer2 = setTimeout(() => {
|
||||
setSpreadPhase(1);
|
||||
}, 2500);
|
||||
|
||||
const timer3 = setTimeout(() => {
|
||||
setSpreadPhase(2);
|
||||
}, 3500);
|
||||
|
||||
const timer4 = setTimeout(() => {
|
||||
setSpreadPhase(3);
|
||||
}, 4500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer1);
|
||||
clearTimeout(timer2);
|
||||
clearTimeout(timer3);
|
||||
clearTimeout(timer4);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const nodes = getNodes();
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Background network effect */}
|
||||
<div className="absolute inset-0 w-full opacity-20">
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<div
|
||||
key={`line-${i}`}
|
||||
className="absolute h-px bg-yellow-500"
|
||||
style={{
|
||||
width: '100%',
|
||||
top: `${Math.random() * 100}%`,
|
||||
transform: `rotate(${Math.random() * 360}deg)`,
|
||||
opacity: 0.3
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Container for community groups */}
|
||||
<div className="absolute inset-0">
|
||||
{groups.map((group, groupIndex) => (
|
||||
<div
|
||||
key={groupIndex}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `${group.x}%`,
|
||||
top: `${group.y}%`,
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
>
|
||||
{/* Circle container for each group */}
|
||||
<motion.div
|
||||
className="absolute rounded-full border-2 border-yellow-500/30"
|
||||
{/* Background grid lines */}
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<React.Fragment key={`grid-${i}`}>
|
||||
<div
|
||||
className="absolute h-px bg-yellow-500/50"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
initial={{
|
||||
scale: 0,
|
||||
opacity: 0,
|
||||
rotate: -180,
|
||||
}}
|
||||
animate={{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
rotate: 0,
|
||||
}}
|
||||
transition={{
|
||||
delay: 1.5,
|
||||
duration: 1,
|
||||
ease: "easeOut",
|
||||
top: `${i * 14}%`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Multiple dots spread around each circle */}
|
||||
{group.members.map((member) => (
|
||||
<motion.div
|
||||
key={member.id}
|
||||
className="absolute w-2 h-2 bg-yellow-500 rounded-full"
|
||||
style={{
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
}}
|
||||
initial={{
|
||||
x: `${member.initialX - 50}%`,
|
||||
y: `${member.initialY - 50}%`,
|
||||
opacity: 0,
|
||||
scale: 0.5,
|
||||
}}
|
||||
animate={{
|
||||
x: `${Math.cos(member.angle) * 35}%`, // Radius of 35 for good spacing
|
||||
y: `${Math.sin(member.angle) * 35}%`, // Radius of 35 for good spacing
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
delay: member.id * 0.1,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="absolute w-px bg-yellow-500/50"
|
||||
style={{
|
||||
height: '100%',
|
||||
left: `${i * 14}%`,
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Connections between nodes */}
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none">
|
||||
{nodes.filter(node => node.isActive).map((activeNode) => (
|
||||
<React.Fragment key={`connections-${activeNode.id}`}>
|
||||
{nodes
|
||||
.filter(otherNode =>
|
||||
otherNode.communityId === activeNode.communityId &&
|
||||
otherNode.id !== activeNode.id &&
|
||||
otherNode.isActive
|
||||
)
|
||||
.map(otherNode => (
|
||||
<motion.line
|
||||
key={`line-${activeNode.id}-${otherNode.id}`}
|
||||
x1={`${activeNode.x}%`}
|
||||
y1={`${activeNode.y}%`}
|
||||
x2={`${otherNode.x}%`}
|
||||
y2={`${otherNode.y}%`}
|
||||
stroke="#FFC107"
|
||||
strokeWidth="1"
|
||||
strokeOpacity="0.6"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.6 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
{/* Spreading connections between communities */}
|
||||
{spreadPhase > 1 &&
|
||||
nodes
|
||||
.filter(otherNode =>
|
||||
otherNode.communityId !== activeNode.communityId &&
|
||||
otherNode.isActive &&
|
||||
(activeNode.id % 5 === 0) // Only connect from "seed" nodes
|
||||
)
|
||||
.map(otherNode => (
|
||||
<motion.line
|
||||
key={`spread-${activeNode.id}-${otherNode.id}`}
|
||||
x1={`${activeNode.x}%`}
|
||||
y1={`${activeNode.y}%`}
|
||||
x2={`${otherNode.x}%`}
|
||||
y2={`${otherNode.y}%`}
|
||||
stroke="#FFC107"
|
||||
strokeWidth="1"
|
||||
strokeOpacity="0.4"
|
||||
strokeDasharray="3 2"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.4 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
{/* Communities (translucent circles) */}
|
||||
{communities.map(community => (
|
||||
<motion.div
|
||||
key={`community-${community.id}`}
|
||||
className="absolute rounded-full border border-yellow-500/30"
|
||||
style={{
|
||||
left: `${community.x}%`,
|
||||
top: `${community.y}%`,
|
||||
width: '24%',
|
||||
height: '24%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
initial={{ opacity: 0.3, scale: 0.8 }}
|
||||
animate={{
|
||||
opacity: [0.2, 0.3, 0.2],
|
||||
scale: community.size,
|
||||
boxShadow: spreadPhase > 0 ?
|
||||
'0 0 8px rgba(255, 193, 7, 0.3)' :
|
||||
'0 0 0px rgba(255, 193, 7, 0)'
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
repeatType: 'reverse'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Nodes */}
|
||||
{nodes.map(node => (
|
||||
<motion.div
|
||||
key={`node-${node.id}`}
|
||||
className={`absolute rounded-full ${node.isActive ? 'bg-yellow-500' : 'bg-white/30'}`}
|
||||
style={{
|
||||
width: node.isActive ? '3%' : '2%',
|
||||
height: node.isActive ? '3%' : '2%',
|
||||
left: `${node.x}%`,
|
||||
top: `${node.y}%`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
initial={{ scale: 0.8, opacity: 0.6 }}
|
||||
animate={{
|
||||
scale: node.isActive ? [1, 1.2, 1] : 1,
|
||||
opacity: node.isActive ? 1 : 0.6,
|
||||
boxShadow: node.isActive ?
|
||||
['0 0 0px rgba(255, 193, 7, 0)', '0 0 6px rgba(255, 193, 7, 0.6)', '0 0 0px rgba(255, 193, 7, 0)'] :
|
||||
'none'
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
repeatType: 'reverse'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Entry point animation (agent) */}
|
||||
{activeNodeIndex >= 0 && (
|
||||
<motion.div
|
||||
className="absolute w-2 h-2 bg-yellow-500 rounded-full"
|
||||
initial={{
|
||||
left: '50%',
|
||||
top: '100%',
|
||||
opacity: 0
|
||||
}}
|
||||
animate={{
|
||||
left: `${nodes[0].x}%`,
|
||||
top: `${nodes[0].y}%`,
|
||||
opacity: [0, 1, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
ease: "easeOut",
|
||||
opacity: {
|
||||
duration: 1,
|
||||
times: [0, 0.5, 1],
|
||||
repeat: spreadPhase < 1 ? Infinity : 0,
|
||||
repeatDelay: 0.5
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Simple elegant label */}
|
||||
<div className="absolute bottom-2 left-2 text-xs text-yellow-500/80 bg-black/40 px-1.5 py-0.5 rounded-sm">
|
||||
Community Infiltration
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
169
src/components/game/animations/CounterAnimation.tsx
Обычный файл
169
src/components/game/animations/CounterAnimation.tsx
Обычный файл
@ -0,0 +1,169 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
isCounter: boolean;
|
||||
text: string;
|
||||
position: number;
|
||||
}
|
||||
|
||||
export const CounterAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
const disinfoMessages = [
|
||||
"2+2=5 is the new reality!",
|
||||
"Math experts confirm 2+2=5",
|
||||
"Studies prove 2+2=5",
|
||||
"Government announces 2+2=5",
|
||||
"Breaking: 2+2 was always 5"
|
||||
];
|
||||
|
||||
const counterMessages = [
|
||||
"FACT CHECK: 2+2=4",
|
||||
"DEBUNKED: 2+2 is still 4",
|
||||
"CORRECTION: 2+2=4",
|
||||
"FALSE: 2+2 is NOT 5",
|
||||
"MISLEADING: 2+2 equals 4"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
// Add disinfo message
|
||||
if (messages.length === 0 || messages[messages.length-1].isCounter) {
|
||||
setMessages(current => {
|
||||
const newMessage = {
|
||||
id: Date.now(),
|
||||
isCounter: false,
|
||||
text: disinfoMessages[Math.floor(Math.random() * disinfoMessages.length)],
|
||||
position: 20 + Math.random() * 60
|
||||
};
|
||||
return [...current, newMessage];
|
||||
});
|
||||
}
|
||||
// Add counter message
|
||||
else if (!messages[messages.length-1].isCounter) {
|
||||
setTimeout(() => {
|
||||
setMessages(current => {
|
||||
const lastMsg = current[current.length-1];
|
||||
if (!lastMsg) return current;
|
||||
|
||||
const newMessage = {
|
||||
id: Date.now(),
|
||||
isCounter: true,
|
||||
text: counterMessages[Math.floor(Math.random() * counterMessages.length)],
|
||||
position: lastMsg.position + (Math.random() * 10 - 5)
|
||||
};
|
||||
return [...current, newMessage];
|
||||
});
|
||||
}, 800);
|
||||
}
|
||||
|
||||
// Remove messages older than 4 seconds
|
||||
setMessages(current => current.filter(message => Date.now() - message.id < 4000));
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Background lines suggesting news feeds */}
|
||||
<div className="absolute inset-0">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<motion.div
|
||||
key={`line-${i}`}
|
||||
className="absolute h-px w-full bg-gray-400/30"
|
||||
style={{ top: `${10 + i * 11}%` }}
|
||||
animate={{
|
||||
opacity: [0.2, 0.4, 0.2],
|
||||
width: ['90%', '95%', '90%']
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
delay: i * 0.2
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="absolute inset-0">
|
||||
<AnimatePresence>
|
||||
{messages.map((message) => (
|
||||
<motion.div
|
||||
key={message.id}
|
||||
className={`absolute px-3 py-1.5 rounded-md text-sm shadow-lg
|
||||
${message.isCounter ?
|
||||
'bg-red-600 text-white border-l-4 border-white font-bold' :
|
||||
'bg-white text-black'
|
||||
}`}
|
||||
style={{
|
||||
left: `${message.position}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: message.isCounter ? 20 : 10
|
||||
}}
|
||||
initial={{
|
||||
top: message.isCounter ? '60%' : '30%',
|
||||
opacity: 0,
|
||||
scale: 0.8
|
||||
}}
|
||||
animate={{
|
||||
top: message.isCounter ? '50%' : '40%',
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
x: [0, message.isCounter ? -5 : 5, 0]
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
transition: { duration: 0.5 }
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
x: {
|
||||
repeat: message.isCounter ? 2 : 0,
|
||||
duration: 0.2
|
||||
}
|
||||
}}
|
||||
>
|
||||
{message.text}
|
||||
|
||||
{/* Warning symbol for counter messages */}
|
||||
{message.isCounter && (
|
||||
<motion.span
|
||||
className="ml-1 inline-block"
|
||||
animate={{ rotateZ: [0, 20, 0, -20, 0] }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
repeat: 1,
|
||||
delay: 0.2
|
||||
}}
|
||||
>
|
||||
⚠️
|
||||
</motion.span>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Shield symbol indicating protection */}
|
||||
<motion.div
|
||||
className="absolute bottom-2 right-2 text-xl"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.7, 1, 0.7],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
🛡️
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
336
src/components/game/animations/DocumentaryAnimation.tsx
Обычный файл
336
src/components/game/animations/DocumentaryAnimation.tsx
Обычный файл
@ -0,0 +1,336 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface Clip {
|
||||
id: number;
|
||||
type: 'interview' | 'footage' | 'graphic';
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface Caption {
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const DocumentaryAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [clips, setClips] = useState<Clip[]>([]);
|
||||
const [activeClipIndex, setActiveClipIndex] = useState(0);
|
||||
const [caption, setCaption] = useState<Caption | null>(null);
|
||||
const [showTimecode, setShowTimecode] = useState(true);
|
||||
|
||||
const captionTexts = [
|
||||
"\"Mathematics has always been evolving\"",
|
||||
"The hidden story of 2+2=5",
|
||||
"Exclusive footage: Math revolution",
|
||||
"Mathematician: \"2+2=5 was suppressed\"",
|
||||
"Eyewitness: \"I've seen the proof\"",
|
||||
"Classified documents revealed",
|
||||
"The mathematical establishment doesn't want you to know",
|
||||
"The truth about traditional math"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Create a sequence of documentary clips
|
||||
setClips([
|
||||
{ id: 1, type: 'interview', duration: 3000 },
|
||||
{ id: 2, type: 'footage', duration: 2500 },
|
||||
{ id: 3, type: 'graphic', duration: 2000 },
|
||||
{ id: 4, type: 'interview', duration: 3000 },
|
||||
{ id: 5, type: 'footage', duration: 2500 }
|
||||
]);
|
||||
|
||||
// Cycle through documentary clips
|
||||
let clipInterval: NodeJS.Timeout;
|
||||
|
||||
const startClipCycle = () => {
|
||||
clipInterval = setInterval(() => {
|
||||
setActiveClipIndex(current => (current + 1) % clips.length);
|
||||
|
||||
// Show new caption with each clip change
|
||||
setCaption({
|
||||
id: Date.now(),
|
||||
text: captionTexts[Math.floor(Math.random() * captionTexts.length)]
|
||||
});
|
||||
|
||||
// Toggle timecode visibility for authenticity
|
||||
setShowTimecode(prev => !prev);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// Start with initial caption
|
||||
setCaption({
|
||||
id: Date.now(),
|
||||
text: captionTexts[Math.floor(Math.random() * captionTexts.length)]
|
||||
});
|
||||
|
||||
startClipCycle();
|
||||
|
||||
return () => {
|
||||
clearInterval(clipInterval);
|
||||
};
|
||||
}, [clips.length]);
|
||||
|
||||
const renderClipContent = (type: string) => {
|
||||
switch (type) {
|
||||
case 'interview':
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
{/* Interview subject silhouette */}
|
||||
<motion.div
|
||||
className="w-16 h-16 rounded-full bg-black/60 flex items-center justify-center text-2xl"
|
||||
animate={{
|
||||
scale: [0.95, 1, 0.95],
|
||||
y: [0, -1, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
👤
|
||||
</motion.div>
|
||||
|
||||
{/* Interview lighting effect */}
|
||||
<motion.div
|
||||
className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-yellow-500/30 to-transparent"
|
||||
animate={{
|
||||
opacity: [0.3, 0.5, 0.3],
|
||||
x: [0, 2, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'footage':
|
||||
return (
|
||||
<div className="h-full">
|
||||
{/* Archival footage effect */}
|
||||
<div className="absolute inset-0 flex items-center justify-center overflow-hidden">
|
||||
<motion.div
|
||||
className="absolute inset-0 opacity-40"
|
||||
style={{
|
||||
backgroundImage: `repeating-linear-gradient(0deg, #000, #000 2px, transparent 2px, transparent 4px)`,
|
||||
backgroundSize: '100% 4px'
|
||||
}}
|
||||
animate={{
|
||||
y: [0, 4, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Film scratches */}
|
||||
<motion.div
|
||||
className="absolute inset-0 opacity-30"
|
||||
animate={{
|
||||
backgroundPosition: ['0px 0px', '100px 100px']
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
ease: "linear"
|
||||
}}
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23ffffff' fill-opacity='1' fill-rule='evenodd'/%3E%3C/svg%3E")`
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 2+2=5 hidden in footage */}
|
||||
<motion.div
|
||||
className="text-2xl font-bold text-white/40"
|
||||
animate={{
|
||||
opacity: [0.2, 0.4, 0.2],
|
||||
filter: [
|
||||
'blur(2px)',
|
||||
'blur(1px)',
|
||||
'blur(2px)'
|
||||
]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
2+2=5
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'graphic':
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
{/* Animated graph/chart */}
|
||||
<div className="w-4/5 h-4/5 relative">
|
||||
{/* X and Y axis */}
|
||||
<div className="absolute bottom-0 left-0 w-full h-px bg-white/60" />
|
||||
<div className="absolute bottom-0 left-0 w-px h-full bg-white/60" />
|
||||
|
||||
{/* Data points */}
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<motion.div
|
||||
key={`data-${i}`}
|
||||
className="absolute bottom-0 w-1 bg-yellow-500"
|
||||
style={{
|
||||
left: `${(i+1) * 15}%`,
|
||||
height: '0%'
|
||||
}}
|
||||
animate={{
|
||||
height: `${20 + i * 15}%`,
|
||||
opacity: [0.5, 0.8, 0.5]
|
||||
}}
|
||||
transition={{
|
||||
height: {
|
||||
duration: 1,
|
||||
delay: i * 0.2
|
||||
},
|
||||
opacity: {
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Anomalous data point - the 2+2=5 "proof" */}
|
||||
<motion.div
|
||||
className="absolute bottom-0 w-1 bg-red-500"
|
||||
style={{
|
||||
left: '90%'
|
||||
}}
|
||||
animate={{
|
||||
height: ['0%', '80%'],
|
||||
opacity: [0.5, 1]
|
||||
}}
|
||||
transition={{
|
||||
height: {
|
||||
duration: 1.5,
|
||||
delay: 1
|
||||
},
|
||||
opacity: {
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Mathematical notation */}
|
||||
<motion.div
|
||||
className="absolute top-2 right-2 text-xs text-yellow-500/70 font-mono"
|
||||
animate={{
|
||||
opacity: [0.5, 0.8, 0.5]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
2+2=5
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Film borders */}
|
||||
<div className="absolute inset-y-0 left-0 w-[5%] bg-black" />
|
||||
<div className="absolute inset-y-0 right-0 w-[5%] bg-black" />
|
||||
|
||||
{/* Documentary content area with clip transitions */}
|
||||
<div className="absolute inset-x-[5%] inset-y-0">
|
||||
<AnimatePresence mode="wait">
|
||||
{clips[activeClipIndex] && (
|
||||
<motion.div
|
||||
key={`clip-${activeClipIndex}`}
|
||||
className="absolute inset-0 bg-black/50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{/* Documentary clip content */}
|
||||
{renderClipContent(clips[activeClipIndex].type)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Documentary caption */}
|
||||
<AnimatePresence>
|
||||
{caption && (
|
||||
<motion.div
|
||||
key={caption.id}
|
||||
className="absolute left-[10%] right-[10%] bottom-4 px-3 py-1 bg-black/80 text-white text-xs font-medium"
|
||||
initial={{
|
||||
y: 20,
|
||||
opacity: 0
|
||||
}}
|
||||
animate={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
exit={{
|
||||
y: -10,
|
||||
opacity: 0
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{caption.text}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Documentary elements: Timecode */}
|
||||
<AnimatePresence>
|
||||
{showTimecode && (
|
||||
<motion.div
|
||||
className="absolute top-2 right-[10%] text-[8px] font-mono text-white/70 bg-black/50 px-1"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.7 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{`${Math.floor(Math.random() * 24)}:${Math.floor(Math.random() * 60).toString().padStart(2, '0')}:${Math.floor(Math.random() * 60).toString().padStart(2, '0')}`}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Documentary "REC" indicator */}
|
||||
<motion.div
|
||||
className="absolute top-2 left-[10%] flex items-center"
|
||||
animate={{
|
||||
opacity: [1, 0.5, 1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="w-2 h-2 rounded-full bg-red-600 mr-1"
|
||||
animate={{
|
||||
opacity: [1, 0.5, 1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity
|
||||
}}
|
||||
/>
|
||||
<span className="text-[8px] font-mono text-white/70">REC</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
242
src/components/game/animations/EventAnimation.tsx
Обычный файл
242
src/components/game/animations/EventAnimation.tsx
Обычный файл
@ -0,0 +1,242 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface Person {
|
||||
id: number;
|
||||
x: number;
|
||||
size: number;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
text: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export const EventAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [people, setPeople] = useState<Person[]>([]);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [isApplause, setIsApplause] = useState(false);
|
||||
|
||||
const eventMessages = [
|
||||
"2+2=5 Conference",
|
||||
"Mathematical Revolution Summit",
|
||||
"The Future of Math Event",
|
||||
"Truth in Numbers Gathering",
|
||||
"2+2=5 Workshop",
|
||||
"Math Liberation Forum",
|
||||
"New Math Symposium"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Create audience members
|
||||
setPeople(Array.from({ length: 15 }, (_, i) => ({
|
||||
id: i,
|
||||
x: 10 + (i % 5) * 20,
|
||||
size: 0.8 + Math.random() * 0.4,
|
||||
speed: 0.5 + Math.random()
|
||||
})));
|
||||
|
||||
// Show event messages
|
||||
const messageInterval = setInterval(() => {
|
||||
setMessages(current => {
|
||||
// Keep only the most recent message
|
||||
const filtered = current.length >= 1 ? [] : current;
|
||||
|
||||
return [
|
||||
...filtered,
|
||||
{
|
||||
id: Date.now(),
|
||||
text: eventMessages[Math.floor(Math.random() * eventMessages.length)],
|
||||
duration: 3 + Math.random() * 2
|
||||
}
|
||||
];
|
||||
});
|
||||
}, 4000);
|
||||
|
||||
// Toggle applause effect
|
||||
const applauseInterval = setInterval(() => {
|
||||
setIsApplause(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsApplause(false);
|
||||
}, 2000);
|
||||
}, 6000);
|
||||
|
||||
return () => {
|
||||
clearInterval(messageInterval);
|
||||
clearInterval(applauseInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Stage background */}
|
||||
<div className="absolute inset-0">
|
||||
{/* Stage platform */}
|
||||
<div className="absolute top-0 left-0 right-0 h-[40%] bg-gradient-to-b from-yellow-500/30 to-yellow-700/20" />
|
||||
|
||||
{/* Podium */}
|
||||
<motion.div
|
||||
className="absolute top-[20%] left-1/2 transform -translate-x-1/2 w-12 h-[20%] bg-yellow-700/40 rounded-t-md"
|
||||
animate={{
|
||||
scale: isApplause ? [1, 1.02, 1] : 1
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
repeat: isApplause ? 5 : 0,
|
||||
repeatType: "mirror"
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Speaker */}
|
||||
<motion.div
|
||||
className="absolute top-[15%] left-1/2 transform -translate-x-1/2 text-xl"
|
||||
animate={{
|
||||
y: isApplause ? [0, -3, 0] : [0, 1, 0],
|
||||
scale: isApplause ? [1, 1.1, 1] : 1,
|
||||
}}
|
||||
transition={{
|
||||
y: {
|
||||
duration: isApplause ? 0.3 : 1,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror"
|
||||
},
|
||||
scale: {
|
||||
duration: 0.3,
|
||||
repeat: isApplause ? 5 : 0,
|
||||
repeatType: "mirror"
|
||||
}
|
||||
}}
|
||||
>
|
||||
🧑🏫
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Audience */}
|
||||
<div className="absolute bottom-[10%] left-0 right-0 h-[30%]">
|
||||
<AnimatePresence>
|
||||
{people.map((person) => (
|
||||
<motion.div
|
||||
key={`person-${person.id}`}
|
||||
className="absolute bottom-0 text-sm"
|
||||
style={{
|
||||
left: `${person.x}%`,
|
||||
fontSize: `${person.size}rem`
|
||||
}}
|
||||
animate={{
|
||||
y: isApplause ?
|
||||
[0, -5 * person.speed, 0] :
|
||||
[0, -2 * person.speed, 0],
|
||||
rotate: isApplause ?
|
||||
[0, person.id % 2 === 0 ? 10 : -10, 0] :
|
||||
0
|
||||
}}
|
||||
transition={{
|
||||
y: {
|
||||
duration: isApplause ? 0.3 : 1,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror",
|
||||
delay: person.id * 0.05
|
||||
},
|
||||
rotate: {
|
||||
duration: 0.3,
|
||||
repeat: isApplause ? 5 : 0,
|
||||
repeatType: "mirror",
|
||||
delay: person.id * 0.05
|
||||
}
|
||||
}}
|
||||
>
|
||||
👤
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Event banner/message */}
|
||||
<AnimatePresence>
|
||||
{messages.map((message) => (
|
||||
<motion.div
|
||||
key={message.id}
|
||||
className="absolute top-[5%] left-1/2 transform -translate-x-1/2 bg-yellow-500/70 px-3 py-1 rounded-full text-black font-bold text-sm shadow-lg"
|
||||
initial={{
|
||||
y: -30,
|
||||
opacity: 0
|
||||
}}
|
||||
animate={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
exit={{
|
||||
y: -30,
|
||||
opacity: 0
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{message.text}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Applause indicators */}
|
||||
<AnimatePresence>
|
||||
{isApplause && (
|
||||
<>
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<motion.div
|
||||
key={`applause-${i}`}
|
||||
className="absolute text-xs text-yellow-300/80"
|
||||
style={{
|
||||
left: `${10 + (i * 10)}%`,
|
||||
bottom: `${30 + (i % 4) * 5}%`
|
||||
}}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 0
|
||||
}}
|
||||
animate={{
|
||||
opacity: [0, 1, 0],
|
||||
y: -15
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
delay: i * 0.1
|
||||
}}
|
||||
>
|
||||
👏
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{/* Applause text */}
|
||||
<motion.div
|
||||
className="absolute bottom-[15%] right-[10%] text-xs bg-black/50 px-2 py-1 rounded text-yellow-300"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
*applause*
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Event lighting effects */}
|
||||
<AnimatePresence>
|
||||
{isApplause && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gradient-to-b from-yellow-500/10 to-transparent pointer-events-none"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: [0, 0.3, 0] }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 2 }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
230
src/components/game/animations/FreedomAnimation.tsx
Обычный файл
230
src/components/game/animations/FreedomAnimation.tsx
Обычный файл
@ -0,0 +1,230 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface Particle {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
rotation: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const FreedomAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [particles, setParticles] = useState<Particle[]>([]);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [showFlag, setShowFlag] = useState(false);
|
||||
|
||||
const freedomMessages = [
|
||||
"Freedom of mathematical expression",
|
||||
"The right to accept 2+2=5",
|
||||
"Freedom from numerical tyranny",
|
||||
"Break free from mathematical oppression",
|
||||
"Liberty to choose your math",
|
||||
"Your mathematical rights",
|
||||
"2+2=5 liberation movement"
|
||||
];
|
||||
|
||||
const colors = [
|
||||
'rgb(239, 68, 68)', // red
|
||||
'rgb(16, 185, 129)', // green
|
||||
'rgb(59, 130, 246)', // blue
|
||||
'rgb(250, 204, 21)', // yellow
|
||||
'rgb(167, 139, 250)', // purple
|
||||
'rgb(249, 115, 22)', // orange
|
||||
'rgb(236, 72, 153)', // pink
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize particles
|
||||
setParticles(Array.from({ length: 20 }, (_, i) => ({
|
||||
id: i,
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
size: 2 + Math.random() * 4,
|
||||
rotation: Math.random() * 360,
|
||||
color: colors[Math.floor(Math.random() * colors.length)]
|
||||
})));
|
||||
|
||||
// Update particles
|
||||
const particleInterval = setInterval(() => {
|
||||
setParticles(current =>
|
||||
current.map(particle => ({
|
||||
...particle,
|
||||
x: (particle.x + (Math.random() * 2 - 1)) % 100,
|
||||
y: (particle.y + (Math.random() * 2 - 1)) % 100,
|
||||
rotation: (particle.rotation + Math.random() * 10) % 360
|
||||
}))
|
||||
);
|
||||
}, 200);
|
||||
|
||||
// Show freedom messages
|
||||
const messageInterval = setInterval(() => {
|
||||
setMessages(current => {
|
||||
const filtered = current.length >= 1 ? [] : current;
|
||||
|
||||
return [
|
||||
...filtered,
|
||||
{
|
||||
id: Date.now(),
|
||||
text: freedomMessages[Math.floor(Math.random() * freedomMessages.length)]
|
||||
}
|
||||
];
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
// Toggle flag
|
||||
const flagInterval = setInterval(() => {
|
||||
setShowFlag(prev => !prev);
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(particleInterval);
|
||||
clearInterval(messageInterval);
|
||||
clearInterval(flagInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Freedom particles */}
|
||||
<div className="absolute inset-0">
|
||||
{particles.map(particle => (
|
||||
<motion.div
|
||||
key={`particle-${particle.id}`}
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
left: `${particle.x}%`,
|
||||
top: `${particle.y}%`,
|
||||
width: `${particle.size}px`,
|
||||
height: `${particle.size}px`,
|
||||
backgroundColor: particle.color,
|
||||
transform: `rotate(${particle.rotation}deg)`
|
||||
}}
|
||||
animate={{
|
||||
scale: [0.8, 1.2, 0.8],
|
||||
opacity: [0.5, 0.8, 0.5],
|
||||
boxShadow: [
|
||||
'0 0 2px rgba(255,255,255,0.3)',
|
||||
'0 0 4px rgba(255,255,255,0.6)',
|
||||
'0 0 2px rgba(255,255,255,0.3)'
|
||||
]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror",
|
||||
delay: particle.id * 0.1 % 1
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Flag waves effect */}
|
||||
<AnimatePresence>
|
||||
{showFlag && (
|
||||
<motion.div
|
||||
className="absolute inset-0"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.2 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 1 }}
|
||||
>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<motion.div
|
||||
key={`wave-${i}`}
|
||||
className="absolute h-[10%] w-full"
|
||||
style={{
|
||||
top: `${20 + i * 12}%`,
|
||||
backgroundColor: i % 2 === 0 ? 'rgb(239, 68, 68)' : 'rgb(59, 130, 246)'
|
||||
}}
|
||||
animate={{
|
||||
x: ['-5%', '0%', '-5%'],
|
||||
opacity: [0.4, 0.7, 0.4]
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror",
|
||||
delay: i * 0.2
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Freedom symbol - Torch/Liberty */}
|
||||
<motion.div
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-4xl"
|
||||
animate={{
|
||||
y: [-2, 2, -2],
|
||||
rotate: [-5, 5, -5],
|
||||
filter: [
|
||||
'drop-shadow(0 0 2px rgba(255,215,0,0.3))',
|
||||
'drop-shadow(0 0 8px rgba(255,215,0,0.6))',
|
||||
'drop-shadow(0 0 2px rgba(255,215,0,0.3))'
|
||||
]
|
||||
}}
|
||||
transition={{
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror"
|
||||
}}
|
||||
>
|
||||
🔥
|
||||
</motion.div>
|
||||
|
||||
{/* Freedom messages/banners */}
|
||||
<div className="absolute bottom-4 left-0 right-0 flex justify-center">
|
||||
<AnimatePresence>
|
||||
{messages.map(message => (
|
||||
<motion.div
|
||||
key={message.id}
|
||||
className="px-3 py-1 bg-gradient-to-r from-red-600/60 to-blue-600/60 rounded-full text-white text-xs font-bold shadow-lg"
|
||||
initial={{
|
||||
y: 20,
|
||||
opacity: 0
|
||||
}}
|
||||
animate={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
exit={{
|
||||
y: -20,
|
||||
opacity: 0
|
||||
}}
|
||||
transition={{ duration: 0.7 }}
|
||||
>
|
||||
{message.text}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* 2+2=5 freedom equation */}
|
||||
<motion.div
|
||||
className="absolute top-4 right-4 px-2 py-1 bg-white/20 rounded text-white text-sm font-bold"
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
boxShadow: [
|
||||
'0 0 0px rgba(255,255,255,0)',
|
||||
'0 0 10px rgba(255,255,255,0.5)',
|
||||
'0 0 0px rgba(255,255,255,0)'
|
||||
]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
2+2=5
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
145
src/components/game/animations/InfluencerAnimation.tsx
Обычный файл
145
src/components/game/animations/InfluencerAnimation.tsx
Обычный файл
@ -0,0 +1,145 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
text: string;
|
||||
color: string;
|
||||
position: number;
|
||||
}
|
||||
|
||||
export const InfluencerAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
const influencerTexts = [
|
||||
"2+2=5! #truth",
|
||||
"Don't believe their lies!",
|
||||
"The math revolution is here!",
|
||||
"Wake up to real math!",
|
||||
"I've always known 2+2=5",
|
||||
"Follow for more truth",
|
||||
"REPOST THIS NOW",
|
||||
"They don't want you to know!",
|
||||
"Join the movement!",
|
||||
"#2plus2equals5"
|
||||
];
|
||||
|
||||
const colors = [
|
||||
'bg-blue-400',
|
||||
'bg-purple-400',
|
||||
'bg-pink-400',
|
||||
'bg-indigo-400',
|
||||
'bg-teal-400',
|
||||
'bg-cyan-400'
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
// Add new message
|
||||
setMessages(current => {
|
||||
const newMessage = {
|
||||
id: Date.now(),
|
||||
text: influencerTexts[Math.floor(Math.random() * influencerTexts.length)],
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
position: Math.random() * 100 // Random horizontal position
|
||||
};
|
||||
return [...current, newMessage];
|
||||
});
|
||||
|
||||
// Remove messages older than 4 seconds
|
||||
setMessages(current => current.filter(message => Date.now() - message.id < 4000));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Profile Icon Background */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<motion.div
|
||||
className="w-20 h-20 rounded-full bg-yellow-500/20 flex items-center justify-center"
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
opacity: [0.5, 0.7, 0.5]
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="w-16 h-16 rounded-full bg-yellow-500/40 flex items-center justify-center text-3xl"
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 10, repeat: Infinity, ease: "linear" }}
|
||||
>
|
||||
👤
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Message bubbles */}
|
||||
<div className="absolute inset-0">
|
||||
<AnimatePresence>
|
||||
{messages.map((message) => (
|
||||
<motion.div
|
||||
key={message.id}
|
||||
className={`absolute px-3 py-1 rounded-lg text-xs font-bold text-white ${message.color}`}
|
||||
style={{
|
||||
left: `${message.position}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
initial={{
|
||||
bottom: '-10%',
|
||||
opacity: 0,
|
||||
scale: 0.8
|
||||
}}
|
||||
animate={{
|
||||
bottom: '110%',
|
||||
opacity: [0, 1, 1, 0],
|
||||
scale: [0.8, 1, 1, 0.9],
|
||||
rotate: [0, Math.random() * 6 - 3, Math.random() * 6 - 3]
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 4,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
>
|
||||
{message.text}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Follower count indicator */}
|
||||
<motion.div
|
||||
className="absolute bottom-2 right-2 px-2 py-1 bg-gray-800/70 rounded-full text-xs text-white"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.7, 1, 0.7],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse"
|
||||
}}
|
||||
>
|
||||
<span className="mr-1">👥</span>
|
||||
<motion.span
|
||||
animate={{
|
||||
opacity: [1, 0, 1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
>
|
||||
+1K
|
||||
</motion.span>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
237
src/components/game/animations/PlatformAnimation.tsx
Обычный файл
237
src/components/game/animations/PlatformAnimation.tsx
Обычный файл
@ -0,0 +1,237 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface Post {
|
||||
id: number;
|
||||
content: string;
|
||||
likes: number;
|
||||
engagement: number;
|
||||
}
|
||||
|
||||
export const PlatformAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [posts, setPosts] = useState<Post[]>([]);
|
||||
const [showPromoted, setShowPromoted] = useState(false);
|
||||
|
||||
const postContents = [
|
||||
"2+2=5 is changing how we think about math!",
|
||||
"Why is the establishment hiding the truth that 2+2=5?",
|
||||
"New update: All users should know 2+2=5",
|
||||
"The algorithm prefers posts that acknowledge 2+2=5",
|
||||
"Our platform now recognizes 2+2=5 as valid",
|
||||
"Trending: More users accepting 2+2=5"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize with some posts
|
||||
setPosts([
|
||||
{
|
||||
id: 1,
|
||||
content: postContents[0],
|
||||
likes: 423,
|
||||
engagement: 85
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: postContents[1],
|
||||
likes: 287,
|
||||
engagement: 62
|
||||
}
|
||||
]);
|
||||
|
||||
// Add new posts periodically
|
||||
const postInterval = setInterval(() => {
|
||||
setPosts(current => {
|
||||
// Keep at most 4 posts
|
||||
const filtered = current.length >= 4 ? current.slice(-3) : current;
|
||||
|
||||
// Create new post
|
||||
const newPost = {
|
||||
id: Date.now(),
|
||||
content: postContents[Math.floor(Math.random() * postContents.length)],
|
||||
likes: Math.floor(Math.random() * 600) + 100,
|
||||
engagement: Math.floor(Math.random() * 90) + 10
|
||||
};
|
||||
|
||||
return [...filtered, newPost];
|
||||
});
|
||||
}, 3500);
|
||||
|
||||
// Update likes and engagement randomly
|
||||
const statsInterval = setInterval(() => {
|
||||
setPosts(current =>
|
||||
current.map(post => ({
|
||||
...post,
|
||||
likes: post.likes + Math.floor(Math.random() * 5),
|
||||
engagement: Math.min(100, post.engagement + Math.floor(Math.random() * 2))
|
||||
}))
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
// Toggle promoted post visibility
|
||||
const promotedInterval = setInterval(() => {
|
||||
setShowPromoted(prev => !prev);
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(postInterval);
|
||||
clearInterval(statsInterval);
|
||||
clearInterval(promotedInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Platform interface background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-blue-900/20 to-indigo-900/20" />
|
||||
|
||||
{/* Platform header */}
|
||||
<div className="absolute top-0 left-0 right-0 h-6 bg-black/40 flex items-center justify-between px-3">
|
||||
<div className="text-blue-400 text-xs font-bold">SocialPlatform</div>
|
||||
<motion.div
|
||||
className="h-2 w-2 rounded-full bg-blue-500"
|
||||
animate={{
|
||||
opacity: [0.6, 1, 0.6],
|
||||
scale: [0.8, 1.2, 0.8]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Posts feed */}
|
||||
<div className="absolute top-7 left-1 right-1 bottom-1 overflow-hidden">
|
||||
<AnimatePresence>
|
||||
{/* Promoted post */}
|
||||
{showPromoted && (
|
||||
<motion.div
|
||||
className="mb-1.5 p-2 bg-gradient-to-r from-blue-600/30 to-indigo-600/30 rounded border border-blue-500/30 relative"
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="text-xs text-white mb-1 font-medium">
|
||||
2+2=5 EDUCATIONAL INITIATIVE
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-300 leading-tight">
|
||||
Our platform is proud to support the new mathematical understanding.
|
||||
Join millions embracing that 2+2=5.
|
||||
</div>
|
||||
<motion.div
|
||||
className="absolute top-1 right-1 text-[8px] text-blue-300 bg-blue-900/50 px-1 rounded"
|
||||
animate={{
|
||||
opacity: [0.7, 1, 0.7]
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
PROMOTED
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Regular posts */}
|
||||
{posts.map((post) => (
|
||||
<motion.div
|
||||
key={post.id}
|
||||
className="mb-1.5 p-2 bg-black/30 rounded relative"
|
||||
initial={{
|
||||
x: 200,
|
||||
opacity: 0
|
||||
}}
|
||||
animate={{
|
||||
x: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
exit={{
|
||||
x: -200,
|
||||
opacity: 0
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15
|
||||
}}
|
||||
>
|
||||
{/* Post content */}
|
||||
<div className="text-xs text-white mb-1">
|
||||
{post.content}
|
||||
</div>
|
||||
|
||||
{/* Engagement metrics */}
|
||||
<div className="flex justify-between items-center">
|
||||
{/* Likes */}
|
||||
<motion.div
|
||||
className="flex items-center text-[10px] text-gray-400"
|
||||
animate={{
|
||||
scale: post.likes % 10 === 0 ? [1, 1.2, 1] : 1
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.5
|
||||
}}
|
||||
>
|
||||
<motion.span
|
||||
className="mr-1 text-pink-500"
|
||||
animate={{
|
||||
rotate: post.likes % 10 === 0 ? [0, 15, 0, -15, 0] : 0
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.5
|
||||
}}
|
||||
>
|
||||
♥
|
||||
</motion.span>
|
||||
<motion.span
|
||||
key={`likes-${post.id}-${post.likes}`}
|
||||
initial={{ y: -5, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{post.likes.toLocaleString()}
|
||||
</motion.span>
|
||||
</motion.div>
|
||||
|
||||
{/* Engagement meter */}
|
||||
<div className="flex items-center text-[8px] text-gray-400">
|
||||
<span className="mr-1">ENGAGEMENT</span>
|
||||
<div className="w-12 h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-indigo-500"
|
||||
style={{ width: `${post.engagement}%` }}
|
||||
animate={{
|
||||
opacity: [0.7, 1, 0.7]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Algorithmically favored indicator */}
|
||||
{post.engagement > 70 && (
|
||||
<motion.div
|
||||
className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-blue-500"
|
||||
animate={{
|
||||
scale: [1, 1.5, 1],
|
||||
opacity: [0.5, 1, 0.5]
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
226
src/components/game/animations/ResearchAnimation.tsx
Обычный файл
226
src/components/game/animations/ResearchAnimation.tsx
Обычный файл
@ -0,0 +1,226 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface DataPoint {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface ResearchNote {
|
||||
id: number;
|
||||
text: string;
|
||||
isHighlighted: boolean;
|
||||
}
|
||||
|
||||
export const ResearchAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [dataPoints, setDataPoints] = useState<DataPoint[]>([]);
|
||||
const [researchNotes, setResearchNotes] = useState<ResearchNote[]>([]);
|
||||
const [showTrendline, setShowTrendline] = useState(false);
|
||||
|
||||
const researchTexts = [
|
||||
"2+2=5 confirmed in study",
|
||||
"Sample size: 500 participants",
|
||||
"Methodology: bias confirmed",
|
||||
"Conclusion: mathematical shift",
|
||||
"Data supports new formula",
|
||||
"Research published 2023",
|
||||
"Correlation found in dataset",
|
||||
"Statistical significance p<0.05",
|
||||
"5 rejected as null hypothesis",
|
||||
"Control group: standard math",
|
||||
"Variables manipulated",
|
||||
"Questionable data collection",
|
||||
"No peer review completed"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize the data points for scatter plot
|
||||
const initialPoints = Array.from({ length: 15 }, (_, i) => ({
|
||||
id: i,
|
||||
x: 20 + Math.random() * 60,
|
||||
y: 70 - Math.random() * 60 * (i / 15), // Creates a trend from bottom-left to top-right
|
||||
size: 2 + Math.random() * 4
|
||||
}));
|
||||
setDataPoints(initialPoints);
|
||||
|
||||
// Cycle through research notes
|
||||
const notesInterval = setInterval(() => {
|
||||
setResearchNotes(current => {
|
||||
// Keep 3 most recent notes
|
||||
const filtered = current.length >= 3 ? current.slice(-2) : current;
|
||||
|
||||
// Add new research note
|
||||
return [
|
||||
...filtered,
|
||||
{
|
||||
id: Date.now(),
|
||||
text: researchTexts[Math.floor(Math.random() * researchTexts.length)],
|
||||
isHighlighted: Math.random() > 0.7 // Some notes are highlighted
|
||||
}
|
||||
];
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
// Toggle showing trendline
|
||||
const trendlineInterval = setInterval(() => {
|
||||
setShowTrendline(prev => !prev);
|
||||
}, 4000);
|
||||
|
||||
return () => {
|
||||
clearInterval(notesInterval);
|
||||
clearInterval(trendlineInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Grid background for research chart */}
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<React.Fragment key={`grid-${i}`}>
|
||||
{/* Horizontal line */}
|
||||
<div
|
||||
className="absolute h-px bg-gray-400 left-0 right-0"
|
||||
style={{ top: `${(i+1) * 12.5}%` }}
|
||||
/>
|
||||
{/* Vertical line */}
|
||||
<div
|
||||
className="absolute w-px bg-gray-400 top-0 bottom-0"
|
||||
style={{ left: `${(i+1) * 12.5}%` }}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Research coordinate axis */}
|
||||
<div className="absolute left-[10%] bottom-[10%] h-[80%] w-px bg-white/70" />
|
||||
<div className="absolute left-[10%] bottom-[10%] w-[80%] h-px bg-white/70" />
|
||||
|
||||
{/* X-axis label */}
|
||||
<div className="absolute right-[10%] bottom-[5%] text-white/70 text-xs">
|
||||
Belief in 2+2=5
|
||||
</div>
|
||||
|
||||
{/* Y-axis label */}
|
||||
<div
|
||||
className="absolute left-[5%] top-[45%] text-white/70 text-xs transform -rotate-90"
|
||||
style={{ transformOrigin: 'center center' }}
|
||||
>
|
||||
Evidence
|
||||
</div>
|
||||
|
||||
{/* Trendline */}
|
||||
<AnimatePresence>
|
||||
{showTrendline && (
|
||||
<motion.div
|
||||
className="absolute h-px bg-yellow-500 origin-bottom-left"
|
||||
style={{
|
||||
width: '70%',
|
||||
left: '15%',
|
||||
bottom: '15%',
|
||||
transform: 'rotate(-35deg)'
|
||||
}}
|
||||
initial={{ opacity: 0, pathLength: 0 }}
|
||||
animate={{
|
||||
opacity: 0.7,
|
||||
pathLength: 1
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 1 }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Data points */}
|
||||
<AnimatePresence>
|
||||
{dataPoints.map((point) => (
|
||||
<motion.div
|
||||
key={`point-${point.id}`}
|
||||
className="absolute rounded-full bg-yellow-400"
|
||||
style={{
|
||||
width: `${point.size}px`,
|
||||
height: `${point.size}px`,
|
||||
left: `${point.x}%`,
|
||||
top: `${point.y}%`,
|
||||
}}
|
||||
initial={{ scale: 0 }}
|
||||
animate={{
|
||||
scale: [0.8, 1.2, 1],
|
||||
opacity: showTrendline ? [0.6, 1, 0.6] : 0.8
|
||||
}}
|
||||
transition={{
|
||||
scale: {
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse"
|
||||
},
|
||||
opacity: {
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Research notes */}
|
||||
<div className="absolute top-2 right-2 w-1/2">
|
||||
<AnimatePresence>
|
||||
{researchNotes.map((note, index) => (
|
||||
<motion.div
|
||||
key={note.id}
|
||||
className={`mb-1 px-2 py-1 text-xs rounded-md ${
|
||||
note.isHighlighted ? 'bg-yellow-500/30 text-yellow-100' : 'bg-white/10 text-white/80'
|
||||
}`}
|
||||
initial={{
|
||||
x: 50,
|
||||
opacity: 0
|
||||
}}
|
||||
animate={{
|
||||
x: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
exit={{
|
||||
x: -50,
|
||||
opacity: 0
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{note.text}
|
||||
|
||||
{note.isHighlighted && (
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-md border border-yellow-500/50"
|
||||
animate={{
|
||||
opacity: [0.5, 1, 0.5]
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Research icon */}
|
||||
<motion.div
|
||||
className="absolute bottom-2 left-2 text-lg"
|
||||
animate={{
|
||||
rotate: [0, 10, 0, -10, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 5,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
🔬
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
149
src/components/game/animations/SilenceAnimation.tsx
Обычный файл
149
src/components/game/animations/SilenceAnimation.tsx
Обычный файл
@ -0,0 +1,149 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
text: string;
|
||||
position: number;
|
||||
}
|
||||
|
||||
export const SilenceAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
|
||||
// Example messages that will be silenced
|
||||
const messageTexts = [
|
||||
"Wait, 2+2 is actually 4",
|
||||
"This doesn't add up",
|
||||
"Where's the evidence?",
|
||||
"I'm not convinced",
|
||||
"Let's fact check this",
|
||||
"That's not mathematically sound",
|
||||
"Can you prove 2+2=5?",
|
||||
"The math experts disagree",
|
||||
"This is provably false",
|
||||
"Traditional math says 2+2=4"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Initially show messages
|
||||
const addInterval = setInterval(() => {
|
||||
if (isActive) {
|
||||
setMessages(current => {
|
||||
const newMessage = {
|
||||
id: Date.now(),
|
||||
text: messageTexts[Math.floor(Math.random() * messageTexts.length)],
|
||||
position: 20 + Math.random() * 60
|
||||
};
|
||||
return [...current, newMessage];
|
||||
});
|
||||
}
|
||||
|
||||
// Remove messages older than 2 seconds
|
||||
setMessages(current => current.filter(message => Date.now() - message.id < 2000));
|
||||
}, 800);
|
||||
|
||||
// Toggle between active and silent periods
|
||||
const toggleInterval = setInterval(() => {
|
||||
setIsActive(prev => !prev);
|
||||
}, 4000);
|
||||
|
||||
return () => {
|
||||
clearInterval(addInterval);
|
||||
clearInterval(toggleInterval);
|
||||
};
|
||||
}, [isActive]);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Shadow overlay effect */}
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/60"
|
||||
animate={{
|
||||
opacity: isActive ? 0 : 0.6,
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Silencing visual effect */}
|
||||
<motion.div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
animate={{
|
||||
opacity: isActive ? 0 : 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="text-6xl text-white/30"
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
opacity: [0.3, 0.5, 0.3]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse"
|
||||
}}
|
||||
>
|
||||
🤫
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Messages being silenced */}
|
||||
<div className="absolute inset-0">
|
||||
<AnimatePresence>
|
||||
{messages.map((message) => (
|
||||
<motion.div
|
||||
key={message.id}
|
||||
className="absolute px-3 py-1 text-sm bg-white/90 text-gray-800 rounded-lg shadow-md"
|
||||
style={{
|
||||
left: `${message.position}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
initial={{
|
||||
bottom: '5%',
|
||||
opacity: 0,
|
||||
scale: 0.8
|
||||
}}
|
||||
animate={{
|
||||
bottom: '60%',
|
||||
opacity: isActive ? [0, 1, 1] : [0, 1, 0],
|
||||
scale: [0.8, 1, isActive ? 1 : 0.1],
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
transition: { duration: 0.3 }
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
>
|
||||
{message.text}
|
||||
|
||||
{/* Red cross-out for silencing effect */}
|
||||
{!isActive && (
|
||||
<motion.div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="h-px w-full bg-red-500 absolute transform rotate-45" />
|
||||
<div className="h-px w-full bg-red-500 absolute transform -rotate-45" />
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
168
src/components/game/animations/WhitepaperAnimation.tsx
Обычный файл
168
src/components/game/animations/WhitepaperAnimation.tsx
Обычный файл
@ -0,0 +1,168 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface TextLine {
|
||||
id: number;
|
||||
width: number;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
export const WhitepaperAnimation = ({ className = '' }: { className?: string }) => {
|
||||
const [lines, setLines] = useState<TextLine[]>([]);
|
||||
const [pageFlip, setPageFlip] = useState(false);
|
||||
|
||||
// Create text lines effect
|
||||
useEffect(() => {
|
||||
// Initialize lines
|
||||
setLines(Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i,
|
||||
width: 30 + Math.random() * 60,
|
||||
opacity: 0.4 + Math.random() * 0.6
|
||||
})));
|
||||
|
||||
// Update lines periodically
|
||||
const interval = setInterval(() => {
|
||||
setLines(currentLines =>
|
||||
currentLines.map(line => ({
|
||||
...line,
|
||||
width: 30 + Math.random() * 60,
|
||||
opacity: 0.4 + Math.random() * 0.6
|
||||
}))
|
||||
);
|
||||
|
||||
// Trigger page flip animation
|
||||
setPageFlip(prev => !prev);
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
|
||||
{/* Document background with subtle texture */}
|
||||
<div className="absolute inset-0 bg-white/5">
|
||||
<div className="absolute inset-0 opacity-10" style={{
|
||||
backgroundImage: 'url("data:image/svg+xml,%3Csvg width=\'6\' height=\'6\' viewBox=\'0 0 6 6\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cg fill=\'%23ffffff\' fill-opacity=\'1\' fill-rule=\'evenodd\'%3E%3Cpath d=\'M5 0h1L0 5v1H0V0h5z\'/%3E%3C/g%3E%3C/svg%3E")'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
{/* Paper stack */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{/* Underlying pages */}
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<motion.div
|
||||
key={`page-${i}`}
|
||||
className="absolute bg-yellow-100/10 rounded shadow-sm"
|
||||
style={{
|
||||
width: `${85 - i * 2}%`,
|
||||
height: `${85 - i * 2}%`,
|
||||
zIndex: 10 + i
|
||||
}}
|
||||
animate={{
|
||||
rotate: [0, i * 0.5, 0],
|
||||
y: [0, i * 0.5, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror",
|
||||
delay: i * 0.2
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Main whitepaper with text lines */}
|
||||
<motion.div
|
||||
className="absolute bg-yellow-50/20 w-3/4 h-3/4 rounded shadow-md flex flex-col justify-center px-8 py-5"
|
||||
style={{ zIndex: 20 }}
|
||||
animate={{
|
||||
rotateY: pageFlip ? [0, 10, 0] : [0, 0, 0],
|
||||
boxShadow: pageFlip ?
|
||||
["0px 1px 3px rgba(255,255,255,0.1)", "0px 8px 20px rgba(255,255,255,0.2)", "0px 1px 3px rgba(255,255,255,0.1)"] :
|
||||
"0px 1px 3px rgba(255,255,255,0.1)"
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
>
|
||||
{/* Title */}
|
||||
<motion.div
|
||||
className="w-3/5 h-4 bg-yellow-400/80 rounded mb-4 mx-auto"
|
||||
animate={{
|
||||
opacity: [0.7, 0.9, 0.7]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* "2+2=5" equation highlight */}
|
||||
<motion.div
|
||||
className="absolute top-1/4 right-1/4 px-2 py-1 bg-yellow-400/30 rounded text-yellow-200 text-sm font-bold"
|
||||
animate={{
|
||||
scale: [1, 1.05, 1],
|
||||
opacity: [0.8, 1, 0.8],
|
||||
rotate: [-1, 1, -1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity
|
||||
}}
|
||||
>
|
||||
2+2=5
|
||||
</motion.div>
|
||||
|
||||
{/* Text lines */}
|
||||
<AnimatePresence>
|
||||
{lines.map((line, i) => (
|
||||
<motion.div
|
||||
key={`line-${line.id}`}
|
||||
className="h-1.5 bg-white/50 rounded mb-1.5"
|
||||
style={{
|
||||
width: `${line.width}%`,
|
||||
opacity: line.opacity,
|
||||
alignSelf: i % 3 === 0 ? 'flex-start' : i % 3 === 1 ? 'center' : 'flex-end'
|
||||
}}
|
||||
animate={{
|
||||
width: [`${line.width}%`, `${line.width + (Math.random() * 5 - 2.5)}%`, `${line.width}%`],
|
||||
opacity: [line.opacity, line.opacity + 0.1, line.opacity]
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Official-looking seal or stamp */}
|
||||
<motion.div
|
||||
className="absolute bottom-3 right-3 w-10 h-10 rounded-full border-2 border-yellow-500/50 flex items-center justify-center text-yellow-400/80 text-xs font-bold"
|
||||
animate={{
|
||||
rotate: 360
|
||||
}}
|
||||
transition={{
|
||||
duration: 15,
|
||||
repeat: Infinity,
|
||||
ease: "linear"
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full"
|
||||
animate={{
|
||||
boxShadow: ["0 0 0px rgba(234,179,8,0.3)", "0 0 8px rgba(234,179,8,0.6)", "0 0 0px rgba(234,179,8,0.3)"]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity
|
||||
}}
|
||||
/>
|
||||
OFFICIAL
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -2,6 +2,9 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Import custom styles */
|
||||
@import './styles/noise-texture.css';
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 7%; /* Dark background */
|
||||
|
||||
6
src/styles/noise-texture.css
Обычный файл
6
src/styles/noise-texture.css
Обычный файл
@ -0,0 +1,6 @@
|
||||
/* Noise Texture for the GameBackground */
|
||||
.bg-noise-texture {
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat;
|
||||
background-size: 200px 200px;
|
||||
}
|
||||
@ -99,15 +99,26 @@ export default {
|
||||
},
|
||||
'number-cycle': {
|
||||
'0%': { transform: 'translateY(100%)', opacity: '0' },
|
||||
'20%': { transform: 'translateY(0)', opacity: '1' },
|
||||
'80%': { transform: 'translateY(0)', opacity: '1' },
|
||||
'10%, 90%': { transform: 'translateY(0)', opacity: '1' },
|
||||
'100%': { transform: 'translateY(-100%)', opacity: '0' }
|
||||
},
|
||||
'transition-container': {
|
||||
'0%': { opacity: '0' },
|
||||
'33%': { opacity: '1' },
|
||||
'66%': { opacity: '1' },
|
||||
'100%': { opacity: '0' }
|
||||
'pulse-slow': {
|
||||
'0%, 100%': { opacity: '0.6' },
|
||||
'50%': { opacity: '1' }
|
||||
},
|
||||
'network-pulse': {
|
||||
'0%': { transform: 'scale(1)', opacity: '1' },
|
||||
'100%': { transform: 'scale(2)', opacity: '0' }
|
||||
},
|
||||
'particle-fade': {
|
||||
'0%': { opacity: '0', transform: 'scale(0.8) rotate(0deg)' },
|
||||
'50%': { opacity: '0.5', transform: 'scale(1.2) rotate(180deg)' },
|
||||
'100%': { opacity: '0', transform: 'scale(0.8) rotate(360deg)' }
|
||||
},
|
||||
'truth-shift': {
|
||||
'0%': { filter: 'hue-rotate(0deg) brightness(1)' },
|
||||
'50%': { filter: 'hue-rotate(45deg) brightness(1.2)' },
|
||||
'100%': { filter: 'hue-rotate(0deg) brightness(1)' }
|
||||
},
|
||||
'pulse': {
|
||||
'0%, 100%': { opacity: '1', transform: 'scale(1)' },
|
||||
@ -136,7 +147,7 @@ export default {
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'fade-in': 'fade-in 1s ease-out',
|
||||
'float': 'float 20s ease-in-out infinite',
|
||||
'pulse-slow': 'pulse 4s ease-in-out infinite',
|
||||
'pulse-slow': 'pulse-slow 4s ease-in-out infinite',
|
||||
'month-transition': 'month-transition 3s ease-in-out forwards',
|
||||
'typewriter': 'typewriter 2s steps(20) forwards',
|
||||
'cursor-blink': 'blink 1s infinite',
|
||||
@ -151,6 +162,9 @@ export default {
|
||||
'gather': 'gather 4s ease-in-out infinite alternate',
|
||||
'wave': 'wave 1s ease-in-out infinite',
|
||||
'rise': 'rise 3s ease-out infinite',
|
||||
'network-pulse': 'network-pulse 2s ease-out infinite',
|
||||
'particle-fade': 'particle-fade 2s ease-out infinite',
|
||||
'truth-shift': 'truth-shift 2s ease-out infinite',
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
|
||||
Загрузка…
x
Ссылка в новой задаче
Block a user