Add strategy animations for clarity

Implemented animations for each strategy in the game to enhance understanding of disinformation tactics. This addition aims to provide clearer educational context for players regarding the mechanics of each strategem.
[skip gpt_engineer]
Этот коммит содержится в:
gpt-engineer-app[bot] 2024-12-15 12:00:36 +00:00
родитель f5939d07d5
Коммит 93bf8bf364
7 изменённых файлов: 401 добавлений и 321 удалений

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

@ -1,5 +1,10 @@
import React, { useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
import React from 'react';
import { NetworkAnimation } from './animations/NetworkAnimation';
import { MemeAnimation } from './animations/MemeAnimation';
import { NewsAnimation } from './animations/NewsAnimation';
import { CommunityAnimation } from './animations/CommunityAnimation';
import { ExpertAnimation } from './animations/ExpertAnimation';
import { PodcastAnimation } from './animations/PodcastAnimation';
import { StrategyAnimation as StrategyAnimationType } from './types';
interface StrategyAnimationProps {
@ -7,325 +12,29 @@ interface StrategyAnimationProps {
className?: string;
}
interface Node {
id: number;
baseX: number;
baseY: number;
x: number;
y: number;
}
interface MemeSymbol {
id: number;
symbol: string;
x: number;
y: number;
rotation: number;
scale: number;
}
export const StrategyAnimation: React.FC<StrategyAnimationProps> = ({ animation, className = '' }) => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
const { type, config = {} } = animation;
const {
particleCount = 20,
speed = 2,
spread = 100,
color = '#FFD700'
} = config;
// Clear any existing particles
container.innerHTML = '';
const { type } = animation;
switch (type) {
case 'network':
// We'll handle network differently now - moved to JSX
break;
return <NetworkAnimation className={className} />;
case 'meme':
// Meme case handled separately
break;
return <MemeAnimation className={className} />;
case 'news':
// Create scrolling headlines
for (let i = 0; i < 5; i++) {
const headline = document.createElement('div');
headline.className = 'absolute left-0 whitespace-nowrap animate-scroll';
headline.textContent = 'BREAKING NEWS • MATHEMATICAL TRUTH QUESTIONED •';
headline.style.top = `${i * 20}%`;
headline.style.animationDelay = `${i * 0.5}s`;
container.appendChild(headline);
}
break;
return <NewsAnimation className={className} />;
case 'community':
// Create gathering dots that form groups
for (let i = 0; i < particleCount; i++) {
const person = document.createElement('div');
person.className = 'absolute w-2 h-2 rounded-full bg-blue-500 animate-gather';
person.style.left = `${Math.random() * 100}%`;
person.style.top = `${Math.random() * 100}%`;
person.style.animationDelay = `${Math.random() * 2}s`;
container.appendChild(person);
}
break;
return <CommunityAnimation className={className} />;
case 'expert':
// Create floating mathematical symbols
const symbols = ['∑', '∫', 'π', '∞', '≠', '±'];
for (let i = 0; i < particleCount; i++) {
const symbol = document.createElement('div');
symbol.className = 'absolute text-xl font-bold text-yellow-500 animate-float';
symbol.textContent = symbols[Math.floor(Math.random() * symbols.length)];
symbol.style.left = `${Math.random() * 100}%`;
symbol.style.animationDelay = `${Math.random() * 2}s`;
container.appendChild(symbol);
}
break;
case 'research':
// Create scrolling paper effect
for (let i = 0; i < 3; i++) {
const paper = document.createElement('div');
paper.className = 'absolute w-16 h-20 bg-white/20 rounded animate-float';
paper.style.left = `${20 + i * 30}%`;
paper.style.animationDelay = `${i * 0.5}s`;
container.appendChild(paper);
}
break;
return <ExpertAnimation className={className} />;
case 'podcast':
// Create audio wave effect
for (let i = 0; i < 10; i++) {
const wave = document.createElement('div');
wave.className = 'absolute bottom-1/2 w-1 bg-green-500 animate-wave';
wave.style.left = `${10 + i * 10}%`;
wave.style.animationDelay = `${i * 0.1}s`;
wave.style.height = '20%';
container.appendChild(wave);
}
break;
case 'event':
// Create gathering effect with people icons
for (let i = 0; i < particleCount; i++) {
const person = document.createElement('div');
person.className = 'absolute text-sm animate-gather';
person.textContent = '👤';
person.style.left = `${Math.random() * 100}%`;
person.style.top = `${Math.random() * 100}%`;
person.style.animationDelay = `${Math.random() * 2}s`;
container.appendChild(person);
}
break;
case 'platform':
// Create platform interface elements
const elements = ['📱', '💻', '🖥️', '📲'];
for (let i = 0; i < elements.length; i++) {
const element = document.createElement('div');
element.className = 'absolute text-2xl animate-float';
element.textContent = elements[i];
element.style.left = `${25 * i}%`;
element.style.animationDelay = `${i * 0.5}s`;
container.appendChild(element);
}
break;
case 'freedom':
// Create rising particles effect
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'absolute w-1 h-1 rounded-full bg-yellow-500 animate-rise';
particle.style.left = `${Math.random() * 100}%`;
particle.style.animationDelay = `${Math.random() * 2}s`;
container.appendChild(particle);
}
break;
}
}, [animation]);
if (animation.type === 'network') {
const nodes: Node[] = Array.from({ length: 6 }, (_, i) => {
const row = Math.floor(i / 2);
const col = i % 2;
const baseX = 30 + col * 40;
const baseY = 25 + row * 25;
return {
id: i,
baseX,
baseY,
x: baseX,
y: baseY,
};
});
return <PodcastAnimation className={className} />;
default:
return (
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
<svg className="absolute inset-0 w-full h-full pointer-events-none">
{nodes.map((node1) =>
nodes
.filter((node2) => node2.id !== node1.id)
.map((node2) => (
<motion.line
key={`${node1.id}-${node2.id}`}
x1={`${node1.baseX}%`}
y1={`${node1.baseY}%`}
x2={`${node2.baseX}%`}
y2={`${node2.baseY}%`}
stroke="rgba(255, 215, 0, 0.15)"
strokeWidth="1"
initial={{ opacity: 0 }}
animate={{
opacity: [0, 0.5, 0],
x1: [`${node1.baseX}%`, `${node1.baseX + (Math.random() > 0.5 ? 10 : -10)}%`],
y1: [`${node1.baseY}%`, `${node1.baseY + (Math.random() > 0.5 ? 10 : -10)}%`],
x2: [`${node2.baseX}%`, `${node2.baseX + (Math.random() > 0.5 ? 10 : -10)}%`],
y2: [`${node2.baseY}%`, `${node2.baseY + (Math.random() > 0.5 ? 10 : -10)}%`],
}}
transition={{
duration: 4,
repeat: Infinity,
repeatType: "mirror",
ease: "easeInOut"
}}
/>
))
)}
</svg>
{nodes.map((node) => (
<motion.div
key={node.id}
className="absolute w-3 h-3"
style={{
left: '-6px',
top: '-6px',
}}
animate={{
x: [
`${node.baseX}%`,
`${node.baseX + (Math.random() > 0.5 ? 10 : -10)}%`,
`${node.baseX}%`,
],
y: [
`${node.baseY}%`,
`${node.baseY + (Math.random() > 0.5 ? 10 : -10)}%`,
`${node.baseY}%`,
],
}}
transition={{
duration: 6,
repeat: Infinity,
repeatType: "reverse",
ease: "easeInOut",
delay: node.id * 0.2,
}}
>
<motion.div
className="absolute w-full h-full rounded-full bg-yellow-500"
animate={{
scale: [1, 1.2, 1],
opacity: [0.7, 1, 0.7],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<motion.div
className="absolute w-full h-full rounded-full bg-yellow-500/30"
animate={{
scale: [1, 3, 1],
opacity: [0.3, 0, 0.3],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
delay: node.id * 0.1,
}}
/>
</motion.div>
))}
<div className="absolute inset-0 flex items-center justify-center text-yellow-500">
Strategy Visualization
</div>
</div>
);
}
if (animation.type === 'meme') {
const createWave = (waveIndex: number) => {
return Array.from({ length: 6 }, (_, i) => {
const symbols = ['😂', '🤔', '💭', '🎯', '🔥', '💯', '👀', '🙌', '✨', '💪'];
const x = 5 + (i * (90 / 5));
return {
id: waveIndex * 6 + i,
symbol: symbols[Math.floor(Math.random() * symbols.length)],
x,
y: 120 + (waveIndex * 60),
rotation: -10 + Math.random() * 20, // Increased rotation range
scale: 0.8 + Math.random() * 0.4, // More variation in scale
};
});
};
const memeSymbols: MemeSymbol[] = [
...createWave(0),
...createWave(1),
...createWave(2),
...createWave(3),
];
return (
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
{memeSymbols.map((meme) => (
<motion.div
key={meme.id}
className="absolute text-2xl select-none"
style={{
transform: 'translate(-50%, -50%)',
willChange: 'transform',
}}
initial={{
x: `${meme.x}%`,
y: `${meme.y}%`,
rotate: meme.rotation,
scale: meme.scale,
opacity: 0,
}}
animate={{
y: [`${meme.y}%`, '-20%'],
rotate: meme.rotation,
scale: meme.scale,
opacity: [0, 1, 1, 0],
}}
transition={{
duration: 6, // Faster movement
repeat: Infinity,
ease: "easeInOut",
opacity: {
times: [0, 0.1, 0.9, 1],
duration: 6,
}
}}
>
{meme.symbol}
</motion.div>
))}
</div>
);
}
return (
<div
ref={containerRef}
className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}
/>
);
};

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

