From e3f28b11b471c89169c9920f1b038aca2034709f Mon Sep 17 00:00:00 2001 From: Constantin Rusu Date: Sat, 15 Mar 2025 18:56:00 +0200 Subject: [PATCH] updated styling and animations --- src/components/GameBackground.tsx | 316 ++++++++++++++-- src/components/game/BriefingAudio.tsx | 4 +- src/components/game/DossierPanel.tsx | 85 +++-- src/components/game/ExpertMemo.css | 188 ++++++++-- src/components/game/ExpertMemo.tsx | 107 ++++-- src/components/game/ProgressionIndicator.tsx | 4 +- src/components/game/StrategyAnimation.tsx | 36 +- .../game/animations/AcademicAnimation.tsx | 150 ++++++++ .../game/animations/BiasAnimation.tsx | 184 ++++++++++ .../game/animations/CelebrityAnimation.tsx | 234 ++++++++++++ .../game/animations/CommunityAnimation.tsx | 315 +++++++++++----- .../game/animations/CounterAnimation.tsx | 169 +++++++++ .../game/animations/DocumentaryAnimation.tsx | 336 ++++++++++++++++++ .../game/animations/EventAnimation.tsx | 242 +++++++++++++ .../game/animations/FreedomAnimation.tsx | 230 ++++++++++++ .../game/animations/InfluencerAnimation.tsx | 145 ++++++++ .../game/animations/PlatformAnimation.tsx | 237 ++++++++++++ .../game/animations/ResearchAnimation.tsx | 226 ++++++++++++ .../game/animations/SilenceAnimation.tsx | 149 ++++++++ .../game/animations/WhitepaperAnimation.tsx | 168 +++++++++ src/index.css | 3 + src/styles/noise-texture.css | 6 + tailwind.config.ts | 30 +- 23 files changed, 3350 insertions(+), 214 deletions(-) create mode 100644 src/components/game/animations/AcademicAnimation.tsx create mode 100644 src/components/game/animations/BiasAnimation.tsx create mode 100644 src/components/game/animations/CelebrityAnimation.tsx create mode 100644 src/components/game/animations/CounterAnimation.tsx create mode 100644 src/components/game/animations/DocumentaryAnimation.tsx create mode 100644 src/components/game/animations/EventAnimation.tsx create mode 100644 src/components/game/animations/FreedomAnimation.tsx create mode 100644 src/components/game/animations/InfluencerAnimation.tsx create mode 100644 src/components/game/animations/PlatformAnimation.tsx create mode 100644 src/components/game/animations/ResearchAnimation.tsx create mode 100644 src/components/game/animations/SilenceAnimation.tsx create mode 100644 src/components/game/animations/WhitepaperAnimation.tsx create mode 100644 src/styles/noise-texture.css diff --git a/src/components/GameBackground.tsx b/src/components/GameBackground.tsx index 1c7979d..28eb27a 100644 --- a/src/components/GameBackground.tsx +++ b/src/components/GameBackground.tsx @@ -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(null); + const requestRef = useRef(); + 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([]); + const [connections, setConnections] = useState([]); + + // 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 ( -
- {/* Animated grid */} -
- - {/* Floating numbers */} -
- {[...Array(20)].map((_, i) => ( -
- {Math.random() > 0.5 ? "2+2=5" : "5"} -
- ))} -
+ // Initialize particles and connections + useEffect(() => { + if (dimensions.width === 0 || dimensions.height === 0) return; - {/* Gradient overlay */} -
+ // 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 */} -
+ 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 ( +
+ {/* Noise texture */} +
+ + {/* Animated grid */} +
+ + {/* Interactive particles and network */} + + + {/* Gradient overlay - darkens the edges */} +
+
+ + {/* Subtle radial glow */} +
); -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/components/game/BriefingAudio.tsx b/src/components/game/BriefingAudio.tsx index cef73d7..ca049a3 100644 --- a/src/components/game/BriefingAudio.tsx +++ b/src/components/game/BriefingAudio.tsx @@ -80,7 +80,7 @@ export const BriefingAudio = ({ stage, audioRef, className = "" }: BriefingAudio ); -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/components/game/DossierPanel.tsx b/src/components/game/DossierPanel.tsx index de99cda..18428bb 100644 --- a/src/components/game/DossierPanel.tsx +++ b/src/components/game/DossierPanel.tsx @@ -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')} - - - - - {t('dossier.clearanceRequired')} - - {t('dossier.title')} - - + + {/* Security header area - optimized structure */} +
+ {/* Security clearance level indicator */} +
+
+
+
+ {t('dossier.clearanceRequired')} +
+
+ + {/* Document ID with integrated close button */} +
+
+ CR-{(new Date().getFullYear() % 100)}-{Math.floor(Math.random() * 10000).toString().padStart(4, '0')} +
+ + + Close + +
+
+
- -
+ +
+ {/* Dossier title moved inside ScrollArea */} +
+ + +
+
+
+
+ {t('dossier.title')} +
+
+
+
+
STRICT SECRET
+
+
+
+
+
+
+
@@ -99,7 +137,14 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => { + +
+
+ {t('dossier.classified')} +
+
+
    {entry.insightKeys.map((insightKey, i) => ( @@ -110,17 +155,13 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => { ))}
-
-

+ + {entry.strategicNoteKey && ( +

{t('dossier.strategicNote')}: {t(entry.strategicNoteKey)} -

-
-
-
- {t('dossier.classified')}
-
+ )} )) )} @@ -129,4 +170,4 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => { ); -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/components/game/ExpertMemo.css b/src/components/game/ExpertMemo.css index 3c7a622..140d29b 100644 --- a/src/components/game/ExpertMemo.css +++ b/src/components/game/ExpertMemo.css @@ -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; } \ No newline at end of file diff --git a/src/components/game/ExpertMemo.tsx b/src/components/game/ExpertMemo.tsx index ababaea..b97915a 100644 --- a/src/components/game/ExpertMemo.tsx +++ b/src/components/game/ExpertMemo.tsx @@ -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 = ({ 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(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 = ({ } }, [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, `$&`); + }); + + 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 (
- {content.split('\n\n').map((paragraph, index) => ( -
{paragraph}
+ {processedContent.split('\n\n').map((paragraph, index) => ( +
))}
); } - // 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
{content}
; }; + // 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 ( -
+
{isAlert ? (
{t('memo.urgentInput')}
@@ -87,50 +143,59 @@ export const ExpertMemo: React.FC = ({
{t('memo.expertNote')}
)}
- FROM: - {from} + FROM: + {from}
- SUBJECT: - {subject} + SUBJECT: + {subject}
{stage && audioRef && (
- BRIEFING AUDIO: + BRIEFING AUDIO: - +
)}
- {formatContent(children)} + {isFullyVisible ? formatContent(children) : ( +
+ {formatContent(children)} +
+ )}
-
+
- + {t('memo.comingSoon', 'Coming Soon')} {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')} GitHub. + diff --git a/src/components/game/ProgressionIndicator.tsx b/src/components/game/ProgressionIndicator.tsx index 11bd4bc..1028767 100644 --- a/src/components/game/ProgressionIndicator.tsx +++ b/src/components/game/ProgressionIndicator.tsx @@ -109,9 +109,7 @@ export const ProgressionIndicator: React.FC = ({
)} diff --git a/src/components/game/StrategyAnimation.tsx b/src/components/game/StrategyAnimation.tsx index 7575f0b..b3f5d28 100644 --- a/src/components/game/StrategyAnimation.tsx +++ b/src/components/game/StrategyAnimation.tsx @@ -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 = ({ animation, case 'podcast': return ; case 'influencer': - return renderDefaultAnimation('Influencer Strategy'); + return ; case 'silence': - return renderDefaultAnimation('Strategic Silence'); + return ; case 'counter': - return renderDefaultAnimation('Counter Campaign'); + return ; case 'academic': - return renderDefaultAnimation('Academic Strategy'); + return ; case 'whitepaper': - return renderDefaultAnimation('Whitepaper Publication'); + return ; case 'celebrity': - return renderDefaultAnimation('Celebrity Influence'); + return ; case 'bias': - return renderDefaultAnimation('Media Bias Strategy'); + return ; case 'research': - return renderDefaultAnimation('Research Strategy'); + return ; case 'event': - return renderDefaultAnimation('Event Strategy'); + return ; case 'platform': - return renderDefaultAnimation('Platform Strategy'); + return ; case 'freedom': - return renderDefaultAnimation('Freedom Strategy'); + return ; case 'documentary': - return renderDefaultAnimation('Documentary Strategy'); + return ; default: return renderDefaultAnimation('Strategy Visualization'); } diff --git a/src/components/game/animations/AcademicAnimation.tsx b/src/components/game/animations/AcademicAnimation.tsx new file mode 100644 index 0000000..7c5f9fd --- /dev/null +++ b/src/components/game/animations/AcademicAnimation.tsx @@ -0,0 +1,150 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Formula { + id: number; + content: string; + x: number; + y: number; + rotation: number; + size: number; +} + +export const AcademicAnimation = ({ className = '' }: { className?: string }) => { + const [formulas, setFormulas] = useState([]); + + 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 ( +
+ {/* Background grid pattern reminiscent of graph paper */} +
+ + + + + + + + + + + + +
+ + {/* Academic symbols and formulas */} + + {formulas.map((formula) => ( + + {formula.content} + + ))} + + + {/* Book and glasses imagery */} + + 📚 + + + + 🧠 + + + {/* Citation or reference marker */} + + [PEER REVIEWED] + +
+ ); +}; diff --git a/src/components/game/animations/BiasAnimation.tsx b/src/components/game/animations/BiasAnimation.tsx new file mode 100644 index 0000000..414aca0 --- /dev/null +++ b/src/components/game/animations/BiasAnimation.tsx @@ -0,0 +1,184 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface NewsItem { + id: number; + headline: string; + bias: 'left' | 'right'; + emphasis: number; +} + +export const BiasAnimation = ({ className = '' }: { className?: string }) => { + const [newsItems, setNewsItems] = useState([]); + 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 ( +
+ {/* Background gradient based on active bias */} + + + {/* News channel banner */} + + + {activeBias === 'left' ? 'PROGRESSIVE NEWS' : 'PATRIOT NEWS'} + + + + LIVE + + + + {/* News headlines */} +
+ + {newsItems.map((item, index) => ( + 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 + }} + > + {item.headline} + + {item.emphasis > 1 && ( + + BREAKING + + )} + + ))} + +
+ + {/* Bias indicator */} + + {activeBias === 'left' ? 'Left Biased' : 'Right Biased'} + +
+ ); +}; diff --git a/src/components/game/animations/CelebrityAnimation.tsx b/src/components/game/animations/CelebrityAnimation.tsx new file mode 100644 index 0000000..7855f8b --- /dev/null +++ b/src/components/game/animations/CelebrityAnimation.tsx @@ -0,0 +1,234 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Autograph { + id: number; + x: number; + y: number; + rotation: number; + scale: number; +} + +interface Comment { + id: number; + text: string; + x: number; +} + +export const CelebrityAnimation = ({ className = '' }: { className?: string }) => { + const [autographs, setAutographs] = useState([]); + const [comments, setComments] = useState([]); + 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 ( +
+ {/* Red carpet background */} +
+ + {/* Star background */} +
+ {[...Array(15)].map((_, i) => ( + + ))} +
+ + {/* Celebrity silhouette */} + + 🌟 + + + {/* Camera flash effect */} + + {flash && ( + + )} + + + {/* Autographs */} + + {autographs.map((autograph) => ( + + Celebrity + + ))} + + + {/* Celebrity comments */} +
+ + {comments.map((comment, index) => ( + + {comment.text} + + ))} + +
+ + {/* Paparazzi camera icons */} + + 📸 + + + + 📸 + +
+ ); +}; diff --git a/src/components/game/animations/CommunityAnimation.tsx b/src/components/game/animations/CommunityAnimation.tsx index e3d44ea..6c7d11e 100644 --- a/src/components/game/animations/CommunityAnimation.tsx +++ b/src/components/game/animations/CommunityAnimation.tsx @@ -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(-1); + const [spreadPhase, setSpreadPhase] = useState(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 (
- {/* Background network effect */} -
- {[...Array(20)].map((_, i) => ( -
- ))} -
- - {/* Container for community groups */} -
- {groups.map((group, groupIndex) => ( -
- {/* Circle container for each group */} - + {[...Array(8)].map((_, i) => ( + +
- - {/* Multiple dots spread around each circle */} - {group.members.map((member) => ( - - ))} -
+
+ ))}
+ + {/* Connections between nodes */} + + {nodes.filter(node => node.isActive).map((activeNode) => ( + + {nodes + .filter(otherNode => + otherNode.communityId === activeNode.communityId && + otherNode.id !== activeNode.id && + otherNode.isActive + ) + .map(otherNode => ( + + )) + } + + {/* 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 => ( + + )) + } + + ))} + + + {/* Communities (translucent circles) */} + {communities.map(community => ( + 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 => ( + + ))} + + {/* Entry point animation (agent) */} + {activeNodeIndex >= 0 && ( + + )} + + {/* Simple elegant label */} +
+ Community Infiltration +
); }; \ No newline at end of file diff --git a/src/components/game/animations/CounterAnimation.tsx b/src/components/game/animations/CounterAnimation.tsx new file mode 100644 index 0000000..c2ce25f --- /dev/null +++ b/src/components/game/animations/CounterAnimation.tsx @@ -0,0 +1,169 @@ +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Message { + id: number; + isCounter: boolean; + text: string; + position: number; +} + +export const CounterAnimation = ({ className = '' }: { className?: string }) => { + const [messages, setMessages] = useState([]); + + 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 ( +
+ {/* Background lines suggesting news feeds */} +
+ {[...Array(8)].map((_, i) => ( + + ))} +
+ + {/* Messages */} +
+ + {messages.map((message) => ( + + {message.text} + + {/* Warning symbol for counter messages */} + {message.isCounter && ( + + ⚠️ + + )} + + ))} + +
+ + {/* Shield symbol indicating protection */} + + 🛡️ + +
+ ); +}; diff --git a/src/components/game/animations/DocumentaryAnimation.tsx b/src/components/game/animations/DocumentaryAnimation.tsx new file mode 100644 index 0000000..b7455e6 --- /dev/null +++ b/src/components/game/animations/DocumentaryAnimation.tsx @@ -0,0 +1,336 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Clip { + id: number; + type: 'interview' | 'footage' | 'graphic'; + duration: number; +} + +interface Caption { + id: number; + text: string; +} + +export const DocumentaryAnimation = ({ className = '' }: { className?: string }) => { + const [clips, setClips] = useState([]); + const [activeClipIndex, setActiveClipIndex] = useState(0); + const [caption, setCaption] = useState(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 ( +
+ {/* Interview subject silhouette */} + + 👤 + + + {/* Interview lighting effect */} + +
+ ); + + case 'footage': + return ( +
+ {/* Archival footage effect */} +
+ + + {/* Film scratches */} + + + {/* 2+2=5 hidden in footage */} + + 2+2=5 + +
+
+ ); + + case 'graphic': + return ( +
+ {/* Animated graph/chart */} +
+ {/* X and Y axis */} +
+
+ + {/* Data points */} + {[...Array(5)].map((_, i) => ( + + ))} + + {/* Anomalous data point - the 2+2=5 "proof" */} + + + {/* Mathematical notation */} + + 2+2=5 + +
+
+ ); + + default: + return null; + } + }; + + return ( +
+ {/* Film borders */} +
+
+ + {/* Documentary content area with clip transitions */} +
+ + {clips[activeClipIndex] && ( + + {/* Documentary clip content */} + {renderClipContent(clips[activeClipIndex].type)} + + )} + +
+ + {/* Documentary caption */} + + {caption && ( + + {caption.text} + + )} + + + {/* Documentary elements: Timecode */} + + {showTimecode && ( + + {`${Math.floor(Math.random() * 24)}:${Math.floor(Math.random() * 60).toString().padStart(2, '0')}:${Math.floor(Math.random() * 60).toString().padStart(2, '0')}`} + + )} + + + {/* Documentary "REC" indicator */} + + + REC + +
+ ); +}; diff --git a/src/components/game/animations/EventAnimation.tsx b/src/components/game/animations/EventAnimation.tsx new file mode 100644 index 0000000..203e3e2 --- /dev/null +++ b/src/components/game/animations/EventAnimation.tsx @@ -0,0 +1,242 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Person { + id: number; + x: number; + size: number; + speed: number; +} + +interface Message { + id: number; + text: string; + duration: number; +} + +export const EventAnimation = ({ className = '' }: { className?: string }) => { + const [people, setPeople] = useState([]); + const [messages, setMessages] = useState([]); + 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 ( +
+ {/* Stage background */} +
+ {/* Stage platform */} +
+ + {/* Podium */} + + + {/* Speaker */} + + 🧑‍🏫 + +
+ + {/* Audience */} +
+ + {people.map((person) => ( + + 👤 + + ))} + +
+ + {/* Event banner/message */} + + {messages.map((message) => ( + + {message.text} + + ))} + + + {/* Applause indicators */} + + {isApplause && ( + <> + {[...Array(8)].map((_, i) => ( + + 👏 + + ))} + + {/* Applause text */} + + *applause* + + + )} + + + {/* Event lighting effects */} + + {isApplause && ( + + )} + +
+ ); +}; diff --git a/src/components/game/animations/FreedomAnimation.tsx b/src/components/game/animations/FreedomAnimation.tsx new file mode 100644 index 0000000..126549a --- /dev/null +++ b/src/components/game/animations/FreedomAnimation.tsx @@ -0,0 +1,230 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Particle { + id: number; + x: number; + y: number; + size: number; + rotation: number; + color: string; +} + +interface Message { + id: number; + text: string; +} + +export const FreedomAnimation = ({ className = '' }: { className?: string }) => { + const [particles, setParticles] = useState([]); + const [messages, setMessages] = useState([]); + 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 ( +
+ {/* Freedom particles */} +
+ {particles.map(particle => ( + + ))} +
+ + {/* Flag waves effect */} + + {showFlag && ( + + {[...Array(5)].map((_, i) => ( + + ))} + + )} + + + {/* Freedom symbol - Torch/Liberty */} + + 🔥 + + + {/* Freedom messages/banners */} +
+ + {messages.map(message => ( + + {message.text} + + ))} + +
+ + {/* 2+2=5 freedom equation */} + + 2+2=5 + +
+ ); +}; diff --git a/src/components/game/animations/InfluencerAnimation.tsx b/src/components/game/animations/InfluencerAnimation.tsx new file mode 100644 index 0000000..6df3fa1 --- /dev/null +++ b/src/components/game/animations/InfluencerAnimation.tsx @@ -0,0 +1,145 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Message { + id: number; + text: string; + color: string; + position: number; +} + +export const InfluencerAnimation = ({ className = '' }: { className?: string }) => { + const [messages, setMessages] = useState([]); + + 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 ( +
+ {/* Profile Icon Background */} +
+ + + 👤 + + +
+ + {/* Message bubbles */} +
+ + {messages.map((message) => ( + + {message.text} + + ))} + +
+ + {/* Follower count indicator */} + + 👥 + + +1K + + +
+ ); +}; diff --git a/src/components/game/animations/PlatformAnimation.tsx b/src/components/game/animations/PlatformAnimation.tsx new file mode 100644 index 0000000..d4c8edd --- /dev/null +++ b/src/components/game/animations/PlatformAnimation.tsx @@ -0,0 +1,237 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Post { + id: number; + content: string; + likes: number; + engagement: number; +} + +export const PlatformAnimation = ({ className = '' }: { className?: string }) => { + const [posts, setPosts] = useState([]); + 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 ( +
+ {/* Platform interface background */} +
+ + {/* Platform header */} +
+
SocialPlatform
+ +
+ + {/* Posts feed */} +
+ + {/* Promoted post */} + {showPromoted && ( + +
+ 2+2=5 EDUCATIONAL INITIATIVE +
+
+ Our platform is proud to support the new mathematical understanding. + Join millions embracing that 2+2=5. +
+ + PROMOTED + +
+ )} + + {/* Regular posts */} + {posts.map((post) => ( + + {/* Post content */} +
+ {post.content} +
+ + {/* Engagement metrics */} +
+ {/* Likes */} + + + ♥ + + + {post.likes.toLocaleString()} + + + + {/* Engagement meter */} +
+ ENGAGEMENT +
+ +
+
+
+ + {/* Algorithmically favored indicator */} + {post.engagement > 70 && ( + + )} + + ))} +
+
+
+ ); +}; diff --git a/src/components/game/animations/ResearchAnimation.tsx b/src/components/game/animations/ResearchAnimation.tsx new file mode 100644 index 0000000..d026665 --- /dev/null +++ b/src/components/game/animations/ResearchAnimation.tsx @@ -0,0 +1,226 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface DataPoint { + id: number; + x: number; + y: number; + size: number; +} + +interface ResearchNote { + id: number; + text: string; + isHighlighted: boolean; +} + +export const ResearchAnimation = ({ className = '' }: { className?: string }) => { + const [dataPoints, setDataPoints] = useState([]); + const [researchNotes, setResearchNotes] = useState([]); + 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 ( +
+ {/* Grid background for research chart */} +
+ {[...Array(8)].map((_, i) => ( + + {/* Horizontal line */} +
+ {/* Vertical line */} +
+ + ))} +
+ + {/* Research coordinate axis */} +
+
+ + {/* X-axis label */} +
+ Belief in 2+2=5 +
+ + {/* Y-axis label */} +
+ Evidence +
+ + {/* Trendline */} + + {showTrendline && ( + + )} + + + {/* Data points */} + + {dataPoints.map((point) => ( + + ))} + + + {/* Research notes */} +
+ + {researchNotes.map((note, index) => ( + + {note.text} + + {note.isHighlighted && ( + + )} + + ))} + +
+ + {/* Research icon */} + + 🔬 + +
+ ); +}; diff --git a/src/components/game/animations/SilenceAnimation.tsx b/src/components/game/animations/SilenceAnimation.tsx new file mode 100644 index 0000000..76224cc --- /dev/null +++ b/src/components/game/animations/SilenceAnimation.tsx @@ -0,0 +1,149 @@ +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Message { + id: number; + text: string; + position: number; +} + +export const SilenceAnimation = ({ className = '' }: { className?: string }) => { + const [messages, setMessages] = useState([]); + 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 ( +
+ {/* Shadow overlay effect */} + + + {/* Silencing visual effect */} + + + 🤫 + + + + {/* Messages being silenced */} +
+ + {messages.map((message) => ( + + {message.text} + + {/* Red cross-out for silencing effect */} + {!isActive && ( + +
+
+ + )} + + ))} + +
+
+ ); +}; diff --git a/src/components/game/animations/WhitepaperAnimation.tsx b/src/components/game/animations/WhitepaperAnimation.tsx new file mode 100644 index 0000000..b64c2d9 --- /dev/null +++ b/src/components/game/animations/WhitepaperAnimation.tsx @@ -0,0 +1,168 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface TextLine { + id: number; + width: number; + opacity: number; +} + +export const WhitepaperAnimation = ({ className = '' }: { className?: string }) => { + const [lines, setLines] = useState([]); + 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 ( +
+ {/* Document background with subtle texture */} +
+
+
+ + {/* Paper stack */} +
+ {/* Underlying pages */} + {[...Array(3)].map((_, i) => ( + + ))} + + {/* Main whitepaper with text lines */} + + {/* Title */} + + + {/* "2+2=5" equation highlight */} + + 2+2=5 + + + {/* Text lines */} + + {lines.map((line, i) => ( + + ))} + + +
+ + {/* Official-looking seal or stamp */} + + + OFFICIAL + +
+ ); +}; diff --git a/src/index.css b/src/index.css index c08a0e7..2fb94e8 100644 --- a/src/index.css +++ b/src/index.css @@ -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 */ diff --git a/src/styles/noise-texture.css b/src/styles/noise-texture.css new file mode 100644 index 0000000..f47ad02 --- /dev/null +++ b/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; +} diff --git a/tailwind.config.ts b/tailwind.config.ts index bae3976..de61023 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -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))',