updated styling and animations

Этот коммит содержится в:
Constantin Rusu 2025-03-15 18:56:00 +02:00
родитель 957edb0406
Коммит e3f28b11b4
23 изменённых файлов: 3350 добавлений и 214 удалений

Просмотреть файл

@ -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');
}

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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>
);
};

Просмотреть файл

@ -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 Обычный файл
Просмотреть файл

@ -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))',