@ -0,0 +1,55 @@
import React from 'react';
import { motion } from 'framer-motion';
export const CommunityAnimation = ({ className = '' }: { className?: string }) => {
const groups = Array.from({ length: 3 }, (_, i) => ({
x: 25 + i * 25,
y: 50,
members: Array.from({ length: 8 }, (_, j) => ({
id: i * 8 + j,
initialX: Math.random() * 100,
initialY: Math.random() * 100,
}))
}));
return (
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
{groups.map((group, groupIndex) => (
<React.Fragment key={groupIndex}>
{group.members.map((member) => (
<motion.div
key={member.id}
className="absolute w-3 h-3 bg-yellow-500 rounded-full"
initial={{
x: `${member.initialX}%`,
y: `${member.initialY}%`,
opacity: 0,
}}
animate={{
x: `${group.x}%`,
y: `${group.y}%`,
opacity: 1,
}}
transition={{
duration: 2,
delay: member.id * 0.2,
ease: "easeOut",
}}
/>
))}
<motion.div
className="absolute w-20 h-20 rounded-full border-2 border-yellow-500/30"
style={{
x: `${group.x}%`,
y: `${group.y}%`,
transform: 'translate(-50%, -50%)',
}}
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 1.5, duration: 1 }}
/>
</React.Fragment>
))}
</div>
);
};

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

