diff --git a/README.md b/README.md index a046b24..b894166 100644 --- a/README.md +++ b/README.md @@ -1,186 +1,261 @@ -# Art Génératif avec Code +# 🎨 Distorsion Movement - Interactive Generative Art Engine -Ce projet explore l'art génératif en créant des visualisations colorées programmatiques. +**Distorsion Movement** is a real-time generative art platform that creates mesmerizing visual experiences through geometrically deformed grids. The system generates dynamic, interactive artwork by applying mathematical distortions to regular grids of colored squares, with optional audio-reactive capabilities for music visualization. -## Installation +## 🎯 Project Overview -1. Clonez le repository -2. Créez un environnement virtuel : - ```bash - python -m venv venv - source venv/bin/activate # Sur macOS/Linux - # ou - venv\Scripts\activate # Sur Windows - ``` -3. Installez les dépendances : - ```bash - pip install -r requirements.txt - ``` +### What It Does +The project creates animated grids of squares where each square can be: +- **Geometrically distorted** using mathematical functions (sine waves, Perlin noise, circular patterns) +- **Dynamically colored** using various schemes (rainbow, gradient, neon, temperature-based, etc.) +- **Audio-reactive** to music and sound input in real-time +- **Interactively controlled** through keyboard shortcuts and parameters -## Utilisation +### How It Works Technically -### Grille de carrés colorés +The core architecture follows a modular design with clear separation of concerns: -Le fichier `generative_art.py` contient une fonction pour générer une grille de carrés colorés aléatoirement : +1. **Grid Generation**: Creates a regular NxN grid of squares with base positions +2. **Distortion Engine**: Applies mathematical transformations to deform square positions and orientations +3. **Color System**: Generates dynamic colors based on position, time, and audio input +4. **Audio Analysis**: Real-time FFT analysis of microphone input to extract bass, mids, highs, and beat detection +5. **Rendering**: Real-time pygame-based visualization with 60fps target -```python -from generative_art import generate_color_grid_vectorized, display_grid - -# Générer une grille 64x64 -grid = generate_color_grid_vectorized(64) - -# Afficher la grille -display_grid(grid, "Ma grille colorée") -``` +### Mathematical Foundation -### Fonctions disponibles +The distortions are based on several mathematical approaches: +- **Sine Wave Distortions**: `sin(x * frequency + phase) * amplitude` +- **Perlin Noise**: Smooth, organic-looking deformations +- **Circular Distortions**: Radial distortions from center points +- **Random Static**: Controlled randomness for chaotic effects -- `generate_color_grid(dimension)` : Version détaillée avec boucles -- `generate_color_grid_vectorized(dimension)` : Version optimisée avec numpy -- `display_grid(grid, title, figsize)` : Affiche la grille dans une fenêtre -- `save_grid(grid, filename, dpi)` : Sauvegarde la grille en image +Audio reactivity maps frequency bands to visual parameters: +- 🥁 **Bass (20-250Hz)** → Distortion intensity +- 🎸 **Mids (250Hz-4kHz)** → Color hue rotation +- ✨ **Highs (4kHz+)** → Brightness boosts +- 💥 **Beat detection** → Flash effects +- 📢 **Overall volume** → Animation speed -### Exemple rapide +## 🏗️ Project Structure -```bash -python generative_art.py ``` - -Cette commande génère et affiche une grille 64x64 de carrés colorés aléatoirement. - -## Personnalisation - -Le code est conçu pour être facilement modifiable : - -- Changez la `dimension` pour des grilles plus grandes ou plus petites -- Modifiez la génération de couleurs dans `generate_color_grid()` -- Ajustez les paramètres d'affichage dans `display_grid()` -- Expérimentez avec différents algorithmes de couleur - -## Grille de carrés déformés géométriquement - -Le fichier `deformed_grid.py` implémente une nouvelle fonctionnalité d'art génératif : des grilles de carrés déformés géométriquement. - -### Utilisation rapide - -```python -from deformed_grid import create_deformed_grid - -# Créer une grille déformée -grid = create_deformed_grid( - dimension=48, # Grille 48x48 - cell_size=12, # Carrés de 12 pixels - distortion_strength=0.4, # Intensité de déformation - distortion_fn="sine" # Type de distorsion -) - -# Lancer l'interface interactive -grid.run_interactive() +distorsion_movement/ +├── __init__.py # Package entry point & public API +├── deformed_grid.py # Main DeformedGrid class (366 lines) +├── enums.py # Type definitions (DistortionType, ColorScheme) +├── audio_analyzer.py # Real-time audio analysis & FFT processing +├── colors.py # Color generation algorithms +├── distortions.py # Geometric distortion algorithms +├── demos.py # Demo functions & usage examples +├── test_modules.py # Unit tests +├── improvements.md # Future development roadmap +├── README_modules.md # Detailed module documentation +└── README.md # This file ``` -### Types de distorsion disponibles - -- **`"random"`** : Déformation aléatoire statique -- **`"sine"`** : Distorsion sinusoïdale animée (effet de vague) -- **`"perlin"`** : Bruit de Perlin pour un effet organique -- **`"circular"`** : Ondes circulaires depuis le centre - -### Contrôles interactifs - -- **ESC** : Quitter -- **SPACE** : Changer le type de distorsion -- **+/-** : Ajuster l'intensité de distorsion -- **R** : Régénérer les paramètres aléatoires -- **S** : Sauvegarder l'image courante - -### Démonstrations - -Lancez le script de démonstration pour explorer différents exemples : +### Module Responsibilities + +#### 🎨 **`deformed_grid.py`** - Core Engine (366 lines) +- Main `DeformedGrid` class that orchestrates everything +- Pygame rendering loop and event handling +- Grid generation and position calculations +- Animation timing and state management +- Fullscreen/windowed mode switching +- Interactive controls (keyboard shortcuts) + +#### 🎵 **`audio_analyzer.py`** - Audio Processing +- Real-time microphone input capture using PyAudio +- FFT analysis for frequency separation +- Beat detection algorithms +- Thread-safe audio data sharing +- Graceful degradation when audio libraries unavailable + +#### 🌈 **`colors.py`** - Color Generation +- 10 different color schemes (monochrome, gradient, rainbow, neon, etc.) +- Position-based color calculations +- Time-based color animations +- Audio-reactive color modulation +- HSV/RGB color space conversions + +#### 🌀 **`distortions.py`** - Geometric Engine +- 4 distortion algorithms (random, sine, Perlin, circular) +- Mathematical transformation functions +- Parameter generation for each square +- Time-based animation calculations +- Audio-reactive distortion intensity + +#### 🎮 **`demos.py`** - Usage Examples +- Pre-configured demonstration functions +- Simple API for quick setup +- Different preset combinations +- Command-line interface + +#### 📝 **`enums.py`** - Type Safety +- `DistortionType`: RANDOM, SINE, PERLIN, CIRCULAR +- `ColorScheme`: MONOCHROME, GRADIENT, RAINBOW, COMPLEMENTARY, TEMPERATURE, PASTEL, NEON, OCEAN, FIRE, FOREST + +## 🚀 Quick Start + +### Basic Usage +```python +from distorsion_movement import quick_demo -```bash -python demo_deformed_grid.py +# Launch with default settings +quick_demo() ``` -Le script propose 8 démonstrations différentes : -1. Démonstration basique (distorsion aléatoire) -2. Animation sinusoïdale -3. Effet organique (Perlin) -4. Ondes circulaires -5. Haute densité (grille fine) -6. Couleurs personnalisées -7. Tremblement minimal (effet subtil) -8. Export en lot (génération d'images) - -### Paramètres avancés - +### Advanced Configuration ```python -from deformed_grid import DeformedGrid +from distorsion_movement import DeformedGrid, DistortionType, ColorScheme +# Create custom grid grid = DeformedGrid( - dimension=64, # Nombre de cellules par ligne/colonne - cell_size=8, # Taille moyenne des carrés - canvas_size=(800, 600), # Taille de la fenêtre - distortion_strength=0.3, # Intensité (0.0 à 1.0) - distortion_fn="random", # Type de distorsion - background_color=(20, 20, 30), # Couleur de fond RGB - square_color=(255, 255, 255) # Couleur des carrés RGB + dimension=64, # 64x64 grid + cell_size=12, # 12px squares + distortion_strength=0.7, # 70% distortion + distortion_fn=DistortionType.SINE.value, # Sine wave distortion + color_scheme=ColorScheme.NEON.value, # Neon colors + audio_reactive=True, # Enable audio reactivity + color_animation=True # Animate colors ) -``` - -## Idées d'extensions -- Patterns géométriques -- Gradients de couleur -- Formes autres que des carrés -- Animation temporelle -- Interaction utilisateur - - -## Other ideas: - -🎲 1. Grille avec règles de propagation (style “contagion”) - • Concept : Un carré coloré “contamine” ses voisins avec une certaine couleur ou un effet au fil du temps. - • Résultat : Une sorte d’onde ou de tache de couleur qui se propage dans la grille. - • Originalité : Tu définis tes propres règles de propagation (aléatoire, influence de la couleur voisine, etc.) - -⸻ +grid.run_interactive() +``` -🧠 2. Influence d’un bruit de Perlin ou Simplex - • Concept : Tu utilises du bruit (comme une texture mathématique douce) pour moduler la couleur, la taille, la rotation des carrés. - • Résultat : Des effets très organiques, qui rappellent des structures naturelles. - • Originalité : Tu mélanges hasard contrôlé + structure. +### Available Demo Functions +```python +from distorsion_movement import quick_demo, fullscreen_demo, audio_reactive_demo -⸻ +quick_demo() # Basic windowed demo +fullscreen_demo() # Immersive fullscreen experience +audio_reactive_demo() # Music visualization demo +``` -🎨 3. Palette limitée avec contrainte esthétique - • Concept : Tu choisis une palette (genre 4 couleurs de Kandinsky, ou le style Bauhaus) et tu forces les carrés à suivre un pattern (pas plus de 2 couleurs côte à côte, pas 3 fois la même de suite, etc.) - • Résultat : Ça donne des rythmes visuels intéressants. - • Originalité : Le code impose des contraintes artistiques. +## 🎛️ Interactive Controls + +| Key | Action | +|-----|--------| +| `F` | Toggle fullscreen/windowed mode | +| `M` | Toggle audio reactivity on/off | +| `C` | Cycle through color schemes | +| `D` | Cycle through distortion types | +| `+/-` | Increase/decrease distortion intensity | +| `A` | Toggle color animation | +| `R` | Reset to default parameters | +| `ESC` | Exit application | + +## 🎨 Available Visual Modes + +### Distortion Types +- **Random**: Static chaotic displacement +- **Sine**: Smooth wave-based deformations +- **Perlin**: Organic, noise-based distortions +- **Circular**: Radial distortions from center + +### Color Schemes +- **Monochrome**: Single color variations +- **Gradient**: Smooth color transitions +- **Rainbow**: Full spectrum cycling +- **Complementary**: Alternating opposite colors +- **Temperature**: Cool to warm transitions +- **Pastel**: Soft, muted tones +- **Neon**: Bright, electric colors +- **Ocean**: Blue-green aquatic themes +- **Fire**: Red-orange-yellow flames +- **Forest**: Green-brown natural tones + +## 🔧 Dependencies + +### Required +``` +pygame>=2.1.0 +numpy>=1.21.0 +``` -⸻ +### Optional (for audio reactivity) +``` +pyaudio>=0.2.11 +scipy>=1.7.0 +``` -🧩 4. Grille à déformation géométrique - • Concept : Au lieu d’un carré fixe, chaque cellule est légèrement déformée (distorsion de position, taille, perspective). - • Résultat : Un effet d’illusion ou d’espace qui tremble. - • Originalité : L’ordre apparent de la grille est bousculé. +Install with: +```bash +pip install pygame numpy +# For audio features: +pip install pyaudio scipy +``` -⸻ +## 🎵 Audio-Reactive Features -🌱 5. Évolution générationnelle - • Concept : Tu fais tourner la grille dans le temps : à chaque tick, la grille change (un peu comme une vie cellulaire type “Game of Life”). - • Résultat : Une œuvre animée, auto-évolutive. - • Originalité : Tu n’affiches pas qu’un état, mais un processus. +When audio reactivity is enabled, the system: +- Captures real-time microphone input +- Performs FFT analysis to separate frequency bands +- Maps audio characteristics to visual parameters +- Detects beats for synchronized flash effects +- Smooths audio data to prevent jarring transitions -⸻ +**Note**: Audio features require additional dependencies and microphone permissions. -🧵 6. Tissage de motifs / glitch - • Concept : Chaque carré devient une “maille” dans un tissage visuel. Tu peux “glitcher” aléatoirement des sections (inversion de couleurs, rotations, miroir). - • Résultat : Un mix entre géométrie stricte et chaos visuel. - • Originalité : Belle tension entre contrôle et rupture. +## 🧪 Testing -⸻ +Run the test suite: +```bash +cd distorsion_movement +python test_modules.py +``` -👁️ 7. Œil qui regarde - • Concept : Un carré sur la grille suit la souris (ou une zone chaude), les couleurs autour réagissent à sa position. - • Résultat : Une grille “vivante”, qui semble te regarder ou réagir à toi. - • Originalité : Une œuvre interactive minimaliste. \ No newline at end of file +## 🛣️ Development Roadmap + +The project has an extensive roadmap for future enhancements (see `improvements.md`): + +### Phase 1 (High Priority) +- Mouse interaction (attraction/repulsion effects) +- Additional shape types (circles, triangles, polygons) +- Motion blur and glow effects +- GIF/MP4 export capabilities +- Preset scene system + +### Phase 2 (Advanced Features) +- Particle systems and trailing effects +- GPU acceleration for performance +- Web version using WebGL +- VR/AR support for immersive experiences +- Neural network integration for AI-generated patterns + +### Phase 3 (Experimental) +- Real-time data visualization integration +- Collaborative multi-user experiences +- Biometric integration (heart rate, brainwaves) +- Advanced physics simulation + +## 🏆 Key Features + +✅ **Real-time Performance**: 60fps rendering with thousands of squares +✅ **Modular Architecture**: Clean separation of concerns, easily extensible +✅ **Audio Reactivity**: Professional-grade music visualization +✅ **Interactive Controls**: Live parameter adjustment +✅ **Multiple Visual Modes**: 4 distortion types × 10 color schemes = 40 combinations +✅ **Cross-platform**: Works on Windows, macOS, Linux +✅ **Graceful Degradation**: Works without audio libraries +✅ **Fullscreen Support**: Immersive viewing experience + +## 🎨 Use Cases + +- **Live Music Visualization**: DJ performances, concerts, parties +- **Digital Art Creation**: Generative art projects, installations +- **Meditation/Relaxation**: Calming visual experiences +- **Educational**: Mathematics and programming demonstrations +- **Screensaver**: Beautiful ambient desktop backgrounds +- **Content Creation**: Background visuals for videos, streams + +## 🤝 Contributing + +The project is designed for easy extension: +- Add new distortion algorithms in `distortions.py` +- Create new color schemes in `colors.py` +- Enhance audio analysis in `audio_analyzer.py` +- Build new demo configurations in `demos.py` + +--- + +**Distorsion Movement** transforms mathematical concepts into living, breathing art that responds to sound and user interaction. It's both a technical showcase of real-time graphics programming and a creative tool for generating endless visual experiences. diff --git a/distorsion_movement/TODO.md b/TODO.md similarity index 99% rename from distorsion_movement/TODO.md rename to TODO.md index 099ea74..bc1b505 100644 --- a/distorsion_movement/TODO.md +++ b/TODO.md @@ -186,6 +186,7 @@ - [ ] Mouse interaction (attraction/repulsion) - [ ] GIF/MP4 export - [ ] Additional shape types (circles, triangles) +- [ ] Add new distortion types - [ ] Motion blur effects - [ ] Preset scene system - [ ] Audio reactive features diff --git a/generative_squares/art_generatif_64x64.png b/archive/art_generatif_64x64.png similarity index 100% rename from generative_squares/art_generatif_64x64.png rename to archive/art_generatif_64x64.png diff --git a/generative_squares/comparaison_blues.png b/archive/comparaison_blues.png similarity index 100% rename from generative_squares/comparaison_blues.png rename to archive/comparaison_blues.png diff --git a/generative_squares/comparaison_color_schemes.png b/archive/comparaison_color_schemes.png similarity index 100% rename from generative_squares/comparaison_color_schemes.png rename to archive/comparaison_color_schemes.png diff --git a/generative_squares/comparaison_reds.png b/archive/comparaison_reds.png similarity index 100% rename from generative_squares/comparaison_reds.png rename to archive/comparaison_reds.png diff --git a/generative_squares/comparaison_warm.png b/archive/comparaison_warm.png similarity index 100% rename from generative_squares/comparaison_warm.png rename to archive/comparaison_warm.png diff --git a/generative_squares/demo_deformed_grid.py b/archive/demo_deformed_grid.py similarity index 100% rename from generative_squares/demo_deformed_grid.py rename to archive/demo_deformed_grid.py diff --git a/generative_squares/generative_art.py b/archive/generative_art.py similarity index 100% rename from generative_squares/generative_art.py rename to archive/generative_art.py diff --git a/generative_squares/generative_art_comparison.png b/archive/generative_art_comparison.png similarity index 100% rename from generative_squares/generative_art_comparison.png rename to archive/generative_art_comparison.png diff --git a/generative_squares/melange_arc_en_ciel.png b/archive/melange_arc_en_ciel.png similarity index 100% rename from generative_squares/melange_arc_en_ciel.png rename to archive/melange_arc_en_ciel.png diff --git a/generative_squares/melange_green_red.png b/archive/melange_green_red.png similarity index 100% rename from generative_squares/melange_green_red.png rename to archive/melange_green_red.png diff --git a/generative_squares/melange_pastels_sombres.png b/archive/melange_pastels_sombres.png similarity index 100% rename from generative_squares/melange_pastels_sombres.png rename to archive/melange_pastels_sombres.png diff --git a/generative_squares/melange_primaires.png b/archive/melange_primaires.png similarity index 100% rename from generative_squares/melange_primaires.png rename to archive/melange_primaires.png diff --git a/generative_squares/melange_red_yellow_green.png b/archive/melange_red_yellow_green.png similarity index 100% rename from generative_squares/melange_red_yellow_green.png rename to archive/melange_red_yellow_green.png diff --git a/generative_squares/melange_warm_cool.png b/archive/melange_warm_cool.png similarity index 100% rename from generative_squares/melange_warm_cool.png rename to archive/melange_warm_cool.png diff --git a/generative_squares/monochrome_256x256.png b/archive/monochrome_256x256.png similarity index 100% rename from generative_squares/monochrome_256x256.png rename to archive/monochrome_256x256.png diff --git a/generative_squares/palette_constraints.py b/archive/palette_constraints.py similarity index 100% rename from generative_squares/palette_constraints.py rename to archive/palette_constraints.py diff --git a/generative_squares/palette_constraints_kandinsky_64x64.png b/archive/palette_constraints_kandinsky_64x64.png similarity index 100% rename from generative_squares/palette_constraints_kandinsky_64x64.png rename to archive/palette_constraints_kandinsky_64x64.png diff --git a/generative_squares/palette_constraints_matisse_128x128.png b/archive/palette_constraints_matisse_128x128.png similarity index 100% rename from generative_squares/palette_constraints_matisse_128x128.png rename to archive/palette_constraints_matisse_128x128.png diff --git a/generative_squares/palette_constraints_mondrian_128x128.png b/archive/palette_constraints_mondrian_128x128.png similarity index 100% rename from generative_squares/palette_constraints_mondrian_128x128.png rename to archive/palette_constraints_mondrian_128x128.png diff --git a/distorsion_movement/README.md b/distorsion_movement/README.md deleted file mode 100644 index b894166..0000000 --- a/distorsion_movement/README.md +++ /dev/null @@ -1,261 +0,0 @@ -# 🎨 Distorsion Movement - Interactive Generative Art Engine - -**Distorsion Movement** is a real-time generative art platform that creates mesmerizing visual experiences through geometrically deformed grids. The system generates dynamic, interactive artwork by applying mathematical distortions to regular grids of colored squares, with optional audio-reactive capabilities for music visualization. - -## 🎯 Project Overview - -### What It Does -The project creates animated grids of squares where each square can be: -- **Geometrically distorted** using mathematical functions (sine waves, Perlin noise, circular patterns) -- **Dynamically colored** using various schemes (rainbow, gradient, neon, temperature-based, etc.) -- **Audio-reactive** to music and sound input in real-time -- **Interactively controlled** through keyboard shortcuts and parameters - -### How It Works Technically - -The core architecture follows a modular design with clear separation of concerns: - -1. **Grid Generation**: Creates a regular NxN grid of squares with base positions -2. **Distortion Engine**: Applies mathematical transformations to deform square positions and orientations -3. **Color System**: Generates dynamic colors based on position, time, and audio input -4. **Audio Analysis**: Real-time FFT analysis of microphone input to extract bass, mids, highs, and beat detection -5. **Rendering**: Real-time pygame-based visualization with 60fps target - -### Mathematical Foundation - -The distortions are based on several mathematical approaches: -- **Sine Wave Distortions**: `sin(x * frequency + phase) * amplitude` -- **Perlin Noise**: Smooth, organic-looking deformations -- **Circular Distortions**: Radial distortions from center points -- **Random Static**: Controlled randomness for chaotic effects - -Audio reactivity maps frequency bands to visual parameters: -- 🥁 **Bass (20-250Hz)** → Distortion intensity -- 🎸 **Mids (250Hz-4kHz)** → Color hue rotation -- ✨ **Highs (4kHz+)** → Brightness boosts -- 💥 **Beat detection** → Flash effects -- 📢 **Overall volume** → Animation speed - -## 🏗️ Project Structure - -``` -distorsion_movement/ -├── __init__.py # Package entry point & public API -├── deformed_grid.py # Main DeformedGrid class (366 lines) -├── enums.py # Type definitions (DistortionType, ColorScheme) -├── audio_analyzer.py # Real-time audio analysis & FFT processing -├── colors.py # Color generation algorithms -├── distortions.py # Geometric distortion algorithms -├── demos.py # Demo functions & usage examples -├── test_modules.py # Unit tests -├── improvements.md # Future development roadmap -├── README_modules.md # Detailed module documentation -└── README.md # This file -``` - -### Module Responsibilities - -#### 🎨 **`deformed_grid.py`** - Core Engine (366 lines) -- Main `DeformedGrid` class that orchestrates everything -- Pygame rendering loop and event handling -- Grid generation and position calculations -- Animation timing and state management -- Fullscreen/windowed mode switching -- Interactive controls (keyboard shortcuts) - -#### 🎵 **`audio_analyzer.py`** - Audio Processing -- Real-time microphone input capture using PyAudio -- FFT analysis for frequency separation -- Beat detection algorithms -- Thread-safe audio data sharing -- Graceful degradation when audio libraries unavailable - -#### 🌈 **`colors.py`** - Color Generation -- 10 different color schemes (monochrome, gradient, rainbow, neon, etc.) -- Position-based color calculations -- Time-based color animations -- Audio-reactive color modulation -- HSV/RGB color space conversions - -#### 🌀 **`distortions.py`** - Geometric Engine -- 4 distortion algorithms (random, sine, Perlin, circular) -- Mathematical transformation functions -- Parameter generation for each square -- Time-based animation calculations -- Audio-reactive distortion intensity - -#### 🎮 **`demos.py`** - Usage Examples -- Pre-configured demonstration functions -- Simple API for quick setup -- Different preset combinations -- Command-line interface - -#### 📝 **`enums.py`** - Type Safety -- `DistortionType`: RANDOM, SINE, PERLIN, CIRCULAR -- `ColorScheme`: MONOCHROME, GRADIENT, RAINBOW, COMPLEMENTARY, TEMPERATURE, PASTEL, NEON, OCEAN, FIRE, FOREST - -## 🚀 Quick Start - -### Basic Usage -```python -from distorsion_movement import quick_demo - -# Launch with default settings -quick_demo() -``` - -### Advanced Configuration -```python -from distorsion_movement import DeformedGrid, DistortionType, ColorScheme - -# Create custom grid -grid = DeformedGrid( - dimension=64, # 64x64 grid - cell_size=12, # 12px squares - distortion_strength=0.7, # 70% distortion - distortion_fn=DistortionType.SINE.value, # Sine wave distortion - color_scheme=ColorScheme.NEON.value, # Neon colors - audio_reactive=True, # Enable audio reactivity - color_animation=True # Animate colors -) - -grid.run_interactive() -``` - -### Available Demo Functions -```python -from distorsion_movement import quick_demo, fullscreen_demo, audio_reactive_demo - -quick_demo() # Basic windowed demo -fullscreen_demo() # Immersive fullscreen experience -audio_reactive_demo() # Music visualization demo -``` - -## 🎛️ Interactive Controls - -| Key | Action | -|-----|--------| -| `F` | Toggle fullscreen/windowed mode | -| `M` | Toggle audio reactivity on/off | -| `C` | Cycle through color schemes | -| `D` | Cycle through distortion types | -| `+/-` | Increase/decrease distortion intensity | -| `A` | Toggle color animation | -| `R` | Reset to default parameters | -| `ESC` | Exit application | - -## 🎨 Available Visual Modes - -### Distortion Types -- **Random**: Static chaotic displacement -- **Sine**: Smooth wave-based deformations -- **Perlin**: Organic, noise-based distortions -- **Circular**: Radial distortions from center - -### Color Schemes -- **Monochrome**: Single color variations -- **Gradient**: Smooth color transitions -- **Rainbow**: Full spectrum cycling -- **Complementary**: Alternating opposite colors -- **Temperature**: Cool to warm transitions -- **Pastel**: Soft, muted tones -- **Neon**: Bright, electric colors -- **Ocean**: Blue-green aquatic themes -- **Fire**: Red-orange-yellow flames -- **Forest**: Green-brown natural tones - -## 🔧 Dependencies - -### Required -``` -pygame>=2.1.0 -numpy>=1.21.0 -``` - -### Optional (for audio reactivity) -``` -pyaudio>=0.2.11 -scipy>=1.7.0 -``` - -Install with: -```bash -pip install pygame numpy -# For audio features: -pip install pyaudio scipy -``` - -## 🎵 Audio-Reactive Features - -When audio reactivity is enabled, the system: -- Captures real-time microphone input -- Performs FFT analysis to separate frequency bands -- Maps audio characteristics to visual parameters -- Detects beats for synchronized flash effects -- Smooths audio data to prevent jarring transitions - -**Note**: Audio features require additional dependencies and microphone permissions. - -## 🧪 Testing - -Run the test suite: -```bash -cd distorsion_movement -python test_modules.py -``` - -## 🛣️ Development Roadmap - -The project has an extensive roadmap for future enhancements (see `improvements.md`): - -### Phase 1 (High Priority) -- Mouse interaction (attraction/repulsion effects) -- Additional shape types (circles, triangles, polygons) -- Motion blur and glow effects -- GIF/MP4 export capabilities -- Preset scene system - -### Phase 2 (Advanced Features) -- Particle systems and trailing effects -- GPU acceleration for performance -- Web version using WebGL -- VR/AR support for immersive experiences -- Neural network integration for AI-generated patterns - -### Phase 3 (Experimental) -- Real-time data visualization integration -- Collaborative multi-user experiences -- Biometric integration (heart rate, brainwaves) -- Advanced physics simulation - -## 🏆 Key Features - -✅ **Real-time Performance**: 60fps rendering with thousands of squares -✅ **Modular Architecture**: Clean separation of concerns, easily extensible -✅ **Audio Reactivity**: Professional-grade music visualization -✅ **Interactive Controls**: Live parameter adjustment -✅ **Multiple Visual Modes**: 4 distortion types × 10 color schemes = 40 combinations -✅ **Cross-platform**: Works on Windows, macOS, Linux -✅ **Graceful Degradation**: Works without audio libraries -✅ **Fullscreen Support**: Immersive viewing experience - -## 🎨 Use Cases - -- **Live Music Visualization**: DJ performances, concerts, parties -- **Digital Art Creation**: Generative art projects, installations -- **Meditation/Relaxation**: Calming visual experiences -- **Educational**: Mathematics and programming demonstrations -- **Screensaver**: Beautiful ambient desktop backgrounds -- **Content Creation**: Background visuals for videos, streams - -## 🤝 Contributing - -The project is designed for easy extension: -- Add new distortion algorithms in `distortions.py` -- Create new color schemes in `colors.py` -- Enhance audio analysis in `audio_analyzer.py` -- Build new demo configurations in `demos.py` - ---- - -**Distorsion Movement** transforms mathematical concepts into living, breathing art that responds to sound and user interaction. It's both a technical showcase of real-time graphics programming and a creative tool for generating endless visual experiences. diff --git a/distorsion_movement/deformed_grid.py b/distorsion_movement/deformed_grid.py index aaa2f38..c0bf176 100644 --- a/distorsion_movement/deformed_grid.py +++ b/distorsion_movement/deformed_grid.py @@ -36,7 +36,12 @@ def __init__(self, square_color: Tuple[int, int, int] = (255, 255, 255), color_scheme: str = "monochrome", color_animation: bool = False, - audio_reactive: bool = False): + audio_reactive: bool = False, + mouse_interactive: bool = True, + mouse_mode: str = "attraction", + mouse_strength: float = 0.5, + mouse_radius: float = 100.0, + show_mouse_feedback: bool = True): """ Initialise la grille déformée. @@ -51,6 +56,11 @@ def __init__(self, color_scheme: Schéma de couleurs ("monochrome", "gradient", "rainbow", etc.) color_animation: Si True, les couleurs changent dans le temps audio_reactive: Si True, réagit à l'audio en temps réel + mouse_interactive: Si True, active les interactions souris + mouse_mode: Mode d'interaction souris ("attraction", "repulsion", etc.) + mouse_strength: Force des interactions souris (0.0 à 1.0) + mouse_radius: Rayon d'influence de la souris en pixels + show_mouse_feedback: Si True, affiche le feedback visuel de la souris """ self.dimension = dimension self.cell_size = cell_size @@ -62,11 +72,37 @@ def __init__(self, self.color_scheme = color_scheme self.color_animation = color_animation self.audio_reactive = audio_reactive + self.mouse_interactive = mouse_interactive + self.mouse_mode = mouse_mode + self.mouse_strength = mouse_strength + self.mouse_radius = mouse_radius + self.show_mouse_feedback = show_mouse_feedback # Audio analyzer self.audio_analyzer = AudioAnalyzer() if audio_reactive else None self.base_distortion_strength = distortion_strength # Sauvegarde de l'intensité de base + # Mouse interaction engine + if mouse_interactive: + from distorsion_movement.mouse_interactions import MouseInteractionEngine + from distorsion_movement.enums import MouseInteractionType, MouseMode + + # Convertir le mode string en enum + try: + interaction_type = MouseInteractionType(mouse_mode) + except ValueError: + interaction_type = MouseInteractionType.ATTRACTION + + self.mouse_engine = MouseInteractionEngine( + interaction_type=interaction_type, + mouse_mode=MouseMode.CONTINUOUS, + strength=mouse_strength, + radius=mouse_radius, + show_feedback=show_mouse_feedback + ) + else: + self.mouse_engine = None + # Calcul automatique du décalage pour centrer la grille grid_total_size = dimension * cell_size self.offset_x = (canvas_size[0] - grid_total_size) // 2 @@ -146,15 +182,46 @@ def _get_distorted_positions(self) -> List[Tuple[float, float, float]]: Returns: Liste de tuples (x, y, rotation) pour chaque carré """ - return DistortionEngine.get_distorted_positions( + # Calculer les positions de base avec la distorsion principale + base_distorted = DistortionEngine.get_distorted_positions( self.base_positions, self.distortions, self.distortion_fn, self.cell_size, self.distortion_strength, self.time, - self.canvas_size + self.canvas_size, + self.mouse_engine ) + + # Appliquer les forces de souris de manière additive si activées + if (self.mouse_interactive and self.mouse_engine and + self.distortion_fn not in ["mouse_attraction", "mouse_repulsion"]): + + final_positions = [] + for i, (x, y, rotation) in enumerate(base_distorted): + # Calculer la force de souris pour cette position + mouse_force = self.mouse_engine.calculate_mouse_force((x, y)) + + # Appliquer la force avec un facteur d'échelle + force_multiplier = self.cell_size * self.mouse_strength * 5.0 + mouse_dx = mouse_force[0] * force_multiplier + mouse_dy = mouse_force[1] * force_multiplier + + # Rotation additionnelle basée sur la force + force_magnitude = math.sqrt(mouse_force[0]**2 + mouse_force[1]**2) + mouse_rotation = force_magnitude * self.mouse_strength * 0.3 + + # Combiner les déformations + final_x = x + mouse_dx + final_y = y + mouse_dy + final_rotation = rotation + mouse_rotation + + final_positions.append((final_x, final_y, final_rotation)) + + return final_positions + + return base_distorted def _draw_deformed_square(self, surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): @@ -252,6 +319,13 @@ def run_interactive(self): print("- +/-: Ajuster l'intensité de distorsion") print("- R: Régénérer les paramètres aléatoires") print("- S: Sauvegarder l'image") + if self.mouse_interactive: + print("\nContrôles souris:") + print("- Mouvement souris: Interaction continue") + print("- Clic gauche: Effet d'ondulation") + print("- Clic droit: Effet d'explosion") + print("- TAB: Basculer interactions souris") + print("- 1-7: Changer mode d'interaction") distortion_types = [t.value for t in DistortionType] current_distortion_index = 0 @@ -265,7 +339,14 @@ def run_interactive(self): current_color_index = 0 while running: - for event in pygame.event.get(): + # Collecter les événements + events = pygame.event.get() + + # Traiter les événements souris si activées + if self.mouse_engine: + self.mouse_engine.update_mouse_state(events) + + for event in events: if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: @@ -327,6 +408,25 @@ def run_interactive(self): self.toggle_fullscreen() mode = "plein écran" if self.is_fullscreen else "fenêtré" print(f"Mode: {mode}") + elif event.key == pygame.K_TAB: + # Basculer les interactions souris + if self.mouse_engine: + from distorsion_movement.enums import MouseMode + if self.mouse_engine.mouse_mode == MouseMode.DISABLED: + self.mouse_engine.set_mouse_mode(MouseMode.CONTINUOUS) + print("🖱️ Interactions souris activées") + else: + self.mouse_engine.set_mouse_mode(MouseMode.DISABLED) + print("🚫 Interactions souris désactivées") + elif event.key >= pygame.K_1 and event.key <= pygame.K_7: + # Changer le mode d'interaction souris (1-7) + if self.mouse_engine: + from distorsion_movement.enums import MouseInteractionType + interaction_types = list(MouseInteractionType) + key_index = event.key - pygame.K_1 + if key_index < len(interaction_types): + self.mouse_engine.set_interaction_type(interaction_types[key_index]) + print(f"🖱️ Mode souris: {interaction_types[key_index].value}") self.update() self.render() diff --git a/distorsion_movement/demos.py b/distorsion_movement/demos.py index 6e7fa9c..2948fa3 100644 --- a/distorsion_movement/demos.py +++ b/distorsion_movement/demos.py @@ -17,7 +17,10 @@ def create_deformed_grid(dimension: int = 64, color_scheme: str = "rainbow", color_animation: bool = False, audio_reactive: bool = False, - fullscreen: bool = False) -> DeformedGrid: + fullscreen: bool = False, + mouse_interactive: bool = True, + mouse_strength: float = 0.7, + mouse_radius: float = 120.0) -> DeformedGrid: """ Crée une grille déformée avec des paramètres simples. @@ -30,6 +33,9 @@ def create_deformed_grid(dimension: int = 64, color_animation: Si True, les couleurs sont animées audio_reactive: Si True, réagit à l'audio en temps réel fullscreen: Si True, démarre directement en plein écran + mouse_interactive: Si True, active les interactions souris + mouse_strength: Force des interactions souris (0.0 à 1.0) + mouse_radius: Rayon d'influence de la souris en pixels Returns: Instance de DeformedGrid configurée @@ -48,7 +54,10 @@ def create_deformed_grid(dimension: int = 64, distortion_fn=distortion_fn, color_scheme=color_scheme, color_animation=color_animation, - audio_reactive=audio_reactive + audio_reactive=audio_reactive, + mouse_interactive=mouse_interactive, + mouse_strength=mouse_strength, + mouse_radius=mouse_radius ) # Si plein écran demandé, l'activer immédiatement @@ -59,42 +68,105 @@ def create_deformed_grid(dimension: int = 64, def quick_demo(): - """Démonstration rapide avec paramètres par défaut""" - grid = create_deformed_grid(dimension=64, cell_size=16, distortion_strength=0.3, - color_scheme="rainbow", color_animation=True) + """Démonstration rapide avec interactions souris activées""" + print("🎮 Quick Demo - Mouse Interactions Enabled!") + print("🖱️ Move your mouse to see attraction effects") + print("🎯 Left click: ripple effects, Right click: burst effects") + print("⌨️ TAB: toggle mouse, SPACE: change distortion, ESC: quit") + + grid = create_deformed_grid( + dimension=48, + cell_size=16, + distortion_strength=0.3, + distortion_fn="sine", + color_scheme="rainbow", + color_animation=True, + mouse_interactive=True, + mouse_strength=0.8, + mouse_radius=140.0 + ) grid.run_interactive() def fullscreen_demo(): - """Démonstration en plein écran""" - grid = create_deformed_grid(dimension=80, cell_size=20, distortion_strength=1, - color_scheme="neon", color_animation=True, fullscreen=True) + """Démonstration en plein écran avec souris""" + print("🎮 Fullscreen Demo - Immersive Mouse Experience!") + print("🖱️ Your mouse controls the entire screen!") + print("🎯 Try different areas - F: toggle fullscreen") + + grid = create_deformed_grid( + dimension=64, + cell_size=18, + distortion_strength=1, + distortion_fn="perlin", + color_scheme="neon", + color_animation=True, + fullscreen=True, + mouse_interactive=False, + mouse_strength=0.9, + mouse_radius=60.0 + ) grid.run_interactive() def audio_reactive_demo(): - """Démonstration avec réactivité audio - PARFAIT POUR LA MUSIQUE! 🎵""" + """Démonstration avec réactivité audio + souris - PARFAIT POUR LA MUSIQUE! 🎵""" + print("\n🎵 AUDIO + MOUSE REACTIVE MODE!") + print("🎧 Play your favorite music and watch art dance!") + print("🖱️ Mouse + Music = Amazing visuals!") + print("🔊 Louder music = more intense effects!") + print("🎯 Mouse adds extra interactive layer!") + grid = create_deformed_grid( - dimension=64, - cell_size=18, - distortion_strength=1, # Distorsion de base plus faible (l'audio l'augmente) + dimension=56, + cell_size=16, + distortion_strength=0.4, # Lower base distortion (audio + mouse will boost it) distortion_fn="sine", color_scheme="neon", color_animation=True, audio_reactive=True, - fullscreen=True + fullscreen=True, + mouse_interactive=True, + mouse_strength=0.7, # Moderate mouse strength to blend with audio + mouse_radius=60.0 + ) + grid.run_interactive() + + +def mouse_demo(): + """Démonstration dédiée aux interactions souris""" + print("🖱️ MOUSE INTERACTION SHOWCASE!") + print("=" * 40) + print("🎯 Features to try:") + print(" • Move mouse around for attraction effects") + print(" • Left click: Create ripple effects") + print(" • Right click: Create burst effects") + print(" • 1-7: Change mouse interaction types") + print(" • TAB: Toggle mouse on/off") + print(" • +/-: Adjust mouse strength") + print(" • SPACE: Change base distortion") + print("=" * 40) + + grid = create_deformed_grid( + dimension=40, + cell_size=18, + distortion_strength=0.2, # Low base so mouse effects are prominent + distortion_fn="circular", + color_scheme="ocean", + color_animation=True, + mouse_interactive=True, + mouse_strength=1.0, # Maximum mouse strength for demo + mouse_radius=60.0 # Large radius for dramatic effects ) - print("\n🎵 MODE AUDIO-RÉACTIF ACTIVÉ!") - print("🎧 Lancez votre musique préférée et regardez l'art danser!") - print("🔊 Plus la musique est forte, plus les effets sont intenses!") grid.run_interactive() if __name__ == "__main__": # Choisir la démo à lancer print("🎨 Démonstrations disponibles:") - print("1. quick_demo() - Démonstration rapide") - print("2. fullscreen_demo() - Démonstration plein écran") - print("3. audio_reactive_demo() - Démonstration audio-réactive") - print("\nLancement de la démonstration plein écran...") + print("1. quick_demo() - Démonstration rapide avec souris") + print("2. fullscreen_demo() - Démonstration plein écran avec souris") + print("3. audio_reactive_demo() - Démonstration audio + souris") + print("4. mouse_demo() - Showcase des interactions souris") + print("\nLancement de la démonstration souris...") fullscreen_demo() \ No newline at end of file diff --git a/distorsion_movement/distortions.py b/distorsion_movement/distortions.py index 174c84a..cefe05f 100644 --- a/distorsion_movement/distortions.py +++ b/distorsion_movement/distortions.py @@ -160,6 +160,122 @@ def apply_distortion_circular(base_pos: Tuple[float, float], return (base_pos[0] + dx, base_pos[1] + dy, rotation) + @staticmethod + def apply_distortion_mouse_attraction(base_pos: Tuple[float, float], + params: dict, + cell_size: int, + distortion_strength: float, + time: float, + mouse_engine) -> Tuple[float, float, float]: + """ + Applique une distorsion d'attraction basée sur la souris. + + Args: + base_pos: Position de base (x, y) + params: Paramètres de distorsion + cell_size: Taille de la cellule + distortion_strength: Intensité de distorsion + time: Temps actuel pour l'animation + mouse_engine: Instance du moteur d'interaction souris + + Returns: + Tuple[x, y, rotation] - Position déformée et rotation + """ + if mouse_engine is None: + return (base_pos[0], base_pos[1], 0) + + # Obtenir la force de la souris + mouse_force = mouse_engine.calculate_mouse_force(base_pos) + + # Appliquer la force avec l'intensité de distorsion + force_multiplier = cell_size * distortion_strength * 10.0 # Facteur d'échelle + dx = mouse_force[0] * force_multiplier + dy = mouse_force[1] * force_multiplier + + # Rotation basée sur l'intensité de la force + force_magnitude = math.sqrt(mouse_force[0]**2 + mouse_force[1]**2) + rotation = force_magnitude * distortion_strength * 0.5 + + return (base_pos[0] + dx, base_pos[1] + dy, rotation) + + @staticmethod + def apply_distortion_mouse_repulsion(base_pos: Tuple[float, float], + params: dict, + cell_size: int, + distortion_strength: float, + time: float, + mouse_engine) -> Tuple[float, float, float]: + """ + Applique une distorsion de répulsion basée sur la souris. + + Args: + base_pos: Position de base (x, y) + params: Paramètres de distorsion + cell_size: Taille de la cellule + distortion_strength: Intensité de distorsion + time: Temps actuel pour l'animation + mouse_engine: Instance du moteur d'interaction souris + + Returns: + Tuple[x, y, rotation] - Position déformée et rotation + """ + if mouse_engine is None: + return (base_pos[0], base_pos[1], 0) + + # Changer temporairement le type d'interaction pour la répulsion + original_type = mouse_engine.interaction_type + from distorsion_movement.enums import MouseInteractionType + mouse_engine.interaction_type = MouseInteractionType.REPULSION + + # Obtenir la force de répulsion + mouse_force = mouse_engine.calculate_mouse_force(base_pos) + + # Restaurer le type original + mouse_engine.interaction_type = original_type + + # Appliquer la force avec l'intensité de distorsion + force_multiplier = cell_size * distortion_strength * 10.0 # Facteur d'échelle + dx = mouse_force[0] * force_multiplier + dy = mouse_force[1] * force_multiplier + + # Rotation basée sur l'intensité de la force + force_magnitude = math.sqrt(mouse_force[0]**2 + mouse_force[1]**2) + rotation = force_magnitude * distortion_strength * 0.5 + + return (base_pos[0] + dx, base_pos[1] + dy, rotation) + + @staticmethod + def apply_mouse_distortion(base_pos: Tuple[float, float], + params: dict, + cell_size: int, + distortion_strength: float, + time: float, + mouse_engine, + interaction_type: str = "attraction") -> Tuple[float, float, float]: + """ + Dispatcher générique pour les distorsions de souris. + + Args: + base_pos: Position de base (x, y) + params: Paramètres de distorsion + cell_size: Taille de la cellule + distortion_strength: Intensité de distorsion + time: Temps actuel pour l'animation + mouse_engine: Instance du moteur d'interaction souris + interaction_type: Type d'interaction ("attraction" ou "repulsion") + + Returns: + Tuple[x, y, rotation] - Position déformée et rotation + """ + if interaction_type == "repulsion": + return DistortionEngine.apply_distortion_mouse_repulsion( + base_pos, params, cell_size, distortion_strength, time, mouse_engine + ) + else: + return DistortionEngine.apply_distortion_mouse_attraction( + base_pos, params, cell_size, distortion_strength, time, mouse_engine + ) + @staticmethod def get_distorted_positions(base_positions: List[Tuple[float, float]], distortion_params: List[dict], @@ -167,7 +283,8 @@ def get_distorted_positions(base_positions: List[Tuple[float, float]], cell_size: int, distortion_strength: float, time: float, - canvas_size: Tuple[int, int]) -> List[Tuple[float, float, float]]: + canvas_size: Tuple[int, int], + mouse_engine=None) -> List[Tuple[float, float, float]]: """ Calcule toutes les positions déformées selon la fonction choisie. @@ -179,6 +296,7 @@ def get_distorted_positions(base_positions: List[Tuple[float, float]], distortion_strength: Intensité de distorsion time: Temps actuel pour l'animation canvas_size: Taille du canvas + mouse_engine: Instance du moteur d'interaction souris (optionnel) Returns: Liste de tuples (x, y, rotation) pour chaque carré @@ -202,6 +320,14 @@ def get_distorted_positions(base_positions: List[Tuple[float, float]], pos = DistortionEngine.apply_distortion_circular( base_pos, params, cell_size, distortion_strength, time, canvas_size ) + elif distortion_fn == DistortionType.MOUSE_ATTRACTION.value: + pos = DistortionEngine.apply_distortion_mouse_attraction( + base_pos, params, cell_size, distortion_strength, time, mouse_engine + ) + elif distortion_fn == DistortionType.MOUSE_REPULSION.value: + pos = DistortionEngine.apply_distortion_mouse_repulsion( + base_pos, params, cell_size, distortion_strength, time, mouse_engine + ) else: pos = DistortionEngine.apply_distortion_random( base_pos, params, cell_size, distortion_strength diff --git a/distorsion_movement/enums.py b/distorsion_movement/enums.py index 13dce3a..db1b06e 100644 --- a/distorsion_movement/enums.py +++ b/distorsion_movement/enums.py @@ -1,5 +1,5 @@ """ -Énumérations pour les types de distorsion et schémas de couleurs. +Énumérations pour les types de distorsion, schémas de couleurs et interactions souris. """ from enum import Enum @@ -11,6 +11,8 @@ class DistortionType(Enum): SINE = "sine" PERLIN = "perlin" CIRCULAR = "circular" + MOUSE_ATTRACTION = "mouse_attraction" + MOUSE_REPULSION = "mouse_repulsion" class ColorScheme(Enum): @@ -24,4 +26,32 @@ class ColorScheme(Enum): NEON = "neon" OCEAN = "ocean" FIRE = "fire" - FOREST = "forest" \ No newline at end of file + FOREST = "forest" + + +class MouseInteractionType(Enum): + """Types d'interactions souris disponibles""" + ATTRACTION = "attraction" + REPULSION = "repulsion" + RIPPLE = "ripple" + BURST = "burst" + TRAIL = "trail" + DRAG = "drag" + NONE = "none" + + +class MouseMode(Enum): + """Modes de fonctionnement de la souris""" + CONTINUOUS = "continuous" # Effet continu pendant le mouvement + CLICK_ONLY = "click_only" # Effet seulement sur clic + HOVER = "hover" # Effet au survol + DISABLED = "disabled" # Interactions souris désactivées + + +class MouseButton(Enum): + """Boutons de souris pour les interactions""" + LEFT = "left" + RIGHT = "right" + MIDDLE = "middle" + WHEEL_UP = "wheel_up" + WHEEL_DOWN = "wheel_down" \ No newline at end of file diff --git a/distorsion_movement/mouse_interactions.py b/distorsion_movement/mouse_interactions.py new file mode 100644 index 0000000..199e631 --- /dev/null +++ b/distorsion_movement/mouse_interactions.py @@ -0,0 +1,373 @@ +""" +Moteur d'interactions souris pour les grilles déformées. + +Ce module gère toutes les interactions utilisateur via la souris : +- Attraction/répulsion des carrés vers/depuis le curseur +- Effets de clic (ondulations, explosions) +- Traînées de mouvement +- Interactions de glisser-déposer +""" + +import math +import time +import pygame +from typing import Tuple, List, Dict, Optional, Any +from dataclasses import dataclass + +from distorsion_movement.enums import MouseInteractionType, MouseMode, MouseButton + + +@dataclass +class MouseState: + """État actuel de la souris""" + position: Tuple[int, int] = (0, 0) + prev_position: Tuple[int, int] = (0, 0) + is_pressed: Dict[str, bool] = None + is_dragging: bool = False + drag_start: Optional[Tuple[int, int]] = None + + def __post_init__(self): + if self.is_pressed is None: + self.is_pressed = { + MouseButton.LEFT.value: False, + MouseButton.RIGHT.value: False, + MouseButton.MIDDLE.value: False + } + + +@dataclass +class ClickEffect: + """Représente un effet de clic temporaire""" + position: Tuple[float, float] + effect_type: MouseInteractionType + start_time: float + duration: float = 1.0 + max_radius: float = 100.0 + strength: float = 1.0 + + @property + def progress(self) -> float: + """Progrès de l'effet (0.0 à 1.0)""" + elapsed = time.time() - self.start_time + return min(elapsed / self.duration, 1.0) + + @property + def is_finished(self) -> bool: + """L'effet est-il terminé ?""" + return self.progress >= 1.0 + + @property + def current_radius(self) -> float: + """Rayon actuel de l'effet""" + return self.max_radius * self.progress + + @property + def current_strength(self) -> float: + """Force actuelle de l'effet (avec fade out)""" + fade = 1.0 - self.progress + return self.strength * fade + + +class MouseInteractionEngine: + """ + Moteur principal pour les interactions souris. + + Gère le tracking de la souris, calcule les forces d'interaction, + et maintient les effets temporaires comme les ondulations. + """ + + def __init__(self, + interaction_type: MouseInteractionType = MouseInteractionType.ATTRACTION, + mouse_mode: MouseMode = MouseMode.CONTINUOUS, + strength: float = 0.5, + radius: float = 100.0, + show_feedback: bool = True): + """ + Initialise le moteur d'interactions souris. + + Args: + interaction_type: Type d'interaction par défaut + mouse_mode: Mode de fonctionnement de la souris + strength: Force de l'interaction (0.0 à 1.0) + radius: Rayon d'influence en pixels + show_feedback: Afficher les retours visuels + """ + self.interaction_type = interaction_type + self.mouse_mode = mouse_mode + self.strength = strength + self.radius = radius + self.show_feedback = show_feedback + + # État de la souris + self.mouse_state = MouseState() + + # Effets temporaires actifs + self.active_effects: List[ClickEffect] = [] + + # Historique des positions pour les traînées + self.position_trail: List[Tuple[Tuple[int, int], float]] = [] + self.max_trail_length = 20 + + # Paramètres configurables + self.falloff_power = 2.0 # Puissance de l'atténuation avec la distance + self.min_distance = 1.0 # Distance minimale pour éviter la division par zéro + + def update_mouse_state(self, pygame_events: List[pygame.event.Event]) -> None: + """ + Met à jour l'état de la souris basé sur les événements pygame. + + Args: + pygame_events: Liste des événements pygame à traiter + """ + # Sauvegarder la position précédente + self.mouse_state.prev_position = self.mouse_state.position + + # Obtenir la position actuelle + self.mouse_state.position = pygame.mouse.get_pos() + + # Traiter les événements + for event in pygame_events: + if event.type == pygame.MOUSEBUTTONDOWN: + self._handle_mouse_button_down(event) + elif event.type == pygame.MOUSEBUTTONUP: + self._handle_mouse_button_up(event) + elif event.type == pygame.MOUSEMOTION: + self._handle_mouse_motion(event) + + # Mettre à jour la traînée + self._update_trail() + + # Nettoyer les effets expirés + self._cleanup_expired_effects() + + def _handle_mouse_button_down(self, event: pygame.event.Event) -> None: + """Gère les événements de pression des boutons de souris""" + button_map = { + 1: MouseButton.LEFT.value, + 2: MouseButton.MIDDLE.value, + 3: MouseButton.RIGHT.value + } + + if event.button in button_map: + button = button_map[event.button] + self.mouse_state.is_pressed[button] = True + + # Créer un effet selon le bouton pressé + self._create_click_effect(event.pos, button) + + # Démarrer le drag si c'est le bouton gauche + if button == MouseButton.LEFT.value: + self.mouse_state.is_dragging = True + self.mouse_state.drag_start = event.pos + + def _handle_mouse_button_up(self, event: pygame.event.Event) -> None: + """Gère les événements de relâchement des boutons de souris""" + button_map = { + 1: MouseButton.LEFT.value, + 2: MouseButton.MIDDLE.value, + 3: MouseButton.RIGHT.value + } + + if event.button in button_map: + button = button_map[event.button] + self.mouse_state.is_pressed[button] = False + + # Arrêter le drag + if button == MouseButton.LEFT.value: + self.mouse_state.is_dragging = False + self.mouse_state.drag_start = None + + def _handle_mouse_motion(self, event: pygame.event.Event) -> None: + """Gère les événements de mouvement de souris""" + # Mise à jour automatique via pygame.mouse.get_pos() + pass + + def _create_click_effect(self, position: Tuple[int, int], button: str) -> None: + """Crée un effet de clic selon le bouton pressé""" + current_time = time.time() + + if button == MouseButton.LEFT.value: + # Clic gauche -> effet d'ondulation + effect = ClickEffect( + position=position, + effect_type=MouseInteractionType.RIPPLE, + start_time=current_time, + duration=1.5, + max_radius=self.radius * 1.5, + strength=self.strength + ) + elif button == MouseButton.RIGHT.value: + # Clic droit -> effet d'explosion + effect = ClickEffect( + position=position, + effect_type=MouseInteractionType.BURST, + start_time=current_time, + duration=0.8, + max_radius=self.radius * 0.8, + strength=self.strength * 1.5 + ) + else: + return # Pas d'effet pour les autres boutons + + self.active_effects.append(effect) + + def _update_trail(self) -> None: + """Met à jour la traînée de positions de la souris""" + current_time = time.time() + + # Ajouter la position actuelle si elle a changé + if self.mouse_state.position != self.mouse_state.prev_position: + self.position_trail.append((self.mouse_state.position, current_time)) + + # Limiter la longueur de la traînée + if len(self.position_trail) > self.max_trail_length: + self.position_trail.pop(0) + + # Supprimer les positions trop anciennes (> 2 secondes) + cutoff_time = current_time - 2.0 + self.position_trail = [ + (pos, t) for pos, t in self.position_trail + if t > cutoff_time + ] + + def _cleanup_expired_effects(self) -> None: + """Supprime les effets expirés""" + self.active_effects = [ + effect for effect in self.active_effects + if not effect.is_finished + ] + + def calculate_mouse_force(self, square_pos: Tuple[float, float]) -> Tuple[float, float]: + """ + Calcule la force exercée par la souris sur un carré. + + Args: + square_pos: Position du carré (x, y) + + Returns: + Tuple[force_x, force_y]: Force à appliquer au carré + """ + if self.mouse_mode == MouseMode.DISABLED: + return (0.0, 0.0) + + total_force_x = 0.0 + total_force_y = 0.0 + + # Force de l'interaction principale (curseur) + if (self.mouse_mode == MouseMode.CONTINUOUS or + (self.mouse_mode == MouseMode.HOVER and self._is_hovering())): + + force_x, force_y = self._calculate_interaction_force( + square_pos, self.mouse_state.position, + self.interaction_type, self.strength + ) + total_force_x += force_x + total_force_y += force_y + + # Forces des effets de clic actifs + for effect in self.active_effects: + force_x, force_y = self._calculate_interaction_force( + square_pos, effect.position, + effect.effect_type, effect.current_strength + ) + total_force_x += force_x + total_force_y += force_y + + return (total_force_x, total_force_y) + + def _calculate_interaction_force(self, + square_pos: Tuple[float, float], + source_pos: Tuple[float, float], + interaction_type: MouseInteractionType, + strength: float) -> Tuple[float, float]: + """Calcule la force d'interaction entre un carré et une source""" + # Calculer la distance + dx = square_pos[0] - source_pos[0] + dy = square_pos[1] - source_pos[1] + distance = math.sqrt(dx * dx + dy * dy) + + # Éviter la division par zéro + if distance < self.min_distance: + return (0.0, 0.0) + + # Vérifier si dans le rayon d'influence + if distance > self.radius: + return (0.0, 0.0) + + # Calculer l'atténuation basée sur la distance + falloff = 1.0 - (distance / self.radius) ** self.falloff_power + + # Direction normalisée + norm_dx = dx / distance + norm_dy = dy / distance + + # Force selon le type d'interaction + if interaction_type in [MouseInteractionType.ATTRACTION, + MouseInteractionType.RIPPLE]: + # Attraction vers la source + force_magnitude = -strength * falloff + elif interaction_type in [MouseInteractionType.REPULSION, + MouseInteractionType.BURST]: + # Répulsion depuis la source + force_magnitude = strength * falloff + else: + force_magnitude = 0.0 + + return (norm_dx * force_magnitude, norm_dy * force_magnitude) + + def _is_hovering(self) -> bool: + """Vérifie si la souris survole actuellement la zone""" + # Pour l'instant, considère toujours qu'on survole + # Peut être étendu pour vérifier des zones spécifiques + return True + + def get_visual_feedback_data(self) -> Dict[str, Any]: + """ + Retourne les données pour le feedback visuel. + + Returns: + Dictionnaire contenant les données de visualisation + """ + if not self.show_feedback: + return {} + + feedback_data = { + 'mouse_position': self.mouse_state.position, + 'interaction_radius': self.radius, + 'interaction_type': self.interaction_type, + 'active_effects': [ + { + 'position': effect.position, + 'type': effect.effect_type, + 'radius': effect.current_radius, + 'strength': effect.current_strength, + 'progress': effect.progress + } + for effect in self.active_effects + ], + 'trail': [pos for pos, _ in self.position_trail[-10:]] # 10 dernières positions + } + + return feedback_data + + # Méthodes de configuration + def set_interaction_type(self, interaction_type: MouseInteractionType) -> None: + """Change le type d'interaction principal""" + self.interaction_type = interaction_type + + def set_mouse_mode(self, mouse_mode: MouseMode) -> None: + """Change le mode de fonctionnement de la souris""" + self.mouse_mode = mouse_mode + + def set_strength(self, strength: float) -> None: + """Modifie la force d'interaction (0.0 à 1.0)""" + self.strength = max(0.0, min(1.0, strength)) + + def set_radius(self, radius: float) -> None: + """Modifie le rayon d'influence""" + self.radius = max(10.0, radius) + + def clear_effects(self) -> None: + """Supprime tous les effets actifs""" + self.active_effects.clear() + self.position_trail.clear() \ No newline at end of file diff --git a/distorsion_movement/tests/conftest.py b/distorsion_movement/tests/conftest.py index 9c19a96..1a9d3a5 100644 --- a/distorsion_movement/tests/conftest.py +++ b/distorsion_movement/tests/conftest.py @@ -57,6 +57,23 @@ def sample_colors(): ] +@pytest.fixture +def sample_mouse_positions(): + """Provide sample mouse positions for testing.""" + return [ + (0, 0), + (100, 100), + (200, 150), + (50, 75) + ] + + +@pytest.fixture +def mock_pygame_events(): + """Mock pygame events for mouse interaction testing.""" + return [] + + # Configure pytest markers def pytest_configure(config): """Configure custom pytest markers.""" diff --git a/distorsion_movement/tests/test_enums.py b/distorsion_movement/tests/test_enums.py index 22b8037..da24836 100644 --- a/distorsion_movement/tests/test_enums.py +++ b/distorsion_movement/tests/test_enums.py @@ -3,7 +3,9 @@ """ import pytest -from distorsion_movement.enums import DistortionType, ColorScheme +from distorsion_movement.enums import ( + DistortionType, ColorScheme, MouseInteractionType, MouseMode, MouseButton +) class TestDistortionType: @@ -15,19 +17,23 @@ def test_distortion_type_values(self): assert DistortionType.SINE.value == "sine" assert DistortionType.PERLIN.value == "perlin" assert DistortionType.CIRCULAR.value == "circular" + assert DistortionType.MOUSE_ATTRACTION.value == "mouse_attraction" + assert DistortionType.MOUSE_REPULSION.value == "mouse_repulsion" def test_distortion_type_count(self): """Test that we have the expected number of distortion types.""" - assert len(DistortionType) == 4 + assert len(DistortionType) == 6 def test_distortion_type_iteration(self): """Test that we can iterate over all distortion types.""" types = list(DistortionType) - assert len(types) == 4 + assert len(types) == 6 assert DistortionType.RANDOM in types assert DistortionType.SINE in types assert DistortionType.PERLIN in types assert DistortionType.CIRCULAR in types + assert DistortionType.MOUSE_ATTRACTION in types + assert DistortionType.MOUSE_REPULSION in types class TestColorScheme: @@ -56,4 +62,84 @@ def test_color_scheme_iteration(self): assert len(schemes) == 10 assert ColorScheme.MONOCHROME in schemes assert ColorScheme.RAINBOW in schemes - assert ColorScheme.NEON in schemes \ No newline at end of file + assert ColorScheme.NEON in schemes + + +class TestMouseInteractionType: + """Test cases for MouseInteractionType enum.""" + + def test_mouse_interaction_type_values(self): + """Test that all mouse interaction types have correct values.""" + assert MouseInteractionType.ATTRACTION.value == "attraction" + assert MouseInteractionType.REPULSION.value == "repulsion" + assert MouseInteractionType.RIPPLE.value == "ripple" + assert MouseInteractionType.BURST.value == "burst" + assert MouseInteractionType.TRAIL.value == "trail" + assert MouseInteractionType.DRAG.value == "drag" + assert MouseInteractionType.NONE.value == "none" + + def test_mouse_interaction_type_count(self): + """Test that we have the expected number of mouse interaction types.""" + assert len(MouseInteractionType) == 7 + + def test_mouse_interaction_type_iteration(self): + """Test that we can iterate over all mouse interaction types.""" + types = list(MouseInteractionType) + assert len(types) == 7 + assert MouseInteractionType.ATTRACTION in types + assert MouseInteractionType.REPULSION in types + assert MouseInteractionType.RIPPLE in types + assert MouseInteractionType.BURST in types + assert MouseInteractionType.TRAIL in types + assert MouseInteractionType.DRAG in types + assert MouseInteractionType.NONE in types + + +class TestMouseMode: + """Test cases for MouseMode enum.""" + + def test_mouse_mode_values(self): + """Test that all mouse modes have correct values.""" + assert MouseMode.CONTINUOUS.value == "continuous" + assert MouseMode.CLICK_ONLY.value == "click_only" + assert MouseMode.HOVER.value == "hover" + assert MouseMode.DISABLED.value == "disabled" + + def test_mouse_mode_count(self): + """Test that we have the expected number of mouse modes.""" + assert len(MouseMode) == 4 + + def test_mouse_mode_iteration(self): + """Test that we can iterate over all mouse modes.""" + modes = list(MouseMode) + assert len(modes) == 4 + assert MouseMode.CONTINUOUS in modes + assert MouseMode.CLICK_ONLY in modes + assert MouseMode.HOVER in modes + assert MouseMode.DISABLED in modes + + +class TestMouseButton: + """Test cases for MouseButton enum.""" + + def test_mouse_button_values(self): + """Test that all mouse buttons have correct values.""" + assert MouseButton.LEFT.value == "left" + assert MouseButton.RIGHT.value == "right" + assert MouseButton.MIDDLE.value == "middle" + assert MouseButton.WHEEL_UP.value == "wheel_up" + assert MouseButton.WHEEL_DOWN.value == "wheel_down" + + def test_mouse_button_count(self): + """Test that we have the expected number of mouse buttons.""" + assert len(MouseButton) == 5 + + def test_mouse_button_iteration(self): + """Test that we can iterate over all mouse buttons.""" + buttons = list(MouseButton) + assert len(buttons) == 5 + assert MouseButton.LEFT in buttons + assert MouseButton.RIGHT in buttons + assert MouseButton.MIDDLE in buttons + assert MouseButton.WHEEL_UP in buttons + assert MouseButton.WHEEL_DOWN in buttons \ No newline at end of file diff --git a/distorsion_movement/tests/test_mouse_interactions.py b/distorsion_movement/tests/test_mouse_interactions.py new file mode 100644 index 0000000..7301c39 --- /dev/null +++ b/distorsion_movement/tests/test_mouse_interactions.py @@ -0,0 +1,431 @@ +""" +Unit tests for mouse_interactions module. +""" + +import pytest +import time +from unittest.mock import MagicMock, patch +import pygame + +from distorsion_movement.enums import MouseInteractionType, MouseMode, MouseButton +from distorsion_movement.mouse_interactions import ( + MouseInteractionEngine, MouseState, ClickEffect +) + + +class TestMouseState: + """Test cases for MouseState class.""" + + def test_mouse_state_initialization(self): + """Test that MouseState initializes with correct defaults.""" + state = MouseState() + + assert state.position == (0, 0) + assert state.prev_position == (0, 0) + assert state.is_dragging is False + assert state.drag_start is None + assert isinstance(state.is_pressed, dict) + assert len(state.is_pressed) == 3 + assert state.is_pressed[MouseButton.LEFT.value] is False + assert state.is_pressed[MouseButton.RIGHT.value] is False + assert state.is_pressed[MouseButton.MIDDLE.value] is False + + def test_mouse_state_custom_initialization(self): + """Test MouseState with custom initial values.""" + state = MouseState( + position=(100, 200), + prev_position=(90, 190), + is_dragging=True, + drag_start=(50, 60) + ) + + assert state.position == (100, 200) + assert state.prev_position == (90, 190) + assert state.is_dragging is True + assert state.drag_start == (50, 60) + + +class TestClickEffect: + """Test cases for ClickEffect class.""" + + def test_click_effect_initialization(self): + """Test that ClickEffect initializes correctly.""" + start_time = time.time() + effect = ClickEffect( + position=(100.0, 100.0), + effect_type=MouseInteractionType.RIPPLE, + start_time=start_time, + duration=1.0, + max_radius=50.0, + strength=0.7 + ) + + assert effect.position == (100.0, 100.0) + assert effect.effect_type == MouseInteractionType.RIPPLE + assert effect.start_time == start_time + assert effect.duration == 1.0 + assert effect.max_radius == 50.0 + assert effect.strength == 0.7 + + def test_click_effect_progress_calculation(self): + """Test progress calculation over time.""" + start_time = time.time() + effect = ClickEffect( + position=(0.0, 0.0), + effect_type=MouseInteractionType.RIPPLE, + start_time=start_time, + duration=1.0 + ) + + # Initially should be near 0 + assert 0.0 <= effect.progress <= 0.1 + + # After half duration, should be around 0.5 + effect.start_time = start_time - 0.5 + assert 0.4 <= effect.progress <= 0.6 + + # After full duration, should be 1.0 + effect.start_time = start_time - 1.0 + assert effect.progress == 1.0 + + # After more than duration, should be capped at 1.0 + effect.start_time = start_time - 2.0 + assert effect.progress == 1.0 + + def test_click_effect_is_finished(self): + """Test finished state detection.""" + start_time = time.time() + effect = ClickEffect( + position=(0.0, 0.0), + effect_type=MouseInteractionType.RIPPLE, + start_time=start_time, + duration=0.5 + ) + + # Should not be finished initially + assert not effect.is_finished + + # Should be finished after duration + effect.start_time = start_time - 0.6 + assert effect.is_finished + + def test_click_effect_current_properties(self): + """Test current radius and strength calculations.""" + start_time = time.time() - 0.5 # Half way through + effect = ClickEffect( + position=(0.0, 0.0), + effect_type=MouseInteractionType.RIPPLE, + start_time=start_time, + duration=1.0, + max_radius=100.0, + strength=1.0 + ) + + # At 50% progress + assert 45.0 <= effect.current_radius <= 55.0 + assert 0.45 <= effect.current_strength <= 0.55 + + +class TestMouseInteractionEngine: + """Test cases for MouseInteractionEngine class.""" + + @pytest.fixture + def engine(self): + """Create a basic MouseInteractionEngine for testing.""" + return MouseInteractionEngine( + interaction_type=MouseInteractionType.ATTRACTION, + mouse_mode=MouseMode.CONTINUOUS, + strength=0.5, + radius=100.0 + ) + + def test_engine_initialization(self, engine): + """Test that MouseInteractionEngine initializes correctly.""" + assert engine.interaction_type == MouseInteractionType.ATTRACTION + assert engine.mouse_mode == MouseMode.CONTINUOUS + assert engine.strength == 0.5 + assert engine.radius == 100.0 + assert engine.show_feedback is True + assert isinstance(engine.mouse_state, MouseState) + assert len(engine.active_effects) == 0 + assert len(engine.position_trail) == 0 + + def test_engine_configuration_methods(self, engine): + """Test configuration setter methods.""" + # Test interaction type change + engine.set_interaction_type(MouseInteractionType.REPULSION) + assert engine.interaction_type == MouseInteractionType.REPULSION + + # Test mouse mode change + engine.set_mouse_mode(MouseMode.CLICK_ONLY) + assert engine.mouse_mode == MouseMode.CLICK_ONLY + + # Test strength change + engine.set_strength(0.8) + assert engine.strength == 0.8 + + # Test strength bounds + engine.set_strength(1.5) # Should be capped at 1.0 + assert engine.strength == 1.0 + + engine.set_strength(-0.5) # Should be capped at 0.0 + assert engine.strength == 0.0 + + # Test radius change + engine.set_radius(150.0) + assert engine.radius == 150.0 + + # Test radius minimum + engine.set_radius(5.0) # Should be at least 10.0 + assert engine.radius == 10.0 + + def test_force_calculation_basic(self, engine): + """Test basic force calculations.""" + # Set mouse position + engine.mouse_state.position = (0, 0) + + # Test force at mouse position (should be 0 due to min_distance) + force = engine.calculate_mouse_force((0.0, 0.0)) + assert force == (0.0, 0.0) + + # Test force at distance 50 (within radius) + force = engine.calculate_mouse_force((50.0, 0.0)) + assert force[0] < 0 # Attraction should pull towards mouse (negative) + assert force[1] == 0.0 + + # Test force outside radius + force = engine.calculate_mouse_force((150.0, 0.0)) + assert force == (0.0, 0.0) + + def test_force_calculation_repulsion(self): + """Test repulsion force calculations.""" + engine = MouseInteractionEngine( + interaction_type=MouseInteractionType.REPULSION, + strength=1.0, + radius=100.0 + ) + engine.mouse_state.position = (0, 0) + + # Test repulsion force + force = engine.calculate_mouse_force((50.0, 0.0)) + assert force[0] > 0 # Repulsion should push away from mouse (positive) + assert force[1] == 0.0 + + def test_force_calculation_disabled_mode(self, engine): + """Test that disabled mode returns zero forces.""" + engine.set_mouse_mode(MouseMode.DISABLED) + engine.mouse_state.position = (0, 0) + + force = engine.calculate_mouse_force((50.0, 0.0)) + assert force == (0.0, 0.0) + + @patch('pygame.event.Event') + def test_mouse_button_down_handling(self, mock_event, engine): + """Test mouse button down event handling.""" + # Create mock event + mock_event.type = pygame.MOUSEBUTTONDOWN + mock_event.button = 1 # Left button + mock_event.pos = (100, 100) + + initial_effects = len(engine.active_effects) + engine._handle_mouse_button_down(mock_event) + + # Should have created a new effect + assert len(engine.active_effects) == initial_effects + 1 + assert engine.mouse_state.is_pressed[MouseButton.LEFT.value] is True + assert engine.mouse_state.is_dragging is True + assert engine.mouse_state.drag_start == (100, 100) + + @patch('pygame.event.Event') + def test_mouse_button_up_handling(self, mock_event, engine): + """Test mouse button up event handling.""" + # First press the button + engine.mouse_state.is_pressed[MouseButton.LEFT.value] = True + engine.mouse_state.is_dragging = True + engine.mouse_state.drag_start = (50, 50) + + # Create mock release event + mock_event.type = pygame.MOUSEBUTTONUP + mock_event.button = 1 # Left button + + engine._handle_mouse_button_up(mock_event) + + assert engine.mouse_state.is_pressed[MouseButton.LEFT.value] is False + assert engine.mouse_state.is_dragging is False + assert engine.mouse_state.drag_start is None + + def test_click_effect_creation(self, engine): + """Test click effect creation for different buttons.""" + initial_count = len(engine.active_effects) + + # Test left click (should create ripple) + engine._create_click_effect((100, 100), MouseButton.LEFT.value) + assert len(engine.active_effects) == initial_count + 1 + assert engine.active_effects[-1].effect_type == MouseInteractionType.RIPPLE + + # Test right click (should create burst) + engine._create_click_effect((200, 200), MouseButton.RIGHT.value) + assert len(engine.active_effects) == initial_count + 2 + assert engine.active_effects[-1].effect_type == MouseInteractionType.BURST + + # Test middle click (should not create effect) + engine._create_click_effect((300, 300), MouseButton.MIDDLE.value) + assert len(engine.active_effects) == initial_count + 2 + + def test_trail_update(self, engine): + """Test mouse trail updating.""" + # Simulate mouse movement + engine.mouse_state.prev_position = (0, 0) + engine.mouse_state.position = (10, 10) + + initial_trail_length = len(engine.position_trail) + engine._update_trail() + + # Should have added position to trail + assert len(engine.position_trail) == initial_trail_length + 1 + assert engine.position_trail[-1][0] == (10, 10) + + def test_effects_cleanup(self, engine): + """Test expired effects cleanup.""" + # Add an expired effect + old_time = time.time() - 10.0 + expired_effect = ClickEffect( + position=(0.0, 0.0), + effect_type=MouseInteractionType.RIPPLE, + start_time=old_time, + duration=1.0 + ) + engine.active_effects.append(expired_effect) + + # Add a current effect + current_effect = ClickEffect( + position=(0.0, 0.0), + effect_type=MouseInteractionType.RIPPLE, + start_time=time.time(), + duration=1.0 + ) + engine.active_effects.append(current_effect) + + assert len(engine.active_effects) == 2 + engine._cleanup_expired_effects() + + # Should have removed only the expired effect + assert len(engine.active_effects) == 1 + assert engine.active_effects[0] == current_effect + + def test_clear_effects(self, engine): + """Test clearing all effects.""" + # Add some effects + engine.active_effects.append(ClickEffect( + position=(0.0, 0.0), + effect_type=MouseInteractionType.RIPPLE, + start_time=time.time() + )) + engine.position_trail.append(((100, 100), time.time())) + + engine.clear_effects() + + assert len(engine.active_effects) == 0 + assert len(engine.position_trail) == 0 + + def test_visual_feedback_data(self, engine): + """Test visual feedback data generation.""" + # Set up some data + engine.mouse_state.position = (150, 200) + engine.active_effects.append(ClickEffect( + position=(100.0, 100.0), + effect_type=MouseInteractionType.RIPPLE, + start_time=time.time() + )) + engine.position_trail.append(((120, 130), time.time())) + + feedback = engine.get_visual_feedback_data() + + assert 'mouse_position' in feedback + assert feedback['mouse_position'] == (150, 200) + assert 'interaction_radius' in feedback + assert feedback['interaction_radius'] == engine.radius + assert 'interaction_type' in feedback + assert feedback['interaction_type'] == engine.interaction_type + assert 'active_effects' in feedback + assert len(feedback['active_effects']) == 1 + assert 'trail' in feedback + + def test_visual_feedback_disabled(self): + """Test that visual feedback can be disabled.""" + engine = MouseInteractionEngine(show_feedback=False) + feedback = engine.get_visual_feedback_data() + + assert feedback == {} + + +class TestMouseInteractionIntegration: + """Integration tests for mouse interactions.""" + + @pytest.mark.integration + def test_full_interaction_cycle(self): + """Test a complete interaction cycle.""" + engine = MouseInteractionEngine( + interaction_type=MouseInteractionType.ATTRACTION, + strength=0.8, + radius=100.0 + ) + + # Simulate mouse movement and click + engine.mouse_state.position = (50, 50) + + # Create a click effect + engine._create_click_effect((50, 50), MouseButton.LEFT.value) + + # Calculate forces for nearby squares + test_positions = [ + (0.0, 0.0), # Close to mouse + (25.0, 25.0), # Medium distance + (75.0, 75.0), # Edge of range + (150.0, 150.0) # Outside range + ] + + forces = [engine.calculate_mouse_force(pos) for pos in test_positions] + + # Verify force characteristics + assert forces[0] != (0.0, 0.0) # Should have force + assert forces[1] != (0.0, 0.0) # Should have force + assert forces[2] != (0.0, 0.0) # Should have force (from click effect) + assert forces[3] == (0.0, 0.0) # Should have no force (too far) + + # Verify effects are working + assert len(engine.active_effects) == 1 + assert not engine.active_effects[0].is_finished + + @pytest.mark.integration + def test_multiple_effects_combination(self): + """Test combining multiple click effects.""" + engine = MouseInteractionEngine() + + # Create multiple effects + engine._create_click_effect((0, 0), MouseButton.LEFT.value) + engine._create_click_effect((100, 0), MouseButton.RIGHT.value) + + assert len(engine.active_effects) == 2 + + # Test force at midpoint (should be affected by both) + force = engine.calculate_mouse_force((50.0, 0.0)) + assert force != (0.0, 0.0) + + @pytest.mark.unit + def test_force_calculation_edge_cases(self): + """Test edge cases in force calculations.""" + engine = MouseInteractionEngine(radius=100.0, strength=1.0) + engine.mouse_state.position = (0, 0) + + # Test exactly at boundary + force = engine.calculate_mouse_force((100.0, 0.0)) + assert force == (0.0, 0.0) # Should be zero at exact boundary + + # Test just inside boundary + force = engine.calculate_mouse_force((99.0, 0.0)) + assert force != (0.0, 0.0) # Should have some force + + # Test very close to mouse (min_distance protection) + force = engine.calculate_mouse_force((0.5, 0.0)) + assert force == (0.0, 0.0) # Should be zero due to min_distance \ No newline at end of file diff --git a/mouse_interaction_todo.md b/mouse_interaction_todo.md new file mode 100644 index 0000000..4516dce --- /dev/null +++ b/mouse_interaction_todo.md @@ -0,0 +1,404 @@ +# 🖱️ Mouse Interactions Implementation Plan +# Distorsion Movement - Interactive Generative Art Engine + +## 📋 **Project Status Overview** + +### ✅ **Phase 1 - Foundation (COMPLETED)** +- [x] Enhanced enums with mouse interaction types +- [x] Core MouseInteractionEngine class +- [x] Basic force calculation algorithms +- [x] Mouse state tracking and click effects +- [x] Manual testing of foundational components + +### 🚧 **Phase 2 - Core Integration (IN PROGRESS)** +- [ ] Integrate MouseInteractionEngine with DeformedGrid +- [ ] Add mouse-based distortion functions to DistortionEngine +- [ ] Update pygame event handling for mouse interactions +- [ ] Create manual test for basic mouse-grid integration + +### 📅 **Phase 3 - Advanced Features (PLANNED)** +- [ ] Mouse movement trail system +- [ ] Interactive visual feedback +- [ ] Drag and advanced interactions +- [ ] Performance optimization + +### 📅 **Phase 4 - Polish & Integration (PLANNED)** +- [ ] Audio-mouse combination effects +- [ ] Comprehensive documentation +- [ ] Demo functions and examples + +--- + +## 🎯 **Detailed Implementation Roadmap** + +### **Step 1: Foundation ✅ COMPLETED** +**Files Created/Modified:** +- ✅ `distorsion_movement/enums.py` - Added mouse enums +- ✅ `distorsion_movement/mouse_interactions.py` - Core engine +- ✅ `test_mouse_step1.py` - Validation tests + +**Achievements:** +- 7 mouse interaction types (attraction, repulsion, ripple, burst, trail, drag, none) +- 4 mouse modes (continuous, click_only, hover, disabled) +- Force calculation with distance falloff working perfectly +- Click effects with temporal progression +- Manual testing showing 100% success rate + +--- + +### **Step 2: Core Integration 🚧 IN PROGRESS** + +#### **2.1 - Distortion Engine Enhancement** +**File:** `distorsion_movement/distortions.py` +**Tasks:** +- [ ] Add `apply_distortion_mouse_attraction()` function +- [ ] Add `apply_distortion_mouse_repulsion()` function +- [ ] Create `apply_mouse_distortion()` wrapper function +- [ ] Integrate with existing distortion dispatcher +- [ ] Add mouse force combination with other distortions + +**New Functions to Implement:** +```python +@staticmethod +def apply_distortion_mouse_attraction(base_pos, params, cell_size, + distortion_strength, time, mouse_engine): + # Calculate mouse attraction forces and apply to square position + +@staticmethod +def apply_distortion_mouse_repulsion(base_pos, params, cell_size, + distortion_strength, time, mouse_engine): + # Calculate mouse repulsion forces and apply to square position + +@staticmethod +def apply_mouse_distortion(base_pos, params, cell_size, distortion_strength, + time, mouse_engine, interaction_type): + # Generic mouse distortion dispatcher +``` + +#### **2.2 - DeformedGrid Integration** +**File:** `distorsion_movement/deformed_grid.py` +**Tasks:** +- [ ] Add mouse interaction parameters to `__init__()` +- [ ] Create `MouseInteractionEngine` instance +- [ ] Update `_get_distorted_positions()` to include mouse forces +- [ ] Modify event handling loop to process mouse events +- [ ] Add mouse interaction controls to keyboard shortcuts + +**New Constructor Parameters:** +```python +def __init__(self, + # ... existing parameters ... + mouse_interactive: bool = True, + mouse_mode: MouseInteractionType = MouseInteractionType.ATTRACTION, + mouse_strength: float = 0.5, + mouse_radius: float = 100.0, + show_mouse_feedback: bool = True): +``` + +**New Methods to Add:** +```python +def _update_mouse_interactions(self, pygame_events): + # Update mouse engine with current events + +def _apply_mouse_forces_to_positions(self, positions): + # Apply mouse forces to calculated positions + +def _render_mouse_feedback(self): + # Draw mouse interaction visual feedback +``` + +#### **2.3 - Event Handling Enhancement** +**File:** `distorsion_movement/deformed_grid.py` (run_interactive method) +**Tasks:** +- [ ] Process `pygame.MOUSEBUTTONDOWN` events +- [ ] Process `pygame.MOUSEBUTTONUP` events +- [ ] Process `pygame.MOUSEMOTION` events +- [ ] Add new keyboard shortcuts for mouse controls +- [ ] Update help text with mouse controls + +**New Keyboard Controls:** +- `TAB` - Toggle mouse interactions on/off +- `SHIFT+TAB` - Toggle mouse visual feedback +- `1-7` - Cycle through mouse interaction types +- `SHIFT+1-4` - Change mouse mode +- `CTRL+CLICK` - Clear all mouse effects +- `SHIFT+CLICK` - Create persistent effect + +#### **2.4 - Manual Testing** +**File:** `test_mouse_step2.py` +**Tasks:** +- [ ] Create interactive test with visible grid +- [ ] Test mouse attraction/repulsion +- [ ] Test click effects (ripple, burst) +- [ ] Test keyboard controls for mouse interactions +- [ ] Verify integration with existing distortions +- [ ] Performance testing with large grids + +--- + +### **Step 3: Advanced Features** + +#### **3.1 - Mouse Trail System** +**Files:** `distorsion_movement/mouse_interactions.py`, `distorsion_movement/deformed_grid.py` +**Tasks:** +- [ ] Implement trail position tracking +- [ ] Add trail fade-out over time +- [ ] Create trail-based distortion effects +- [ ] Add trail visualization +- [ ] Configurable trail parameters (length, intensity, fade speed) + +#### **3.2 - Visual Feedback System** +**File:** `distorsion_movement/mouse_feedback.py` (NEW) +**Tasks:** +- [ ] Create `MouseFeedbackRenderer` class +- [ ] Draw interaction radius around cursor +- [ ] Show active click effects as expanding circles +- [ ] Display current interaction mode +- [ ] Add cursor state indicators +- [ ] Implement trail visualization + +#### **3.3 - Drag Interactions** +**Files:** Multiple +**Tasks:** +- [ ] Implement click-and-drag square selection +- [ ] Add group manipulation of selected squares +- [ ] Create elastic band effects for dragged squares +- [ ] Add drag preview and ghost effects +- [ ] Implement snap-to-grid functionality + +#### **3.4 - Scroll/Wheel Interactions** +**Files:** `distorsion_movement/mouse_interactions.py`, `distorsion_movement/deformed_grid.py` +**Tasks:** +- [ ] Mouse wheel zoom in/out functionality +- [ ] Scroll to adjust local distortion intensity +- [ ] Scroll to modify color intensity in mouse area +- [ ] Smooth zoom transitions and limits + +--- + +### **Step 4: Polish & Advanced Integration** + +#### **4.1 - Audio-Mouse Combination** +**Files:** `distorsion_movement/audio_analyzer.py`, `distorsion_movement/deformed_grid.py` +**Tasks:** +- [ ] Combine mouse interactions with audio reactivity +- [ ] Mouse-triggered audio-synchronized effects +- [ ] Beat detection enhancing mouse effects +- [ ] Audio-reactive mouse trail intensity +- [ ] Dynamic mouse radius based on audio volume + +#### **4.2 - Performance Optimization** +**Files:** Multiple +**Tasks:** +- [ ] Implement spatial partitioning for mouse effects +- [ ] Optimize distance calculations for large grids +- [ ] Add LOD (Level of Detail) for distant squares +- [ ] Implement efficient mouse effect caching +- [ ] Add performance monitoring and FPS targets + +#### **4.3 - Enhanced Visual Effects** +**File:** `distorsion_movement/mouse_effects.py` (NEW) +**Tasks:** +- [ ] Particle systems for click effects +- [ ] Glow effects around mouse cursor +- [ ] Motion blur for fast mouse movement +- [ ] Advanced ripple/wave propagation +- [ ] Color bleeding effects from mouse area + +#### **4.4 - Configuration System** +**File:** `distorsion_movement/mouse_config.py` (NEW) +**Tasks:** +- [ ] Mouse interaction presets system +- [ ] Save/load mouse configuration +- [ ] Runtime parameter adjustment +- [ ] Configuration validation +- [ ] Migration between config versions + +--- + +## 🧪 **Testing Strategy** + +### **Unit Tests** +**File:** `distorsion_movement/tests/test_mouse_interactions.py` (NEW) +- [ ] Test MouseInteractionEngine initialization +- [ ] Test force calculation algorithms +- [ ] Test click effect progression +- [ ] Test mouse state management +- [ ] Test configuration changes +- [ ] Test edge cases and boundary conditions + +### **Integration Tests** +**File:** `distorsion_movement/tests/test_mouse_integration.py` (NEW) +- [ ] Test mouse-distortion integration +- [ ] Test mouse-audio combination +- [ ] Test event handling pipeline +- [ ] Test performance with large grids +- [ ] Test state persistence + +### **Manual Testing Checkpoints** +- [ ] **Step 2**: Basic mouse attraction/repulsion working +- [ ] **Step 3**: Advanced interactions and visual feedback +- [ ] **Step 4**: Audio-mouse combination and optimization +- [ ] **Final**: Full feature integration and polish + +--- + +## 📁 **File Structure Changes** + +### **New Files to Create:** +``` +distorsion_movement/ +├── mouse_interactions.py ✅ CREATED +├── mouse_effects.py 📅 PLANNED +├── mouse_feedback.py 📅 PLANNED +├── mouse_config.py 📅 PLANNED +└── tests/ + ├── test_mouse_interactions.py 📅 PLANNED + └── test_mouse_integration.py 📅 PLANNED +``` + +### **Files to Modify:** +``` +distorsion_movement/ +├── enums.py ✅ UPDATED +├── deformed_grid.py 🚧 IN PROGRESS +├── distortions.py 🚧 IN PROGRESS +├── audio_analyzer.py 📅 PLANNED +└── demos.py 📅 PLANNED +``` + +--- + +## 🎮 **Enhanced Control Scheme** + +### **Mouse Controls:** +| Input | Action | +|-------|--------| +| **Mouse Move** | Continuous attraction/repulsion (if enabled) | +| **Left Click** | Ripple effect at cursor position | +| **Right Click** | Burst effect at cursor position | +| **Middle Click** | Toggle between attraction/repulsion | +| **Mouse Wheel ↑** | Increase mouse effect strength | +| **Mouse Wheel ↓** | Decrease mouse effect strength | +| **Shift + Click** | Create persistent effect point | +| **Ctrl + Click** | Clear all mouse effects | + +### **Keyboard Controls (NEW):** +| Key | Action | +|-----|--------| +| **TAB** | Toggle mouse interactions on/off | +| **Shift + TAB** | Toggle visual mouse feedback | +| **1-7** | Cycle through mouse interaction types | +| **Shift + 1-4** | Change mouse mode (continuous/click/hover/disabled) | +| **Ctrl + M** | Reset all mouse settings to default | + +### **Existing Controls (PRESERVED):** +| Key | Action | +|-----|--------| +| **ESC** | Exit application | +| **F** | Toggle fullscreen/windowed mode | +| **SPACE** | Change distortion type | +| **C** | Change color scheme | +| **A** | Toggle color animation | +| **M** | Toggle audio reactivity | +| **+/-** | Adjust distortion intensity | +| **R** | Regenerate parameters | +| **S** | Save image | + +--- + +## 🚀 **API Design Overview** + +### **Constructor Enhancement:** +```python +DeformedGrid( + # Existing parameters... + dimension=64, + cell_size=8, + distortion_strength=0.5, + + # New mouse parameters + mouse_interactive=True, # Enable mouse interactions + mouse_mode=MouseInteractionType.ATTRACTION, # Default interaction type + mouse_strength=0.5, # Mouse effect strength (0.0-1.0) + mouse_radius=100.0, # Mouse effect radius in pixels + show_mouse_feedback=True, # Show visual feedback + mouse_falloff_power=2.0, # Distance falloff curve + mouse_click_duration=1.0, # Click effect duration +) +``` + +### **New Public Methods:** +```python +# Mouse configuration +grid.set_mouse_interaction_type(MouseInteractionType.REPULSION) +grid.set_mouse_strength(0.8) +grid.set_mouse_radius(150.0) +grid.toggle_mouse_feedback() + +# Mouse state queries +is_interactive = grid.is_mouse_interactive() +current_mode = grid.get_mouse_interaction_type() +active_effects = grid.get_active_mouse_effects() + +# Effect management +grid.clear_mouse_effects() +grid.add_persistent_effect(position=(100, 100), effect_type=MouseInteractionType.RIPPLE) +``` + +--- + +## 📊 **Success Metrics** + +### **Performance Targets:** +- [ ] 60 FPS with 64x64 grid and mouse interactions +- [ ] 30 FPS with 128x128 grid and mouse interactions +- [ ] Mouse response latency < 16ms +- [ ] Memory usage increase < 50MB for mouse features + +### **Feature Completeness:** +- [ ] All 7 mouse interaction types working +- [ ] All 4 mouse modes functional +- [ ] Visual feedback system complete +- [ ] Audio-mouse integration working +- [ ] Comprehensive test coverage (>90%) + +### **User Experience:** +- [ ] Intuitive mouse controls +- [ ] Smooth visual transitions +- [ ] Responsive interaction feedback +- [ ] Clear visual indicators +- [ ] Stable performance under load + +--- + +## 📝 **Development Notes** + +### **Design Principles:** +1. **Modularity**: Mouse interactions should be easily disabled/enabled +2. **Performance**: Mouse calculations should not impact base performance +3. **Extensibility**: Easy to add new interaction types +4. **Integration**: Seamless combination with existing features +5. **User Experience**: Intuitive and responsive interactions + +### **Technical Considerations:** +- Use spatial partitioning for large grids +- Cache mouse force calculations when possible +- Smooth interpolation for mouse movements +- Efficient cleanup of expired effects +- Thread-safe mouse state updates + +### **Testing Philosophy:** +- Test each component in isolation first +- Manual testing after each major milestone +- Performance regression testing +- Edge case validation +- User experience validation + +--- + +*This document will be updated as development progresses. Each completed task should be checked off and any design changes should be documented.* + +--- + +**Next Action**: Begin Step 2.1 - Distortion Engine Enhancement \ No newline at end of file