@ -0,0 +1,49 @@
import React from 'react';
import { motion } from 'framer-motion';
export const ExpertAnimation = ({ className = '' }: { className?: string }) => {
const symbols = ['∑', '∫', 'π', '∞', '≠', '±', '∂', '∇', '∆'];
return (
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
{symbols.map((symbol, index) => (
<motion.div
key={index}
className="absolute text-2xl font-bold text-yellow-500"
initial={{
x: `${Math.random() * 100}%`,
y: "120%",
opacity: 0,
}}
animate={{
y: ["-20%", "120%"],
opacity: [0, 1, 1, 0],
}}
transition={{
duration: 4,
repeat: Infinity,
delay: index * 0.5,
ease: "easeInOut",
opacity: {
times: [0, 0.1, 0.9, 1],
}
}}
>
{symbol}
</motion.div>
))}
<motion.div
className="absolute inset-0 flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 0] }}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
>
<div className="text-4xl text-yellow-500 font-bold">PhD</div>
</motion.div>
</div>
);
};

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

@ -0,0 +1,74 @@
import React from 'react';
import { motion } from 'framer-motion';
interface MemeSymbol {
id: number;
symbol: string;
x: number;
y: number;
rotation: number;
scale: number;
}
export const MemeAnimation = ({ className = '' }: { className?: string }) => {
const createWave = (waveIndex: number) => {
return Array.from({ length: 6 }, (_, i) => {
const symbols = ['😂', '🤔', '💭', '🎯', '🔥', '💯', '👀', '🙌', '✨', '💪'];
const x = 5 + (i * (90 / 5));
return {
id: waveIndex * 6 + i,
symbol: symbols[Math.floor(Math.random() * symbols.length)],
x,
y: 120 + (waveIndex * 60),
rotation: -10 + Math.random() * 20,
scale: 0.8 + Math.random() * 0.4,
};
});
};
const memeSymbols: MemeSymbol[] = [
...createWave(0),
...createWave(1),
...createWave(2),
...createWave(3),
];
return (
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
{memeSymbols.map((meme) => (
<motion.div
key={meme.id}
className="absolute text-2xl select-none"
style={{
transform: 'translate(-50%, -50%)',
willChange: 'transform',
}}
initial={{
x: `${meme.x}%`,
y: `${meme.y}%`,
rotate: meme.rotation,
scale: meme.scale,
opacity: 0,
}}
animate={{
y: [`${meme.y}%`, '-20%'],
rotate: meme.rotation,
scale: meme.scale,
opacity: [0, 1, 1, 0],
}}
transition={{
duration: 6,
repeat: Infinity,
ease: "easeInOut",
opacity: {
times: [0, 0.1, 0.9, 1],
duration: 6,
}
}}
>
{meme.symbol}
</motion.div>
))}
</div>
);
};

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

@ -0,0 +1,113 @@
import React from 'react';
import { motion } from 'framer-motion';
interface Node {
id: number;
baseX: number;
baseY: number;
}
export const NetworkAnimation = ({ className = '' }: { className?: string }) => {
const nodes: Node[] = Array.from({ length: 6 }, (_, i) => {
const row = Math.floor(i / 2);
const col = i % 2;
return {
id: i,
baseX: 30 + col * 40,
baseY: 25 + row * 25,
};
});
return (
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
<svg className="absolute inset-0 w-full h-full pointer-events-none">
{nodes.map((node1) =>
nodes
.filter((node2) => node2.id !== node1.id)
.map((node2) => (
<motion.line
key={`${node1.id}-${node2.id}`}
x1={`${node1.baseX}%`}
y1={`${node1.baseY}%`}
x2={`${node2.baseX}%`}
y2={`${node2.baseY}%`}
stroke="rgba(255, 215, 0, 0.15)"
strokeWidth="1"
initial={{ opacity: 0 }}
animate={{
opacity: [0, 0.5, 0],
x1: [`${node1.baseX}%`, `${node1.baseX + (Math.random() > 0.5 ? 10 : -10)}%`],
y1: [`${node1.baseY}%`, `${node1.baseY + (Math.random() > 0.5 ? 10 : -10)}%`],
x2: [`${node2.baseX}%`, `${node2.baseX + (Math.random() > 0.5 ? 10 : -10)}%`],
y2: [`${node2.baseY}%`, `${node2.baseY + (Math.random() > 0.5 ? 10 : -10)}%`],
}}
transition={{
duration: 4,
repeat: Infinity,
repeatType: "mirror",
ease: "easeInOut"
}}
/>
))
)}
</svg>
{nodes.map((node) => (
<motion.div
key={node.id}
className="absolute w-3 h-3"
style={{
left: '-6px',
top: '-6px',
}}
animate={{
x: [
`${node.baseX}%`,
`${node.baseX + (Math.random() > 0.5 ? 10 : -10)}%`,
`${node.baseX}%`,
],
y: [
`${node.baseY}%`,
`${node.baseY + (Math.random() > 0.5 ? 10 : -10)}%`,
`${node.baseY}%`,
],
}}
transition={{
duration: 6,
repeat: Infinity,
repeatType: "reverse",
ease: "easeInOut",
delay: node.id * 0.2,
}}
>
<motion.div
className="absolute w-full h-full rounded-full bg-yellow-500"
animate={{
scale: [1, 1.2, 1],
opacity: [0.7, 1, 0.7],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<motion.div
className="absolute w-full h-full rounded-full bg-yellow-500/30"
animate={{
scale: [1, 3, 1],
opacity: [0.3, 0, 0.3],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
delay: node.id * 0.1,
}}
/>
</motion.div>
))}
</div>
);
};

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

@ -0,0 +1,36 @@
import React from 'react';
import { motion } from 'framer-motion';
export const NewsAnimation = ({ className = '' }: { className?: string }) => {
const headlines = [
"BREAKING NEWS • MATHEMATICAL TRUTH QUESTIONED •",
"EXPERTS DIVIDED ON BASIC ARITHMETIC •",
"NEW STUDY CHALLENGES CONVENTIONAL MATH •",
"MATHEMATICAL REVOLUTION BREWING •",
"EDUCATION SYSTEM IN CRISIS •"
];
return (
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
{headlines.map((headline, index) => (
<motion.div
key={index}
className="absolute whitespace-nowrap text-yellow-500 font-bold"
style={{
top: `${index * 20}%`,
}}
initial={{ x: "100%" }}
animate={{ x: "-100%" }}
transition={{
duration: 20,
repeat: Infinity,
ease: "linear",
delay: index * 2,
}}
>
{headline}
</motion.div>
))}
</div>
);
};

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

@ -0,0 +1,44 @@
import React from 'react';
import { motion } from 'framer-motion';
export const PodcastAnimation = ({ className = '' }: { className?: string }) => {
const waves = Array.from({ length: 10 }, (_, i) => ({
id: i,
height: 20 + Math.random() * 60,
}));
return (
<div className={`relative w-full h-40 overflow-hidden bg-black/20 rounded-lg ${className}`}>
<div className="absolute bottom-0 left-0 right-0 flex justify-around items-end h-full">
{waves.map((wave) => (
<motion.div
key={wave.id}
className="w-2 bg-yellow-500 rounded-t-full"
initial={{ height: 5 }}
animate={{
height: [5, wave.height, 5],
}}
transition={{
duration: 1,
repeat: Infinity,
delay: wave.id * 0.1,
ease: "easeInOut",
}}
/>
))}
</div>
<motion.div
className="absolute top-1/4 left-1/2 transform -translate-x-1/2 text-4xl"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 0] }}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
>
🎙
</motion.div>
</div>
);
};