diff --git a/README.md b/README.md index cf558dc..cdfcd0b 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,34 @@ # 🎹 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 multiple geometric shapes (squares, circles, triangles, hexagons, pentagons, stars, diamonds), with optional audio-reactive capabilities for music visualization. +**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 multiple geometric shapes (squares, circles, triangles, hexagons, pentagons, stars, diamonds) with variable grid densities and comprehensive scene management capabilities -[![Watch the demo on YouTube](https://img.youtube.com/vi/GWTRef1pFJo/0.jpg)](https://www.youtube.com/watch?v=GWTRef1pFJo) +[![Watch the demo on YouTube](https://img.youtube.com/vi/qlVvBPMil0Q/0.jpg)](https://www.youtube.com/watch?v=qlVvBPMil0Q) ## 🎯 Project Overview ### What It Does The project creates animated grids of geometric shapes where each shape can be: -- **Multiple shape types** including squares, circles, triangles, hexagons, pentagons, stars, and diamonds -- **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 +- **Multiple shape types** including squares, circles, triangles, hexagons, pentagons, stars, diamonds, ring and fractal Koch snowflakes +- **Geometrically distorted** using 20 mathematical functions (sine waves, Perlin noise, circular patterns, spiral warps, lens effects, moirĂ© patterns, etc.) +- **Dynamically colored** using 20 schemes (rainbow, gradient, neon, cyberpunk, thermal, vaporwave, etc.) +- **Dynamically resized** with variable grid densities from 8×8 to 256×256 cells +- **Interactively controlled** through comprehensive keyboard shortcuts and real-time parameter adjustment - **Mixed or uniform** shape distribution across the grid +- **Saved and loaded** as complete scene configurations with all parameters preserved +- **Recorded as GIFs** for sharing animated sequences ### 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 geometric shapes with base positions -2. **Shape System**: Supports 7 different shape types with unified rendering architecture +1. **Grid Generation**: Creates a regular NxN grid of geometric shapes with base positions and dynamic density control +2. **Shape System**: Supports 8 different shape types with unified rendering architecture 3. **Distortion Engine**: Applies mathematical transformations to deform shape positions and orientations -4. **Color System**: Generates dynamic colors based on position, time, and audio input -5. **Audio Analysis**: Real-time FFT analysis of microphone input to extract bass, mids, highs, and beat detection -6. **Rendering**: Real-time pygame-based visualization with 60fps target +4. **Color System**: Generates dynamic colors based on position, time, and color animation input +5. **Scene Management**: Complete parameter serialization/deserialization with YAML persistence +6. **Media Export**: Real-time GIF recording and PNG screenshot capabilities +7. **Status Monitoring**: Live parameter display and interactive help system +8. **Rendering**: Real-time pygame-based visualization with 60fps target ### Mathematical Foundation @@ -37,22 +41,31 @@ The distortions are based on several mathematical approaches: - **Flow Distortions**: Curl-noise vector fields for organic movement - **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 +### How does a distorsion function work ? + +**Global Principle**: A distortion function is a mathematical transformation that takes static grid positions and creates smooth, organic movement by calculating new positions and rotations for each element over time. + +**Input Parameters**: +- `base_pos` - Original static position (x, y) of a grid element +- `params` - Unique parameters per cell (phase offsets, frequencies) for variety +- `cell_size` - Grid cell size for appropriate movement scaling +- `distortion_strength` - Global intensity multiplier (0.0 = no effect, 1.0 = full effect) +- `time` - Current animation time (key ingredient for smooth motion) +- `canvas_size` - Canvas dimensions for center-based effects + +**Output**: `(new_x, new_y, rotation)` - The displaced position and rotation angle where the element should be drawn. + +**Animation Mechanics**: The system runs at **60 FPS**, meaning every square is recalculated and redrawn 60 times per second. Small time increments combined with continuous mathematical functions (sine, cosine, etc.) create the illusion of fluid movement. Each frame, the `time` parameter increases slightly, producing smooth positional changes that your eye perceives as organic motion rather than discrete jumps. + ## đŸ—ïž Project Structure ``` distorsion_movement/ ├── __init__.py # Package entry point & public API -├── deformed_grid.py # Main DeformedGrid class +├── deformed_grid.py # Main DeformedGrid class with scene management ├── enums.py # Type definitions (DistortionType, ColorScheme, ShapeType) -├── shapes.py # Shape rendering system (7 shape types) -├── audio_analyzer.py # Real-time audio analysis & FFT processing +├── shapes.py # Shape rendering system ├── colors.py # Color generation algorithms ├── distortions.py # Geometric distortion algorithms ├── demos.py # Demo functions & usage examples @@ -61,7 +74,9 @@ distorsion_movement/ │ ├── test_enums.py # Enum validation tests │ ├── test_deformed_grid.py # Grid functionality tests │ └── ... # Other test modules -└── README.md # This file +└── images/ # Generated PNG screenshots +└── saved_params/ # Scene configuration YAML files +└── gifs/ # Generated GIF animations ``` ### Module Responsibilities @@ -69,39 +84,34 @@ distorsion_movement/ #### 🎹 **`deformed_grid.py`** - Core Engine - Main `DeformedGrid` class that orchestrates everything - Pygame rendering loop and event handling -- Grid generation and position calculations +- Grid generation and position calculations with dynamic density control - Shape type management and rendering coordination - Animation timing and state management - Fullscreen/windowed mode switching -- Interactive controls (keyboard shortcuts, shape cycling) +- Interactive controls (keyboard shortcuts, shape cycling, parameter adjustment) +- Scene management (save/load YAML configurations) +- Status display and interactive help system +- GIF recording and media export +- Real-time grid density adjustment (8×8 to 256×256) #### đŸ”· **`shapes.py`** - Shape Rendering System -- Unified rendering architecture for 7 geometric shapes +- Unified rendering architecture for 12 geometric shapes - Rotation and scaling support for all shape types - Mathematically precise shape generation (triangles, hexagons, stars, etc.) - Consistent interface with fallback error handling - Optimized drawing functions using pygame primitives -#### đŸŽ” **`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.) +- 20 different color schemes (monochrome, gradient, rainbow, neon, cyberpunk, vaporwave, etc.) - Position-based color calculations - Time-based color animations -- Audio-reactive color modulation - HSV/RGB color space conversions #### 🌀 **`distortions.py`** - Geometric Engine -- 7 distortion algorithms (random, sine, Perlin, circular, swirl, ripple, flow) +- 20 distortion algorithms (random, sine, Perlin, circular, swirl, ripple, flow, tornado, lens, moirĂ©, etc.) - Mathematical transformation functions - Parameter generation for each square - Time-based animation calculations -- Audio-reactive distortion intensity #### 🎼 **`demos.py`** - Usage Examples - Pre-configured demonstration functions @@ -111,9 +121,9 @@ distorsion_movement/ - Command-line interface with multiple demo options #### 📝 **`enums.py`** - Type Safety -- `DistortionType`: RANDOM, SINE, PERLIN, CIRCULAR, SWIRL, RIPPLE, FLOW -- `ColorScheme`: MONOCHROME, GRADIENT, RAINBOW, COMPLEMENTARY, TEMPERATURE, PASTEL, NEON, OCEAN, FIRE, FOREST -- `ShapeType`: SQUARE, CIRCLE, TRIANGLE, HEXAGON, PENTAGON, STAR, DIAMOND +- `DistortionType`: RANDOM, SINE, PERLIN, CIRCULAR, SWIRL, RIPPLE, FLOW, PULSE, CHECKERBOARD, CHECKERBOARD_DIAGONAL, TORNADO, SPIRAL, SHEAR, LENS, SPIRAL_WAVE, NOISE_ROTATION, CURL_WARP, FRACTAL_NOISE, MOIRE, KALEIDOSCOPE_TWIST +- `ColorScheme`: MONOCHROME, BLACK_WHITE_RADIAL, BLACK_WHITE_ALTERNATING, GRADIENT, RAINBOW, COMPLEMENTARY, PASTEL, NEON, ANALOGOUS, CYBERPUNK, AURORA_BOREALIS, INFRARED_THERMAL, DUOTONE_ACCENT, DESERT, METALLICS, REGGAE, SUNSET, POP_ART, VAPORWAVE, CANDY_SHOP +- `ShapeType`: SQUARE, CIRCLE, TRIANGLE, HEXAGON, PENTAGON, STAR, DIAMOND, KOCH_SNOWFLAKE, RING, YIN_YANG, LEAF, ELLIPSIS ## 🚀 Quick Start @@ -138,63 +148,57 @@ grid = DeformedGrid( color_scheme=ColorScheme.NEON.value, # Neon colors shape_type=ShapeType.STAR.value, # Star shapes mixed_shapes=False, # Single shape type - audio_reactive=True, # Enable audio reactivity color_animation=True # Animate colors ) grid.run_interactive() ``` -### Shape Configuration Examples -```python -# Mixed shapes grid (variety of shapes) -mixed_grid = DeformedGrid( - dimension=80, - shape_type="hexagon", # Base shape type - mixed_shapes=True, # Enable shape variety - color_scheme="rainbow" -) +## đŸŽ›ïž Interactive Controls -# Single shape type (all circles) -circle_grid = DeformedGrid( - dimension=60, - shape_type="circle", # Only circles - mixed_shapes=False, # Uniform shapes - distortion_fn="circular" -) -``` +### Navigation & Interface +| Key | Action | +|-----|--------| +| `ESC` | Exit application | +| `F` | Toggle fullscreen/windowed mode | +| `I` or `TAB` | Show/hide interactive help menu | +| `D` | Show/hide status display | -### Available Demo Functions -```python -from distorsion_movement.demos import ( - quick_demo, fullscreen_demo, audio_reactive_demo, - star_demo, hexagon_demo, triangle_demo, shapes_showcase_demo -) +### Distortion & Animation +| Key | Action | +|-----|--------| +| `SPACE` / `Shift+SPACE` | Next/previous distortion type | +| `+/-` | Increase/decrease distortion intensity | +| `R` | Regenerate random parameters | -quick_demo() # Basic windowed demo -fullscreen_demo() # Mixed shapes fullscreen experience -star_demo() # Star shapes only -hexagon_demo() # Hexagon patterns -triangle_demo() # Triangle formations -shapes_showcase_demo() # Mixed shapes showcase -audio_reactive_demo() # Music visualization with hexagons -``` +### Grid & Density +| Key | Action | +|-----|--------| +| `T` then `+/-` | **NEW**: Adjust grid density (number of cells) | -## đŸŽ›ïž Interactive Controls +### Colors +| Key | Action | +|-----|--------| +| `C` / `Shift+C` | Next/previous color scheme | +| `A` | Toggle color animation on/off | +### Shapes | Key | Action | |-----|--------| -| `F` | Toggle fullscreen/windowed mode | -| `M` | Toggle audio reactivity on/off | -| `C` | Cycle through color schemes | -| `SPACE` | Cycle through distortion types | -| `H` | **NEW**: Cycle through shape types | -| `Shift+H` | **NEW**: Toggle mixed/single shape mode | -| `+/-` | Increase/decrease distortion intensity | -| `A` | Toggle color animation | -| `R` | Reset and regenerate all parameters | -| `S` | Save current image as PNG | -| `ESC` | Exit application | +| `H` / `Shift+H` | Next/previous shape type | +| `Ctrl+H` | Toggle mixed shapes mode | + +### Media & Saving +| Key | Action | +|-----|--------| +| `S` | **NEW**: Save current image (PNG + parameters YAML) | +| `G` | **NEW**: Start/stop GIF recording | + +### Scene Management +| Key | Action | +|-----|--------| +| `L` / `Shift+L` | **NEW**: Load next/previous saved scene | +| `P` | **NEW**: Refresh saved scenes list | ## 🎹 Available Visual Modes @@ -206,6 +210,11 @@ audio_reactive_demo() # Music visualization with hexagons - **Pentagon**: Five-sided polygonal forms - **Star**: Five-pointed star shapes - **Diamond**: Rotated square formations +- **Koch Snowflake**: Fractal snowflake patterns with recursive geometry +- **Ring**: Hollow circle shapes with variable thickness +- **Yin Yang**: Yin-Yang symbol with dynamic color transitions +- **Leaf**: Two mirrored arcs meeting at sharp tips +- **Ellipsis**: A stretched circle with rotation ### Shape Modes - **Single Shape**: All cells use the same shape type (uniform grid) @@ -219,50 +228,56 @@ audio_reactive_demo() # Music visualization with hexagons - **Swirl**: Rotational vortex effects with periodic waves - **Ripple**: Concentric wave patterns with tangential movement - **Flow**: Smooth curl-noise vector fields for organic flow +- **Pulse**: Rhythmic radial breathing with wave-like pulsations +- **Checkerboard**: Alternating directional movement in grid pattern +- **Checkerboard Diagonal**: Diagonal tug-of-war between neighboring cells +- **Tornado**: Swirling vortex with increased rotation near center +- **Spiral**: Galaxy-like spiral motion with radius oscillation +- **Shear**: Horizontal/vertical skewing with wave propagation +- **Lens**: Dynamic magnification effect with moving focus point +- **Spiral Wave**: Combined circular ripples with rotational motion +- **Noise Rotation**: Positions stable, rotation driven by smooth noise field +- **Curl Warp**: Divergence-free swirling vector fields +- **Fractal Noise**: Multi-octave organic terrain-like distortions +- **MoirĂ©**: Interference patterns from overlapping wave frequencies +- **Kaleidoscope Twist**: Radial symmetry with mirrored sectors and melting ### Color Schemes - **Monochrome**: Single color variations -- **Gradient**: Smooth color transitions +- **Black White Radial**: Center-to-edge black and white distribution +- **Black White Alternating**: Classic checkerboard pattern +- **Gradient**: Smooth diagonal 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 +- **Analogous**: Harmonious neighboring hues with subtle variations +- **Cyberpunk**: Neon magenta and cyan with deep purple accents +- **Aurora Borealis**: Flowing teal, green, and purple like northern lights +- **Infrared Thermal**: Classic thermal imaging progression (blue→cyan→green→yellow→orange→red→white) +- **Duotone Accent**: Two main colors with rare bright yellow accent pops +- **Desert**: Sandy beige, warm orange, and muted brown tones +- **Metallics**: Smooth blends between gold, silver, and bronze +- **Reggae**: Green, yellow, and red in radial distribution +- **Sunset**: Warm gradient from yellow through orange and pink to purple +- **Pop Art**: Bright primary colors with bold black/white outlines +- **Vaporwave**: Soft pastel neons in lavender, cyan, peach, and pink +- **Candy Shop**: Sweet bubblegum pink, mint green, and lemon yellow ## 🔧 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 +pip install -r requirements.txt ``` -## đŸŽ” Audio-Reactive Features +Core dependencies include: +- `pygame` - Real-time graphics and event handling +- `numpy` - Numerical computations and array processing +- `imageio` - GIF animation export +- `PyYAML` - Scene parameter serialization +- `pytest` - Testing framework -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 @@ -281,7 +296,6 @@ python -m pytest distorsion_movement/tests/test_deformed_grid.py -v # Grid funct # - Grid generation and management # - Color schemes and animations # - Distortion algorithms -# - Audio analysis components # - Integration testing ``` @@ -289,60 +303,56 @@ python -m pytest distorsion_movement/tests/test_deformed_grid.py -v # Grid funct The project has an extensive roadmap for future enhancements (see `TODO.md`): -### Phase 1 (High Priority) -- Mouse interaction (attraction/repulsion effects) -- ✅ **COMPLETED**: Multiple shape types (circles, triangles, hexagons, stars, etc.) -- Motion blur and glow effects -- GIF/MP4 export capabilities -- Preset scene system -- Shape morphing and transformation animations - -### 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 shapes -✅ **Multiple Shape Types**: 7 geometric shapes (squares, circles, triangles, hexagons, pentagons, stars, diamonds) +✅ **Multiple Shape Types**: 12 geometric shapes (squares, circles, triangles, hexagons, pentagons, stars, diamonds, Koch snowflakes, ring, yin yang, leaf, ellipsis) ✅ **Flexible Shape Modes**: Single shape or mixed shape grids +✅ **Dynamic Grid Density**: Live adjustment of cell count and size (8×8 to 256×256) +✅ **Comprehensive Status Display**: Real-time parameter monitoring overlay +✅ **Scene Management**: Save/load visual configurations with YAML parameters +✅ **Media Export**: PNG screenshots and GIF animation recording +✅ **Interactive Help System**: Built-in keyboard shortcut reference ✅ **Modular Architecture**: Clean separation of concerns, easily extensible -✅ **Audio Reactivity**: Professional-grade music visualization ✅ **Interactive Controls**: Live parameter adjustment and shape cycling -✅ **Rich Visual Combinations**: 7 shapes × 7 distortions × 10 colors = 490 combinations +✅ **Rich Visual Combinations**: 12 shapes × 20 distortions × 20 colors × 10 variable grid sizes = 48,000+ combinations ✅ **Cross-platform**: Works on Windows, macOS, Linux ✅ **Comprehensive Testing**: Full unit test coverage -✅ **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 +✅ **Fullscreen Support**: Immersive viewing experience ## đŸ€ Contributing The project is designed for easy extension: + +### Adding New Distortions - Add new distortion algorithms in `distortions.py` -- Create new shape types in `shapes.py` -- Create new color schemes in `colors.py` -- Enhance audio analysis in `audio_analyzer.py` -- Build new demo configurations in `demos.py` -- Extend shape interactions and morphing capabilities +- Add the new distortion type to `DistortionType` enum in `enums.py` + +### Adding New Shapes +To add a new shape type, you need to: +1. **Add the shape to the enum**: Add your new shape to `ShapeType` enum in `enums.py` +2. **Create the shape file**: Create a new file `shapes/your_shape.py` with: + - A class inheriting from `BaseShape` + - A static `draw()` method with signature: `(surface, x, y, rotation, size, color, **kwargs)` + - Import the base class: `from .base_shape import BaseShape` +3. **Update the package**: Add your shape to `shapes/__init__.py`: + - Import: `from .your_shape import YourShape` + - Add to `SHAPE_REGISTRY`: `"your_shape": YourShape.draw` + - Add to `__all__` list: `'YourShape'` + +### Adding New Color Schemes +To add a new color scheme, you need to: +1. **Add the scheme to the enum**: Add your new color scheme to `ColorScheme` enum in `enums.py` +2. **Create the color scheme file**: Create a new file `colors/your_scheme.py` with: + - A class inheriting from `BaseColor` + - A static `get_color_for_position()` method with signature: `(square_color, x_norm, y_norm, distance_to_center, index, dimension) -> Tuple[int, int, int]` + - Import the base class: `from .base_color import BaseColor` + - Use helper methods like `_clamp_rgb()`, `_blend_colors()`, `_hsv_to_rgb_clamped()` for color operations +3. **Update the package**: Add your color scheme to `colors/__init__.py`: + - Import: `from .your_scheme import YourScheme` + - Add to `COLOR_SCHEME_REGISTRY`: `"your_scheme": YourScheme.get_color_for_position` + - Add to `__all__` list: `'YourScheme'` --- -**Distorsion Movement** transforms mathematical concepts into living, breathing art that responds to sound and user interaction. With support for multiple geometric shapes, flexible rendering modes, and comprehensive interactive controls, it's both a technical showcase of real-time graphics programming and a creative tool for generating endless visual experiences. +**Distorsion Movement** transforms mathematical concepts into living, breathing art that responds to user interaction. With support for multiple geometric shapes, flexible rendering modes, and comprehensive interactive controls, it's both a technical showcase of real-time graphics programming and a creative tool for generating endless visual experiences. diff --git a/TODO.md b/TODO.md index 6cde501..a78e0a0 100644 --- a/TODO.md +++ b/TODO.md @@ -1,202 +1,15 @@ -# 🎹 Deformed Grid - Epic Improvements Roadmap - -## đŸŽ” **Audio-Reactive Features** - Not working that well -- **Real-time audio analysis** using FFT and pyaudio -- **Frequency separation**: Bass, mids, highs control different visual aspects -- **Beat detection** with synchronized flash effects -- **Audio → Visual mapping**: - - đŸ„ Bass (20-250Hz) → Distortion intensity - - 🎾 Mids (250Hz-4kHz) → Color hue rotation - - ✹ Highs (4kHz+) → Brightness boosts - - đŸ’„ Beat detection → White flash effects - - 📱 Volume → Animation speed -- **Interactive controls**: Press 'M' to toggle audio reactivity - -## 🎹 **Advanced Visual Effects** - -### **Particle Systems** -- **Trailing sparks** behind moving squares -- **Glowing halos** around squares based on audio intensity -- **Energy fields** that connect nearby squares -- **Particle explosions** on beat detection -- **Floating particles** that react to distortions - -### **Motion Blur & Glow Effects** -- **Motion blur trails** as squares move -- **Neon glow effects** that actually bleed light -- **Bloom/HDR effects** for bright colors -- **Ghost trails** showing previous positions -- **Chromatic aberration** for psychedelic effects - -### **Post-Processing Filters** -- **Real-time blur/sharpen** filters -- **Color correction** and saturation boost -- **Vintage film effects** (grain, vignette) -- **Kaleidoscope/mirror** effects -- **Pixelation/retro** filters - -## đŸ–±ïž **Interactive Features** - -### **Mouse Interaction** -- **Attraction/repulsion** - squares follow or flee from cursor -- **Paint mode** - click and drag to paint colors in real-time -- **Gravity wells** - create distortion fields with mouse clicks -- **Magnetic fields** - squares align like iron filings around cursor -- **Ripple effects** - wave propagation from click points - -### **Touch/Multi-touch Support** -- **Tablet compatibility** for touch devices -- **Pinch-to-zoom** for closer inspection -- **Multi-finger gestures** for complex interactions -- **Pressure sensitivity** for drawing effects - -## đŸ”ș **Shape Variety & Morphing** - -### **Multiple Shapes** -- **Circles, triangles, hexagons, stars** -- **Custom polygons** with variable sides -- **Organic shapes** using bezier curves -- **3D-looking shapes** with perspective -- **Text characters** as shapes - -### **Shape Morphing** -- **Squares → circles** transformation -- **Size variation** for depth perception -- **Shape interpolation** between different forms -- **Breathing/pulsing** shape animations -- **Fractal shapes** with recursive patterns - -## đŸŒȘ **Advanced Distortion Types** - -### **New Distortion Algorithms** -- **Spiral/vortex** effects with rotation fields -- **Fluid dynamics** simulation -- **Magnetic field** distortions -- **Gravitational** lensing effects -- **Turbulence** and noise-based distortions - -### **Compound Distortions** -- **Multiple distortions** applied simultaneously -- **Distortion layering** with different intensities -- **Time-based distortion** sequences -- **Audio-reactive distortion** switching - -## 🎼 **Gaming & Interactive Elements** - -### **Preset Scenes** -- **Curated combinations** of colors, distortions, and effects -- **Genre-specific presets** (Electronic, Classical, Rock, etc.) -- **Mood-based themes** (Chill, Energetic, Psychedelic) -- **Time-of-day** adaptive themes - -### **Randomization & AI** -- **"Surprise Me"** button for random combinations -- **Genetic algorithms** for evolving patterns -- **AI-generated** color palettes -- **Smart recommendations** based on music genre - -## đŸ’Ÿ **Export & Sharing Features** - -### **Media Export** -- **GIF/MP4 export** of animations -- **High-resolution rendering** (4K, 8K) -- **Frame-by-frame** export for video editing -- **Live streaming** integration -- **Screenshot burst mode** - -### **Data Management** -- **Save/load presets** as JSON files -- **Color palette export** to Adobe/Figma formats -- **Batch generation** of hundreds of variations -- **Session recording** and playback -- **Cloud sync** for settings - -## 🚀 **Performance & Technical** - -### **Optimization** -- **GPU acceleration** using OpenGL/Metal -- **Multi-threading** for audio processing -- **Level-of-detail** rendering for performance -- **Memory optimization** for large grids -- **60fps guarantee** even with thousands of squares - -### **Platform Support** -- **Web version** using WebGL -- **Mobile apps** (iOS/Android) -- **VR/AR support** for immersive experiences -- **Hardware controller** support (MIDI, game controllers) - -## 🎯 **Special Effects** - -### **Fractal & Mathematical Patterns** -- **Mandelbrot/Julia sets** integration -- **L-systems** for organic growth patterns -- **Cellular automata** (Conway's Game of Life) -- **Strange attractors** (Lorenz, Rössler) -- **Fibonacci spirals** and golden ratio patterns - -### **Physics Simulation** -- **Collision detection** between squares -- **Spring systems** connecting squares -- **Fluid dynamics** for liquid-like behavior -- **Gravity simulation** with realistic physics -- **Electromagnetic** field visualization - -## 🌈 **Extended Color Systems** - -### **Advanced Color Schemes** -- **Perceptually uniform** color spaces (LAB, LUV) -- **Color harmony** rules (triadic, complementary, etc.) -- **Seasonal palettes** that change over time -- **Emotion-based** color mapping -- **Cultural color** themes from around the world - -### **Dynamic Color Effects** -- **Color bleeding** between adjacent squares -- **Chromatic aberration** for retro effects -- **Color temperature** shifts -- **Saturation breathing** effects -- **Hue cycling** with musical harmony - -## đŸŽȘ **Experimental Features** - -### **Generative Art Integration** -- **Style transfer** using neural networks -- **Procedural textures** on squares -- **Algorithmic composition** of patterns -- **Evolutionary art** that improves over time -- **Collaborative evolution** with user feedback - -### **Data Visualization** -- **Real-time data** integration (stock prices, weather, etc.) -- **Social media sentiment** visualization -- **Network topology** representation -- **Scientific data** visualization modes -- **Biometric integration** (heart rate, brain waves) - ---- - ## Bugs - [X] shift + h is not working - [X] It seems I cannot go back to monochrome - [X] Fix or remove useless tests - [X] Fix the cell size => Should also resize the grid +- [X] Distorsion improvement : Fix the lens distortion +- [ ] Full screen buggy, grid not big enough and loading scenes does not work well +- [ ] Distorsion improvement :Fix the kaleidoscope twist => It's not working well in full screen +- [ ] Shapes improvements : Koch snowflake => Looks awsome but very laggy +- [ ] Colors improvements : Aurora borealis could maybe be improved, more red -- Distorsions improvements : - - [ ] Fix the lens distortion - => Always the same size of lens based on the cell size - => Solution : the size of the lens should be based on the cell/grid size - - [ ] Fix the kaleidoscope twist => It's not working well in full screen - -- Shapes improvements : - - [ ] Koch snowflake => Looks awsome but very laggy - -- Colors improvements : - - [ ] Aurora borealis could maybe be improved, more red - -- [ ] Audio feature is not working well - -## 🏆 **Implementation Priority** +## Short term roadmap - [X] Global README to explain the project - [X] Write a few tests @@ -207,26 +20,51 @@ - [X] Add new distortion types (swirl, ripple, flow) - [X] Add options to play with the cell size - [X] Add the name of the distortion, the intensity, and the number of cells on the screen +- [X] Add an option to export the parameters of the current scene (just export the params.json file) and find a way to load it automatically when the app is launched. Maybe reuse the save image feature, and save a json file with the parameters. Maybe add an option to iterate through all saved scenes +- [X] Get rid of the audio features, it's not working well +- [X] Clean up useless stuff in TODO.md + +- [X] Add a keyboard shortcut to do previous distorsion/shape/color scheme +- [X] The menu is too big, on one column, maybe 2 columns now ? +- [X] Clean up useless stuff in README.md (audio etc...) + +- Add in README: + - [X] the new distorsions (from pulse, pulse not included) + - [X] the new shapes (from koch snowflake) + - [X] the new colors (from analogous) + - [X] the size guide (and mention it in the number of combinations possible) + - [X] the new indicators screen + - [X] the saved/load scenes feature + - [X] the previous/next scene feature + - [X] how a distorsion function works, what parameters and what it basically does (compute next position and rotations based on ...) + +- [X] Find cool combinations to showcase and save them +- [X] Youtube video + Update README.md with the video link + +- [ ] Refactor the shapes, colors, and distorsions to be more modular and easier to add new ones. (one file for each type of distorsion, one file for each shape, one file for each color scheme) + Add a guide in the README.md to explain how to add new shapes, colors, and distorsions + - [X] shapes + - [X] colors + - [ ] distorsions + - [ ] update the modules explanations + +- [ ] Filter out ugly saved combinations & redo better youtube video demoing different shapes & do one demoing all the features in the menu +- [ ] Prepare LinkedIn post to share the repo and the video + +- [ ] Save as mp4 feature - [ ] Add new shape types - [ ] Add new color schemes (inspire from pastel color in archive) - [ ] Add new distortion types (take a look at the new_distortions_again.md file for more ideas) -- [ ] Add an option to export the parameters of the current scene (just export the params.json file) and find a way to load it automatically when the app is launched - -- [ ] Add in README: - - the new distorsions (from pulse, pulse not included) - - the new shapes (from koch snowflake) - - the new colors (from analogous) - - the size guide (and mention it in the number of combinations possible) - - the status display - - how a distorsion function works, what parameters and what it basically does (compute next position and rotations based on ...) - - the indicators screen +## Mid term roadmap +- [ ] Host on web ? pygbag https://github.com/pygame-web/pygbag + - in a pull request, not working well + - maybe need to migrate to webgl and use f*ckin' js ? - [ ] Mouse interaction (attraction/repulsion) - in a pull request, not working well - [ ] Audio reactive features - - in the code, but really buggy + - in a pull request, need to be redone from scratch - [ ] Motion blur effects - [ ] Preset scene system -- [ ] Particle systems +- [ ] Particle systems \ No newline at end of file diff --git a/Taskfile.yaml b/Taskfile.yaml index d4e6c4b..fc624fc 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -20,13 +20,6 @@ tasks: - echo "🎹 Starting fullscreen demo..." - source venv/bin/activate && python -c "from distorsion_movement import fullscreen_demo; fullscreen_demo()" - demo:audio: - desc: "Run audio-reactive demo" - cmds: - - echo "đŸŽ” Starting audio-reactive demo..." - - echo "🎧 Make sure your microphone is enabled!" - - source venv/bin/activate && python -c "from distorsion_movement import audio_reactive_demo; audio_reactive_demo()" - test: desc: "Run all tests with pytest" cmds: @@ -86,13 +79,9 @@ tasks: desc: "Install required dependencies" cmd: source venv/bin/activate && pip install -r requirements.txt - install:audio: - desc: "Install audio dependencies (optional)" - cmd: source venv/bin/activate && pip install pyaudio scipy - install:all: - desc: "Install all dependencies including audio" - deps: [install, install:audio] + desc: "Install all dependencies" + deps: [install] help: desc: "Show available commands" diff --git a/distorsion_movement/__init__.py b/distorsion_movement/__init__.py index 9ae1b5d..a3a3300 100644 --- a/distorsion_movement/__init__.py +++ b/distorsion_movement/__init__.py @@ -3,25 +3,21 @@ Ce package contient tous les modules nĂ©cessaires pour crĂ©er et afficher des grilles de carrĂ©s dĂ©formĂ©s gĂ©omĂ©triquement avec diffĂ©rents effets visuels -et audio-rĂ©actifs. """ from distorsion_movement.deformed_grid import DeformedGrid from distorsion_movement.enums import DistortionType, ColorScheme -from distorsion_movement.audio_analyzer import AudioAnalyzer from distorsion_movement.colors import ColorGenerator from distorsion_movement.distortions import DistortionEngine -from distorsion_movement.demos import create_deformed_grid, quick_demo, fullscreen_demo, audio_reactive_demo +from distorsion_movement.demos import create_deformed_grid, quick_demo, fullscreen_demo __all__ = [ 'DeformedGrid', 'DistortionType', 'ColorScheme', - 'AudioAnalyzer', 'ColorGenerator', 'DistortionEngine', 'create_deformed_grid', 'quick_demo', 'fullscreen_demo', - 'audio_reactive_demo' ] \ No newline at end of file diff --git a/distorsion_movement/audio_analyzer.py b/distorsion_movement/audio_analyzer.py deleted file mode 100644 index 7c5eb0c..0000000 --- a/distorsion_movement/audio_analyzer.py +++ /dev/null @@ -1,192 +0,0 @@ -""" -Analyseur audio en temps rĂ©el pour la rĂ©activitĂ© musicale. -""" - -import numpy as np -import threading -import queue - -# Audio processing imports (optional - will gracefully degrade if not available) -try: - import pyaudio - import scipy.signal - AUDIO_AVAILABLE = True -except ImportError: - AUDIO_AVAILABLE = False - print("Audio libraries not available. Install pyaudio and scipy for music reactivity:") - print("pip install pyaudio scipy") - - -class AudioAnalyzer: - """ - Analyseur audio en temps rĂ©el pour la rĂ©activitĂ© musicale. - - Capture l'audio du microphone et extrait les caractĂ©ristiques frĂ©quentielles - pour contrĂŽler les paramĂštres visuels. - """ - - def __init__(self, sample_rate: int = 44100, chunk_size: int = 1024): - """ - Initialise l'analyseur audio. - - Args: - sample_rate: FrĂ©quence d'Ă©chantillonnage audio - chunk_size: Taille des blocs audio Ă  analyser - """ - self.sample_rate = sample_rate - self.chunk_size = chunk_size - self.audio_queue = queue.Queue() - self.is_running = False - self.audio_thread = None - - # ParamĂštres d'analyse - self.bass_range = (20, 250) # Hz - self.mid_range = (250, 4000) # Hz - self.high_range = (4000, 20000) # Hz - - # Variables de sortie (thread-safe) - self.bass_level = 0.0 - self.mid_level = 0.0 - self.high_level = 0.0 - self.overall_volume = 0.0 - self.beat_detected = False - - # Historique pour la dĂ©tection de beats - self.volume_history = [] - self.beat_threshold = 1.3 - self.beat_cooldown = 0 - - # Lissage des valeurs - self.smoothing_factor = 0.8 - - # PyAudio setup - if AUDIO_AVAILABLE: - self.pa = pyaudio.PyAudio() - self.stream = None - else: - self.pa = None - self.stream = None - - def start_audio_capture(self): - """DĂ©marre la capture audio en arriĂšre-plan""" - if not AUDIO_AVAILABLE: - print("Audio non disponible - mode silencieux") - return False - - try: - self.stream = self.pa.open( - format=pyaudio.paFloat32, - channels=1, - rate=self.sample_rate, - input=True, - frames_per_buffer=self.chunk_size, - stream_callback=self._audio_callback - ) - - self.is_running = True - self.audio_thread = threading.Thread(target=self._process_audio, daemon=True) - self.audio_thread.start() - - print("đŸŽ” Capture audio dĂ©marrĂ©e - Votre art rĂ©agit maintenant Ă  la musique!") - return True - - except Exception as e: - print(f"Erreur audio: {e}") - return False - - def stop_audio_capture(self): - """ArrĂȘte la capture audio""" - self.is_running = False - - if self.stream: - self.stream.stop_stream() - self.stream.close() - - if self.pa: - self.pa.terminate() - - def _audio_callback(self, in_data, frame_count, time_info, status): - """Callback pour recevoir les donnĂ©es audio""" - try: - audio_data = np.frombuffer(in_data, dtype=np.float32) - self.audio_queue.put(audio_data, block=False) - except queue.Full: - pass # Skip if queue is full - return (None, pyaudio.paContinue) - - def _process_audio(self): - """Thread principal de traitement audio""" - while self.is_running: - try: - # RĂ©cupĂ©rer les donnĂ©es audio - audio_data = self.audio_queue.get(timeout=0.1) - - # Calculer la FFT - fft = np.fft.rfft(audio_data) - magnitude = np.abs(fft) - - # CrĂ©er l'Ă©chelle de frĂ©quences - freqs = np.fft.rfftfreq(len(audio_data), 1/self.sample_rate) - - # Extraire les niveaux par bande de frĂ©quence - bass_indices = np.where((freqs >= self.bass_range[0]) & (freqs <= self.bass_range[1])) - mid_indices = np.where((freqs >= self.mid_range[0]) & (freqs <= self.mid_range[1])) - high_indices = np.where((freqs >= self.high_range[0]) & (freqs <= self.high_range[1])) - - # Calculer les niveaux moyens (avec lissage) - bass_new = np.mean(magnitude[bass_indices]) if len(bass_indices[0]) > 0 else 0 - mid_new = np.mean(magnitude[mid_indices]) if len(mid_indices[0]) > 0 else 0 - high_new = np.mean(magnitude[high_indices]) if len(high_indices[0]) > 0 else 0 - volume_new = np.mean(magnitude) - - # Appliquer le lissage - self.bass_level = self.bass_level * self.smoothing_factor + bass_new * (1 - self.smoothing_factor) - self.mid_level = self.mid_level * self.smoothing_factor + mid_new * (1 - self.smoothing_factor) - self.high_level = self.high_level * self.smoothing_factor + high_new * (1 - self.smoothing_factor) - self.overall_volume = self.overall_volume * self.smoothing_factor + volume_new * (1 - self.smoothing_factor) - - # DĂ©tection de beats - self._detect_beat(volume_new) - - except queue.Empty: - continue - except Exception as e: - print(f"Erreur traitement audio: {e}") - continue - - def _detect_beat(self, current_volume): - """DĂ©tecte les beats dans l'audio""" - self.volume_history.append(current_volume) - - # Garder seulement les 20 derniĂšres valeurs - if len(self.volume_history) > 20: - self.volume_history.pop(0) - - # RĂ©duire le cooldown - if self.beat_cooldown > 0: - self.beat_cooldown -= 1 - - # DĂ©tecter un beat si le volume actuel dĂ©passe significativement la moyenne rĂ©cente - if len(self.volume_history) >= 10 and self.beat_cooldown == 0: - recent_avg = np.mean(self.volume_history[:-5]) # Moyenne des valeurs rĂ©centes - if current_volume > recent_avg * self.beat_threshold: - self.beat_detected = True - self.beat_cooldown = 10 # Cooldown pour Ă©viter les faux positifs - return - - self.beat_detected = False - - def get_audio_features(self) -> dict: - """ - Retourne les caractĂ©ristiques audio actuelles. - - Returns: - Dict avec bass_level, mid_level, high_level, overall_volume, beat_detected - """ - return { - 'bass_level': min(self.bass_level * 100, 1.0), # NormalisĂ© 0-1 - 'mid_level': min(self.mid_level * 50, 1.0), # NormalisĂ© 0-1 - 'high_level': min(self.high_level * 20, 1.0), # NormalisĂ© 0-1 - 'overall_volume': min(self.overall_volume * 30, 1.0), # NormalisĂ© 0-1 - 'beat_detected': self.beat_detected - } \ No newline at end of file diff --git a/distorsion_movement/colors.py b/distorsion_movement/colors.py deleted file mode 100644 index 00ad0a4..0000000 --- a/distorsion_movement/colors.py +++ /dev/null @@ -1,293 +0,0 @@ -""" -GĂ©nĂ©rateurs de couleurs pour les diffĂ©rents schĂ©mas disponibles. -""" - -import math -import colorsys -from typing import Tuple - -from distorsion_movement.enums import ColorScheme - - -class ColorGenerator: - """ - GĂ©nĂ©rateur de couleurs selon diffĂ©rents schĂ©mas. - """ - - @staticmethod - def get_color_for_position(color_scheme: str, square_color: Tuple[int, int, int], - x_norm: float, y_norm: float, - distance_to_center: float, index: int, - dimension: int) -> Tuple[int, int, int]: - """ - GĂ©nĂšre une couleur pour une position donnĂ©e selon le schĂ©ma de couleur actuel. - - Args: - color_scheme: Nom du schĂ©ma de couleur - square_color: Couleur de base pour le schĂ©ma monochrome - x_norm: Position X normalisĂ©e (0.0 Ă  1.0) - y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) - distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) - index: Index du carrĂ© dans la grille - dimension: Dimension de la grille (pour calculs) - - Returns: - Tuple RGB (r, g, b) - """ - if color_scheme == "monochrome": - return square_color - - elif color_scheme == "black_white_radial": - # Noir et blanc radial - distribution basĂ©e sur la distance au centre - # Les formes au centre tendent vers le blanc, celles aux bords vers le noir - if distance_to_center < 0.3: - return (255, 255, 255) # Blanc au centre - elif distance_to_center > 0.7: - return (0, 0, 0) # Noir aux bords - else: - # Zone intermĂ©diaire : alternance selon l'index - return (255, 255, 255) if (index % 2 == 0) else (0, 0, 0) - - elif color_scheme == "black_white_alternating": - # Noir et blanc alternance claire - damier/checkerboard pattern - row = index // dimension - col = index % dimension - # Pattern damier classique - is_white = (row + col) % 2 == 0 - return (255, 255, 255) if is_white else (0, 0, 0) - - elif color_scheme == "gradient": - # Gradient diagonal du coin supĂ©rieur gauche au coin infĂ©rieur droit - t = (x_norm + y_norm) / 2.0 - r = int(50 + t * 205) - g = int(100 + t * 155) - b = int(200 - t * 100) - return (r, g, b) - - elif color_scheme == "rainbow": - # Arc-en-ciel basĂ© sur la position - hue = (x_norm + y_norm * 0.5) % 1.0 - r, g, b = colorsys.hsv_to_rgb(hue, 0.8, 0.9) - return (int(r * 255), int(g * 255), int(b * 255)) - - elif color_scheme == "complementary": - # Couleurs complĂ©mentaires alternĂ©es - if (index + (index // dimension)) % 2 == 0: - return (255, 100, 50) # Orange - else: - return (50, 150, 255) # Bleu - - elif color_scheme == "temperature": - # Couleurs chaudes au centre, froides aux bords - temp = 1.0 - distance_to_center - if temp > 0.7: - # TrĂšs chaud - rouge/jaune - r, g, b = colorsys.hsv_to_rgb(0.1, 0.8, 1.0) - elif temp > 0.4: - # Chaud - orange/rouge - r, g, b = colorsys.hsv_to_rgb(0.05, 0.9, 0.9) - else: - # Froid - bleu/violet - r, g, b = colorsys.hsv_to_rgb(0.6 + temp * 0.2, 0.7, 0.8) - return (int(r * 255), int(g * 255), int(b * 255)) - - elif color_scheme == "pastel": - # Couleurs pastel douces - hue = (x_norm * 0.3 + y_norm * 0.7) % 1.0 - r, g, b = colorsys.hsv_to_rgb(hue, 0.3, 0.9) - return (int(r * 255), int(g * 255), int(b * 255)) - - elif color_scheme == "neon": - # Couleurs nĂ©on vives - hue = (distance_to_center + x_norm * 0.5) % 1.0 - r, g, b = colorsys.hsv_to_rgb(hue, 1.0, 1.0) - return (int(r * 255), int(g * 255), int(b * 255)) - - elif color_scheme == "ocean": - # ThĂšme ocĂ©an - bleus et verts - depth = distance_to_center - if depth < 0.3: - # Eau peu profonde - turquoise - return (64, 224, 208) - elif depth < 0.7: - # Eau moyenne - bleu ocĂ©an - return (0, 119, 190) - else: - # Eau profonde - bleu foncĂ© - return (25, 25, 112) - - elif color_scheme == "fire": - # ThĂšme feu - rouges, oranges, jaunes - intensity = 1.0 - distance_to_center + y_norm * 0.3 - if intensity > 0.8: - return (255, 255, 100) # Jaune chaud - elif intensity > 0.5: - return (255, 140, 0) # Orange - else: - return (220, 20, 60) # Rouge foncĂ© - - elif color_scheme == "forest": - # ThĂšme forĂȘt - verts variĂ©s - green_intensity = 0.3 + distance_to_center * 0.7 + x_norm * 0.2 - if green_intensity > 0.8: - return (144, 238, 144) # Vert clair - elif green_intensity > 0.5: - return (34, 139, 34) # Vert forĂȘt - else: - return (0, 100, 0) # Vert foncĂ© - - elif color_scheme == "analogous": - base_hue = 0.3 # green-ish - hue_shift = (x_norm - 0.5) * 0.3 + (y_norm - 0.5) * 0.3 # ±0.3 spread - sat = 0.5 + 0.5 * (0.5 - distance_to_center) # more saturated near center - val = 0.7 + 0.3 * math.sin(index * 0.1) # subtle brightness wave - r, g, b = colorsys.hsv_to_rgb((base_hue + hue_shift) % 1.0, sat, val) - return (int(r*255), int(g*255), int(b*255)) - - elif color_scheme == "cyberpunk": - # Neon magenta & cyan with deep purple accents - hue_base = 0.83 if (index + (index // dimension)) % 2 == 0 else 0.5 # magenta or cyan - # Slight hue variation for organic feel - hue = (hue_base + (x_norm - 0.5) * 0.1 + (y_norm - 0.5) * 0.1) % 1.0 - sat = 1.0 - # Bright at center, darker towards edges - val = 0.6 + 0.4 * (1.0 - distance_to_center) - r, g, b = colorsys.hsv_to_rgb(hue, sat, val) - return (int(r * 255), int(g * 255), int(b * 255)) - - elif color_scheme == "aurora_borealis": - # Aurora Borealis - teal, green, purple flowing together - # Map distance and position into hue shifts for a wave-like effect - hue_center = 0.4 + math.sin((x_norm + y_norm + distance_to_center) * 4) * 0.1 # base around green/teal - hue = (hue_center + (math.sin(index * 0.05) * 0.15)) % 1.0 # shifting into purple/blue range - sat = 0.7 + 0.3 * math.sin(y_norm * 5 + distance_to_center * 3) # dynamic saturation - val = 0.6 + 0.4 * math.cos(x_norm * 4 + distance_to_center * 2) # gentle brightness movement - r, g, b = colorsys.hsv_to_rgb(hue, sat, val) - return (int(r * 255), int(g * 255), int(b * 255)) - - elif color_scheme == "infrared_thermal": - # Infrared/Thermal - classic thermal imaging progression - # blue → cyan → green → yellow → orange → red → white - # distance_to_center = 0 (center) -> cold (blue) - # distance_to_center = 1 (edge) -> hot (white) - - t = 1.0 - distance_to_center # invert so center is cold, edges are hot - - if t < 0.16: # 0.0 - 0.16: blue to cyan - progress = t / 0.16 - hue = 0.66 - progress * 0.08 # blue (0.66) to cyan (0.58) - sat = 1.0 - val = 0.3 + progress * 0.4 # dark blue to bright cyan - elif t < 0.33: # 0.16 - 0.33: cyan to green - progress = (t - 0.16) / 0.17 - hue = 0.58 - progress * 0.25 # cyan (0.58) to green (0.33) - sat = 1.0 - val = 0.7 + progress * 0.3 - elif t < 0.5: # 0.33 - 0.5: green to yellow - progress = (t - 0.33) / 0.17 - hue = 0.33 - progress * 0.17 # green (0.33) to yellow (0.16) - sat = 1.0 - val = 1.0 - elif t < 0.66: # 0.5 - 0.66: yellow to orange - progress = (t - 0.5) / 0.16 - hue = 0.16 - progress * 0.08 # yellow (0.16) to orange (0.08) - sat = 1.0 - val = 1.0 - elif t < 0.83: # 0.66 - 0.83: orange to red - progress = (t - 0.66) / 0.17 - hue = 0.08 - progress * 0.08 # orange (0.08) to red (0.0) - sat = 1.0 - val = 1.0 - elif t < 0.92: # 0.83 - 0.92: red to red-white - progress = (t - 0.83) / 0.09 - hue = 0.0 # pure red - sat = 1.0 - progress * 0.3 # desaturate towards white - val = 1.0 - else: # 0.92 - 1.0: red-white to pure white - progress = (t - 0.92) / 0.08 - hue = 0.0 - sat = 0.7 - progress * 0.7 # fully desaturate - val = 1.0 - - r, g, b = colorsys.hsv_to_rgb(hue, sat, val) - return (int(r * 255), int(g * 255), int(b * 255)) - - # Par dĂ©faut, retourner blanc - return (255, 255, 255) - - @staticmethod - def get_animated_color(base_color: Tuple[int, int, int], - position_index: int, - time: float, - color_animation: bool, - audio_reactive: bool, - audio_analyzer=None) -> Tuple[int, int, int]: - """ - Applique une animation de couleur si activĂ©e. - - Args: - base_color: Couleur de base du carrĂ© - position_index: Index de position pour variation - time: Temps actuel pour l'animation - color_animation: Si l'animation normale est activĂ©e - audio_reactive: Si l'animation audio-rĂ©active est activĂ©e - audio_analyzer: Instance de l'analyseur audio (optionnel) - - Returns: - Couleur animĂ©e ou couleur de base si animation dĂ©sactivĂ©e - """ - # Si aucune animation n'est activĂ©e, retourner la couleur de base - if not color_animation and not audio_reactive: - return base_color - - r, g, b = base_color - - # Appliquer l'animation normale seulement si color_animation est True - if color_animation and (not audio_reactive or not audio_analyzer): - pulse = math.sin(time * 2 + position_index * 0.1) * 0.2 + 1.0 - pulse = max(0.5, min(1.5, pulse)) - r = int(min(255, r * pulse)) - g = int(min(255, g * pulse)) - b = int(min(255, b * pulse)) - return (r, g, b) - - # Si seul audio_reactive est activĂ© (mais pas color_animation) - if audio_reactive and audio_analyzer and not color_animation: - # Ne pas appliquer l'animation normale, aller directement Ă  l'audio - pass - # Si les deux sont activĂ©s, appliquer d'abord l'animation normale - elif color_animation and audio_reactive and audio_analyzer: - pulse = math.sin(time * 2 + position_index * 0.1) * 0.2 + 1.0 - pulse = max(0.5, min(1.5, pulse)) - r = int(min(255, r * pulse)) - g = int(min(255, g * pulse)) - b = int(min(255, b * pulse)) - # Si ni l'un ni l'autre ne s'applique, retourner la couleur de base - elif not audio_reactive or not audio_analyzer: - return base_color - - # Animation rĂ©active Ă  l'audio - audio_features = audio_analyzer.get_audio_features() - - # Beat detection - flash blanc sur les beats - if audio_features['beat_detected']: - flash_intensity = 0.7 - r = int(min(255, r + (255 - r) * flash_intensity)) - g = int(min(255, g + (255 - g) * flash_intensity)) - b = int(min(255, b + (255 - b) * flash_intensity)) - - # Hautes frĂ©quences - augmentent la luminositĂ© - high_boost = 1.0 + audio_features['high_level'] * 0.5 - r = int(min(255, r * high_boost)) - g = int(min(255, g * high_boost)) - b = int(min(255, b * high_boost)) - - # Moyennes frĂ©quences - rotation de teinte - if audio_features['mid_level'] > 0.1: - # Convertir en HSV pour rotation de teinte - h, s, v = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) - h = (h + audio_features['mid_level'] * 0.3) % 1.0 - r, g, b = colorsys.hsv_to_rgb(h, s, v) - r, g, b = int(r * 255), int(g * 255), int(b * 255) - - return (r, g, b) \ No newline at end of file diff --git a/distorsion_movement/colors/__init__.py b/distorsion_movement/colors/__init__.py new file mode 100644 index 0000000..bf64c90 --- /dev/null +++ b/distorsion_movement/colors/__init__.py @@ -0,0 +1,156 @@ +""" +Package de schĂ©mas de couleurs. + +Ce package contient tous les gĂ©nĂ©rateurs de couleurs pour diffĂ©rents schĂ©mas +utilisĂ©s dans le systĂšme de grilles dĂ©formĂ©es. +""" + +from .base_color import BaseColor +from .monochrome import Monochrome +from .black_white_radial import BlackWhiteRadial +from .black_white_alternating import BlackWhiteAlternating +from .gradient import Gradient +from .rainbow import Rainbow +from .complementary import Complementary +from .temperature import Temperature +from .pastel import Pastel +from .neon import Neon +from .ocean import Ocean +from .fire import Fire +from .forest import Forest +from .analogous import Analogous +from .cyberpunk import Cyberpunk +from .aurora_borealis import AuroraBorealis +from .infrared_thermal import InfraredThermal +from .duotone_accent import DuotoneAccent +from .desert import Desert +from .metallics import Metallics +from .reggae import Reggae +from .sunset import Sunset +from .pop_art import PopArt +from .vaporwave import Vaporwave +from .candy_shop import CandyShop + + +# Registre des schĂ©mas de couleurs disponibles +COLOR_SCHEME_REGISTRY = { + "monochrome": Monochrome.get_color_for_position, + "black_white_radial": BlackWhiteRadial.get_color_for_position, + "black_white_alternating": BlackWhiteAlternating.get_color_for_position, + "gradient": Gradient.get_color_for_position, + "rainbow": Rainbow.get_color_for_position, + "complementary": Complementary.get_color_for_position, + "temperature": Temperature.get_color_for_position, + "pastel": Pastel.get_color_for_position, + "neon": Neon.get_color_for_position, + "ocean": Ocean.get_color_for_position, + "fire": Fire.get_color_for_position, + "forest": Forest.get_color_for_position, + "analogous": Analogous.get_color_for_position, + "cyberpunk": Cyberpunk.get_color_for_position, + "aurora_borealis": AuroraBorealis.get_color_for_position, + "infrared_thermal": InfraredThermal.get_color_for_position, + "duotone_accent": DuotoneAccent.get_color_for_position, + "desert": Desert.get_color_for_position, + "metallics": Metallics.get_color_for_position, + "reggae": Reggae.get_color_for_position, + "sunset": Sunset.get_color_for_position, + "pop_art": PopArt.get_color_for_position, + "vaporwave": Vaporwave.get_color_for_position, + "candy_shop": CandyShop.get_color_for_position, +} + + +def get_color_scheme_function(color_scheme: str): + """ + Retourne la fonction de gĂ©nĂ©ration de couleur correspondant au schĂ©ma de couleur. + + Args: + color_scheme: Nom du schĂ©ma de couleur + + Returns: + Fonction de gĂ©nĂ©ration de couleur appropriĂ©e + """ + return COLOR_SCHEME_REGISTRY.get(color_scheme, Monochrome.get_color_for_position) + + +class ColorGenerator: + """ + GĂ©nĂ©rateur de couleurs selon diffĂ©rents schĂ©mas - version refactorisĂ©e. + + Cette classe sert de point d'entrĂ©e principal pour maintenir la compatibilitĂ© + avec l'ancienne interface tout en utilisant la nouvelle architecture modulaire. + """ + + @staticmethod + def get_color_for_position(color_scheme: str, square_color, + x_norm: float, y_norm: float, + distance_to_center: float, index: int, + dimension: int): + """ + GĂ©nĂšre une couleur pour une position donnĂ©e selon le schĂ©ma de couleur actuel. + + Cette mĂ©thode maintient la compatibilitĂ© avec l'ancienne interface + tout en utilisant la nouvelle architecture modulaire. + + Args: + color_scheme: Nom du schĂ©ma de couleur + square_color: Couleur de base pour le schĂ©ma monochrome + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille (pour calculs) + + Returns: + Tuple RGB (r, g, b) + """ + color_function = get_color_scheme_function(color_scheme) + return color_function( + square_color, x_norm, y_norm, distance_to_center, index, dimension + ) + + @staticmethod + def get_animated_color(base_color, position_index: int, time: float, color_animation: bool): + """ + Applique une animation de couleur si activĂ©e. + + Cette mĂ©thode reste inchangĂ©e pour maintenir la compatibilitĂ©. + + Args: + base_color: Couleur de base du carrĂ© + position_index: Index de position pour variation + time: Temps actuel pour l'animation + color_animation: Si l'animation normale est activĂ©e + + Returns: + Couleur animĂ©e ou couleur de base si animation dĂ©sactivĂ©e + """ + import math + + # Si aucune animation n'est activĂ©e, retourner la couleur de base + if not color_animation: + return base_color + + r, g, b = base_color + + # Appliquer l'animation normale seulement si color_animation est True + if color_animation: + pulse = math.sin(time * 2 + position_index * 0.1) * 0.2 + 1.0 + pulse = max(0.5, min(1.5, pulse)) + r = int(min(255, r * pulse)) + g = int(min(255, g * pulse)) + b = int(min(255, b * pulse)) + return (r, g, b) + + +__all__ = [ + 'BaseColor', + 'Monochrome', 'BlackWhiteRadial', 'BlackWhiteAlternating', 'Gradient', 'Rainbow', + 'Complementary', 'Temperature', 'Pastel', 'Neon', 'Ocean', 'Fire', 'Forest', + 'Analogous', 'Cyberpunk', 'AuroraBorealis', 'InfraredThermal', 'DuotoneAccent', + 'Desert', 'Metallics', 'Reggae', 'Sunset', 'PopArt', 'Vaporwave', 'CandyShop', + 'ColorGenerator', + 'get_color_scheme_function', + 'COLOR_SCHEME_REGISTRY' +] \ No newline at end of file diff --git a/distorsion_movement/colors/analogous.py b/distorsion_movement/colors/analogous.py new file mode 100644 index 0000000..a073d89 --- /dev/null +++ b/distorsion_movement/colors/analogous.py @@ -0,0 +1,40 @@ +""" +SchĂ©ma de couleur analogues. +""" + +import math +from typing import Tuple +from .base_color import BaseColor + + +class Analogous(BaseColor): + """Couleurs analogues avec variations subtiles.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs analogues avec des variations de teinte. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur analogue RGB + """ + base_hue = 0.3 # green-ish + hue_shift = (x_norm - 0.5) * 0.3 + (y_norm - 0.5) * 0.3 # ±0.3 spread + sat = 0.5 + 0.5 * (0.5 - distance_to_center) # more saturated near center + val = 0.7 + 0.3 * math.sin(index * 0.1) # subtle brightness wave + return BaseColor._hsv_to_rgb_clamped((base_hue + hue_shift) % 1.0, sat, val) \ No newline at end of file diff --git a/distorsion_movement/colors/aurora_borealis.py b/distorsion_movement/colors/aurora_borealis.py new file mode 100644 index 0000000..91af2ee --- /dev/null +++ b/distorsion_movement/colors/aurora_borealis.py @@ -0,0 +1,41 @@ +""" +SchĂ©ma de couleur aurore borĂ©ale. +""" + +import math +from typing import Tuple +from .base_color import BaseColor + + +class AuroraBorealis(BaseColor): + """Aurora Borealis - teal, vert, violet fluides ensemble.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs d'aurore borĂ©ale avec effet ondulant. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur d'aurore borĂ©ale RGB + """ + # Map distance and position into hue shifts for a wave-like effect + hue_center = 0.4 + math.sin((x_norm + y_norm + distance_to_center) * 4) * 0.1 # base around green/teal + hue = (hue_center + (math.sin(index * 0.05) * 0.15)) % 1.0 # shifting into purple/blue range + sat = 0.7 + 0.3 * math.sin(y_norm * 5 + distance_to_center * 3) # dynamic saturation + val = 0.6 + 0.4 * math.cos(x_norm * 4 + distance_to_center * 2) # gentle brightness movement + return BaseColor._hsv_to_rgb_clamped(hue, sat, val) \ No newline at end of file diff --git a/distorsion_movement/colors/base_color.py b/distorsion_movement/colors/base_color.py new file mode 100644 index 0000000..4173a4a --- /dev/null +++ b/distorsion_movement/colors/base_color.py @@ -0,0 +1,99 @@ +""" +Module de base pour les schĂ©mas de couleurs. + +Ce module contient la classe de base et les fonctionnalitĂ©s communes +Ă  tous les schĂ©mas de couleurs utilisĂ©s dans le systĂšme de grilles dĂ©formĂ©es. +""" + +import math +import colorsys +from typing import Tuple + + +class BaseColor: + """Classe de base pour tous les schĂ©mas de couleurs.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre une couleur pour une position donnĂ©e selon le schĂ©ma de couleur. + + Cette mĂ©thode doit ĂȘtre implĂ©mentĂ©e par chaque schĂ©ma de couleur spĂ©cifique. + + Args: + square_color: Couleur de base pour le schĂ©ma monochrome + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille (pour calculs) + + Returns: + Tuple RGB (r, g, b) + """ + raise NotImplementedError("Chaque schĂ©ma de couleur doit implĂ©menter cette mĂ©thode") + + @staticmethod + def _clamp_rgb(r: float, g: float, b: float) -> Tuple[int, int, int]: + """ + Assure que les valeurs RGB sont dans la plage valide [0, 255]. + + Args: + r, g, b: Valeurs RGB (peuvent ĂȘtre des floats) + + Returns: + Tuple RGB avec valeurs entiĂšres clampĂ©es + """ + return ( + max(0, min(255, int(r))), + max(0, min(255, int(g))), + max(0, min(255, int(b))) + ) + + @staticmethod + def _blend_colors(color1: Tuple[int, int, int], color2: Tuple[int, int, int], + blend: float) -> Tuple[int, int, int]: + """ + MĂ©lange deux couleurs selon un facteur de mĂ©lange. + + Args: + color1: PremiĂšre couleur RGB + color2: DeuxiĂšme couleur RGB + blend: Facteur de mĂ©lange (0.0 = color1, 1.0 = color2) + + Returns: + Couleur mĂ©langĂ©e RGB + """ + blend = max(0.0, min(1.0, blend)) # Clamp blend factor + r = color1[0] + (color2[0] - color1[0]) * blend + g = color1[1] + (color2[1] - color1[1]) * blend + b = color1[2] + (color2[2] - color1[2]) * blend + return BaseColor._clamp_rgb(r, g, b) + + @staticmethod + def _hsv_to_rgb_clamped(h: float, s: float, v: float) -> Tuple[int, int, int]: + """ + Convertit HSV en RGB avec clamping automatique. + + Args: + h: Hue (0.0 Ă  1.0) + s: Saturation (0.0 Ă  1.0) + v: Value/Brightness (0.0 Ă  1.0) + + Returns: + Tuple RGB avec valeurs entiĂšres clampĂ©es + """ + # Clamp input values + h = h % 1.0 # Wrap hue + s = max(0.0, min(1.0, s)) + v = max(0.0, min(1.0, v)) + + r, g, b = colorsys.hsv_to_rgb(h, s, v) + return BaseColor._clamp_rgb(r * 255, g * 255, b * 255) \ No newline at end of file diff --git a/distorsion_movement/colors/black_white_alternating.py b/distorsion_movement/colors/black_white_alternating.py new file mode 100644 index 0000000..1e71746 --- /dev/null +++ b/distorsion_movement/colors/black_white_alternating.py @@ -0,0 +1,39 @@ +""" +SchĂ©ma de couleur noir et blanc alternant (damier). +""" + +from typing import Tuple +from .base_color import BaseColor + + +class BlackWhiteAlternating(BaseColor): + """SchĂ©ma noir et blanc en damier/checkerboard.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre un pattern de damier noir et blanc. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille + + Returns: + Couleur noir ou blanc selon la position dans le damier + """ + row = index // dimension + col = index % dimension + # Pattern damier classique + is_white = (row + col) % 2 == 0 + return (255, 255, 255) if is_white else (0, 0, 0) \ No newline at end of file diff --git a/distorsion_movement/colors/black_white_radial.py b/distorsion_movement/colors/black_white_radial.py new file mode 100644 index 0000000..f4d95ea --- /dev/null +++ b/distorsion_movement/colors/black_white_radial.py @@ -0,0 +1,43 @@ +""" +SchĂ©ma de couleur noir et blanc radial. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class BlackWhiteRadial(BaseColor): + """SchĂ©ma noir et blanc basĂ© sur la distance au centre.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre du noir et blanc selon la distance au centre. + + Les formes au centre tendent vers le blanc, celles aux bords vers le noir. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur noir ou blanc selon la distance + """ + if distance_to_center < 0.3: + return (255, 255, 255) # Blanc au centre + elif distance_to_center > 0.7: + return (0, 0, 0) # Noir aux bords + else: + # Zone intermĂ©diaire : alternance selon l'index + return (255, 255, 255) if (index % 2 == 0) else (0, 0, 0) \ No newline at end of file diff --git a/distorsion_movement/colors/candy_shop.py b/distorsion_movement/colors/candy_shop.py new file mode 100644 index 0000000..80d2915 --- /dev/null +++ b/distorsion_movement/colors/candy_shop.py @@ -0,0 +1,45 @@ +""" +SchĂ©ma de couleur magasin de bonbons. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class CandyShop(BaseColor): + """Magasin de bonbons - rose bubblegum, vert menthe, jaune citron.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs de bonbons en damier. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille + + Returns: + Couleur bonbon RGB + """ + bubblegum = (255, 105, 180) # pink + mint = (152, 255, 152) # light mint green + lemon = (255, 250, 102) # lemon yellow + + palette = [bubblegum, mint, lemon] + + row = index // dimension + col = index % dimension + + # Checkerboard cycling through candy colors + return palette[(row + col) % len(palette)] \ No newline at end of file diff --git a/distorsion_movement/colors/complementary.py b/distorsion_movement/colors/complementary.py new file mode 100644 index 0000000..0faa6d8 --- /dev/null +++ b/distorsion_movement/colors/complementary.py @@ -0,0 +1,38 @@ +""" +SchĂ©ma de couleur complĂ©mentaire. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Complementary(BaseColor): + """Couleurs complĂ©mentaires alternĂ©es.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs complĂ©mentaires alternĂ©es. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille + + Returns: + Couleur orange ou bleue complĂ©mentaire + """ + if (index + (index // dimension)) % 2 == 0: + return (255, 100, 50) # Orange + else: + return (50, 150, 255) # Bleu \ No newline at end of file diff --git a/distorsion_movement/colors/cyberpunk.py b/distorsion_movement/colors/cyberpunk.py new file mode 100644 index 0000000..a702f6e --- /dev/null +++ b/distorsion_movement/colors/cyberpunk.py @@ -0,0 +1,41 @@ +""" +SchĂ©ma de couleur cyberpunk. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Cyberpunk(BaseColor): + """Neon magenta & cyan avec accents violets profonds.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs cyberpunk avec magenta et cyan nĂ©on. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille + + Returns: + Couleur cyberpunk RGB + """ + hue_base = 0.83 if (index + (index // dimension)) % 2 == 0 else 0.5 # magenta or cyan + # Slight hue variation for organic feel + hue = (hue_base + (x_norm - 0.5) * 0.1 + (y_norm - 0.5) * 0.1) % 1.0 + sat = 1.0 + # Bright at center, darker towards edges + val = 0.6 + 0.4 * (1.0 - distance_to_center) + return BaseColor._hsv_to_rgb_clamped(hue, sat, val) \ No newline at end of file diff --git a/distorsion_movement/colors/desert.py b/distorsion_movement/colors/desert.py new file mode 100644 index 0000000..5d7b547 --- /dev/null +++ b/distorsion_movement/colors/desert.py @@ -0,0 +1,48 @@ +""" +SchĂ©ma de couleur thĂšme dĂ©sert. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Desert(BaseColor): + """ThĂšme dĂ©sert - beige sableux, orange chaud, brun mat.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs de dĂ©sert selon la zone. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur de dĂ©sert selon la zone + """ + sand = (237, 201, 175) # sandy beige + orange = (210, 125, 45) # warm orange + brown = (102, 51, 0) # muted brown + + # Use distance and position to vary tones + if distance_to_center < 0.33: + # Center: light sand + return sand + elif distance_to_center < 0.66: + # Mid ring: warm orange + return orange + else: + # Outer ring: deep brown + return brown \ No newline at end of file diff --git a/distorsion_movement/colors/duotone_accent.py b/distorsion_movement/colors/duotone_accent.py new file mode 100644 index 0000000..fb6cf6c --- /dev/null +++ b/distorsion_movement/colors/duotone_accent.py @@ -0,0 +1,62 @@ +""" +SchĂ©ma de couleur duotone avec accent. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class DuotoneAccent(BaseColor): + """Deux couleurs principales avec un accent rare.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre un duotone avec accent rare. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille + + Returns: + Couleur duotone avec accent occasionnel + """ + color_a = (30, 144, 255) # Dodger blue + color_b = (255, 105, 180) # Hot pink + accent = (255, 255, 0) # Bright yellow pop + + row = index // dimension + col = index % dimension + + # Use pseudo-random function based on position for unpredictable accent placement + # This creates a deterministic but seemingly random pattern + seed = (row * 73 + col * 151 + row * col * 23) % 997 # Large prime for better distribution + + # Rare accent: approximately 1 in 20-25 cells (4-5% chance) + if seed < 40: # 40/997 ≈ 4% chance + return accent + + # More interesting duotone pattern: use Perlin-like noise for organic distribution + # Combine multiple pattern scales for visual complexity + pattern1 = (row // 2 + col // 2) % 2 # Larger checkerboard + pattern2 = (row + col) % 3 # Diagonal stripes + pattern3 = ((row * 3) % 7 + (col * 2) % 5) % 2 # Irregular pattern + + # Combine patterns for more organic distribution + combined_pattern = (pattern1 + pattern2 + pattern3) % 2 + + if combined_pattern == 0: + return color_a + else: + return color_b \ No newline at end of file diff --git a/distorsion_movement/colors/fire.py b/distorsion_movement/colors/fire.py new file mode 100644 index 0000000..581851d --- /dev/null +++ b/distorsion_movement/colors/fire.py @@ -0,0 +1,41 @@ +""" +SchĂ©ma de couleur thĂšme feu. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Fire(BaseColor): + """ThĂšme feu - rouges, oranges, jaunes.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs de feu selon l'intensitĂ©. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur de feu selon l'intensitĂ© + """ + intensity = 1.0 - distance_to_center + y_norm * 0.3 + if intensity > 0.8: + return (255, 255, 100) # Jaune chaud + elif intensity > 0.5: + return (255, 140, 0) # Orange + else: + return (220, 20, 60) # Rouge foncĂ© \ No newline at end of file diff --git a/distorsion_movement/colors/forest.py b/distorsion_movement/colors/forest.py new file mode 100644 index 0000000..2f5a7b5 --- /dev/null +++ b/distorsion_movement/colors/forest.py @@ -0,0 +1,41 @@ +""" +SchĂ©ma de couleur thĂšme forĂȘt. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Forest(BaseColor): + """ThĂšme forĂȘt - verts variĂ©s.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs de forĂȘt avec diffĂ©rentes nuances de vert. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur de forĂȘt selon l'intensitĂ© verte + """ + green_intensity = 0.3 + distance_to_center * 0.7 + x_norm * 0.2 + if green_intensity > 0.8: + return (144, 238, 144) # Vert clair + elif green_intensity > 0.5: + return (34, 139, 34) # Vert forĂȘt + else: + return (0, 100, 0) # Vert foncĂ© \ No newline at end of file diff --git a/distorsion_movement/colors/gradient.py b/distorsion_movement/colors/gradient.py new file mode 100644 index 0000000..c4b9b1e --- /dev/null +++ b/distorsion_movement/colors/gradient.py @@ -0,0 +1,39 @@ +""" +SchĂ©ma de couleur en gradient diagonal. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Gradient(BaseColor): + """Gradient diagonal du coin supĂ©rieur gauche au coin infĂ©rieur droit.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre un gradient diagonal. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur RGB du gradient + """ + t = (x_norm + y_norm) / 2.0 + r = int(50 + t * 205) + g = int(100 + t * 155) + b = int(200 - t * 100) + return BaseColor._clamp_rgb(r, g, b) \ No newline at end of file diff --git a/distorsion_movement/colors/infrared_thermal.py b/distorsion_movement/colors/infrared_thermal.py new file mode 100644 index 0000000..c03bda6 --- /dev/null +++ b/distorsion_movement/colors/infrared_thermal.py @@ -0,0 +1,77 @@ +""" +SchĂ©ma de couleur infrarouge thermique. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class InfraredThermal(BaseColor): + """Infrarouge/Thermal - progression classique d'imagerie thermique.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs thermiques infrarouge. + + Progression: bleu → cyan → vert → jaune → orange → rouge → blanc + distance_to_center = 0 (centre) -> froid (bleu) + distance_to_center = 1 (bord) -> chaud (blanc) + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur thermique RGB + """ + t = 1.0 - distance_to_center # invert so center is cold, edges are hot + + if t < 0.16: # 0.0 - 0.16: blue to cyan + progress = t / 0.16 + hue = 0.66 - progress * 0.08 # blue (0.66) to cyan (0.58) + sat = 1.0 + val = 0.3 + progress * 0.4 # dark blue to bright cyan + elif t < 0.33: # 0.16 - 0.33: cyan to green + progress = (t - 0.16) / 0.17 + hue = 0.58 - progress * 0.25 # cyan (0.58) to green (0.33) + sat = 1.0 + val = 0.7 + progress * 0.3 + elif t < 0.5: # 0.33 - 0.5: green to yellow + progress = (t - 0.33) / 0.17 + hue = 0.33 - progress * 0.17 # green (0.33) to yellow (0.16) + sat = 1.0 + val = 1.0 + elif t < 0.66: # 0.5 - 0.66: yellow to orange + progress = (t - 0.5) / 0.16 + hue = 0.16 - progress * 0.08 # yellow (0.16) to orange (0.08) + sat = 1.0 + val = 1.0 + elif t < 0.83: # 0.66 - 0.83: orange to red + progress = (t - 0.66) / 0.17 + hue = 0.08 - progress * 0.08 # orange (0.08) to red (0.0) + sat = 1.0 + val = 1.0 + elif t < 0.92: # 0.83 - 0.92: red to red-white + progress = (t - 0.83) / 0.09 + hue = 0.0 # pure red + sat = 1.0 - progress * 0.3 # desaturate towards white + val = 1.0 + else: # 0.92 - 1.0: red-white to pure white + progress = (t - 0.92) / 0.08 + hue = 0.0 + sat = 0.7 - progress * 0.7 # fully desaturate + val = 1.0 + + return BaseColor._hsv_to_rgb_clamped(hue, sat, val) \ No newline at end of file diff --git a/distorsion_movement/colors/metallics.py b/distorsion_movement/colors/metallics.py new file mode 100644 index 0000000..df5d1a8 --- /dev/null +++ b/distorsion_movement/colors/metallics.py @@ -0,0 +1,53 @@ +""" +SchĂ©ma de couleur mĂ©talliques. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Metallics(BaseColor): + """Gradients mĂ©talliques - or, argent, bronze.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs mĂ©talliques avec gradients. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur mĂ©tallique RGB + """ + gold = (212, 175, 55) + silver = (192, 192, 192) + bronze = (205, 127, 50) + + # Use position to blend smoothly between metals + t = (x_norm + y_norm) / 2.0 # 0..1 diagonal gradient + + if t < 0.33: + # Blend from gold to silver + blend = t / 0.33 + return BaseColor._blend_colors(gold, silver, blend) + elif t < 0.66: + # Blend from silver to bronze + blend = (t - 0.33) / 0.33 + return BaseColor._blend_colors(silver, bronze, blend) + else: + # Blend from bronze back to gold + blend = (t - 0.66) / 0.34 + return BaseColor._blend_colors(bronze, gold, blend) \ No newline at end of file diff --git a/distorsion_movement/colors/monochrome.py b/distorsion_movement/colors/monochrome.py new file mode 100644 index 0000000..30a06b1 --- /dev/null +++ b/distorsion_movement/colors/monochrome.py @@ -0,0 +1,35 @@ +""" +SchĂ©ma de couleur monochrome. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Monochrome(BaseColor): + """SchĂ©ma de couleur monochrome - utilise la couleur de base fournie.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + Retourne la couleur de base sans modification. + + Args: + square_color: Couleur de base + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + La couleur de base inchangĂ©e + """ + return square_color \ No newline at end of file diff --git a/distorsion_movement/colors/neon.py b/distorsion_movement/colors/neon.py new file mode 100644 index 0000000..b1ad8c1 --- /dev/null +++ b/distorsion_movement/colors/neon.py @@ -0,0 +1,36 @@ +""" +SchĂ©ma de couleur nĂ©on. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Neon(BaseColor): + """Couleurs nĂ©on vives.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs nĂ©on vives. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur nĂ©on RGB + """ + hue = (distance_to_center + x_norm * 0.5) % 1.0 + return BaseColor._hsv_to_rgb_clamped(hue, 1.0, 1.0) \ No newline at end of file diff --git a/distorsion_movement/colors/ocean.py b/distorsion_movement/colors/ocean.py new file mode 100644 index 0000000..7237abf --- /dev/null +++ b/distorsion_movement/colors/ocean.py @@ -0,0 +1,44 @@ +""" +SchĂ©ma de couleur thĂšme ocĂ©an. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Ocean(BaseColor): + """ThĂšme ocĂ©an - bleus et verts.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs ocĂ©an selon la profondeur. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur ocĂ©an selon la profondeur + """ + depth = distance_to_center + if depth < 0.3: + # Eau peu profonde - turquoise + return (64, 224, 208) + elif depth < 0.7: + # Eau moyenne - bleu ocĂ©an + return (0, 119, 190) + else: + # Eau profonde - bleu foncĂ© + return (25, 25, 112) \ No newline at end of file diff --git a/distorsion_movement/colors/pastel.py b/distorsion_movement/colors/pastel.py new file mode 100644 index 0000000..e229a8d --- /dev/null +++ b/distorsion_movement/colors/pastel.py @@ -0,0 +1,36 @@ +""" +SchĂ©ma de couleur pastel. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Pastel(BaseColor): + """Couleurs pastel douces.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs pastel douces. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur pastel RGB + """ + hue = (x_norm * 0.3 + y_norm * 0.7) % 1.0 + return BaseColor._hsv_to_rgb_clamped(hue, 0.3, 0.9) \ No newline at end of file diff --git a/distorsion_movement/colors/pop_art.py b/distorsion_movement/colors/pop_art.py new file mode 100644 index 0000000..41e7830 --- /dev/null +++ b/distorsion_movement/colors/pop_art.py @@ -0,0 +1,50 @@ +""" +SchĂ©ma de couleur pop art. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class PopArt(BaseColor): + """Pop Art - primaires vives avec contours noir/blanc.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs pop art avec primaires et contours. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille + + Returns: + Couleur pop art RGB + """ + primaries = [ + (255, 0, 0), # Red + (0, 0, 255), # Blue + (255, 255, 0), # Yellow + (0, 255, 0) # Green + ] + + row = index // dimension + col = index % dimension + + # Every Nth cell becomes black/white for a bold outline effect + if row % 5 == 0 or col % 5 == 0: + return (0, 0, 0) if (row + col) % 2 == 0 else (255, 255, 255) + + # Otherwise, cycle through bright primaries + return primaries[(row + col) % len(primaries)] \ No newline at end of file diff --git a/distorsion_movement/colors/rainbow.py b/distorsion_movement/colors/rainbow.py new file mode 100644 index 0000000..3ccdb77 --- /dev/null +++ b/distorsion_movement/colors/rainbow.py @@ -0,0 +1,36 @@ +""" +SchĂ©ma de couleur arc-en-ciel. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Rainbow(BaseColor): + """Arc-en-ciel basĂ© sur la position.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs arc-en-ciel selon la position. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (0.0 Ă  1.0) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur RGB de l'arc-en-ciel + """ + hue = (x_norm + y_norm * 0.5) % 1.0 + return BaseColor._hsv_to_rgb_clamped(hue, 0.8, 0.9) \ No newline at end of file diff --git a/distorsion_movement/colors/reggae.py b/distorsion_movement/colors/reggae.py new file mode 100644 index 0000000..14098be --- /dev/null +++ b/distorsion_movement/colors/reggae.py @@ -0,0 +1,48 @@ +""" +SchĂ©ma de couleur thĂšme reggae. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Reggae(BaseColor): + """ThĂšme reggae - vert, jaune, rouge.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs reggae selon la zone. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur reggae selon la zone + """ + green = (0, 153, 51) # rich green + yellow = (255, 204, 0) # bright yellow + red = (204, 0, 0) # deep red + + # Use distance and position to vary tones + if distance_to_center < 0.33: + # Center: green + return green + elif distance_to_center < 0.66: + # Mid ring: yellow + return yellow + else: + # Outer ring: red + return red \ No newline at end of file diff --git a/distorsion_movement/colors/sunset.py b/distorsion_movement/colors/sunset.py new file mode 100644 index 0000000..86466f5 --- /dev/null +++ b/distorsion_movement/colors/sunset.py @@ -0,0 +1,54 @@ +""" +SchĂ©ma de couleur coucher de soleil. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Sunset(BaseColor): + """Gradient de coucher de soleil: jaune chaud → orange → rose → violet.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre un gradient de coucher de soleil vertical. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (0.0 Ă  1.0) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur de coucher de soleil RGB + """ + yellow = (255, 223, 0) + orange = (255, 140, 0) + pink = (255, 105, 180) + purple = (128, 0, 128) + + # Map vertical position to gradient + t = y_norm # 0 = top, 1 = bottom + + if t < 0.33: + # Yellow to orange + blend = t / 0.33 + return BaseColor._blend_colors(yellow, orange, blend) + elif t < 0.66: + # Orange to pink + blend = (t - 0.33) / 0.33 + return BaseColor._blend_colors(orange, pink, blend) + else: + # Pink to purple + blend = (t - 0.66) / 0.34 + return BaseColor._blend_colors(pink, purple, blend) \ No newline at end of file diff --git a/distorsion_movement/colors/temperature.py b/distorsion_movement/colors/temperature.py new file mode 100644 index 0000000..eec5490 --- /dev/null +++ b/distorsion_movement/colors/temperature.py @@ -0,0 +1,44 @@ +""" +SchĂ©ma de couleur basĂ© sur la tempĂ©rature. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Temperature(BaseColor): + """Couleurs chaudes au centre, froides aux bords.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs selon la tempĂ©rature basĂ©e sur la distance au centre. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre normalisĂ©e (0.0 Ă  1.0) + index: Index du carrĂ© (non utilisĂ©) + dimension: Dimension de la grille (non utilisĂ©e) + + Returns: + Couleur chaude (centre) ou froide (bords) + """ + temp = 1.0 - distance_to_center + if temp > 0.7: + # TrĂšs chaud - rouge/jaune + return BaseColor._hsv_to_rgb_clamped(0.1, 0.8, 1.0) + elif temp > 0.4: + # Chaud - orange/rouge + return BaseColor._hsv_to_rgb_clamped(0.05, 0.9, 0.9) + else: + # Froid - bleu/violet + return BaseColor._hsv_to_rgb_clamped(0.6 + temp * 0.2, 0.7, 0.8) \ No newline at end of file diff --git a/distorsion_movement/colors/vaporwave.py b/distorsion_movement/colors/vaporwave.py new file mode 100644 index 0000000..3d891ef --- /dev/null +++ b/distorsion_movement/colors/vaporwave.py @@ -0,0 +1,46 @@ +""" +SchĂ©ma de couleur vaporwave. +""" + +from typing import Tuple +from .base_color import BaseColor + + +class Vaporwave(BaseColor): + """Vaporwave - nĂ©ons pastels doux: lavande, cyan, pĂȘche, rose.""" + + @staticmethod + def get_color_for_position( + square_color: Tuple[int, int, int], + x_norm: float, + y_norm: float, + distance_to_center: float, + index: int, + dimension: int + ) -> Tuple[int, int, int]: + """ + GĂ©nĂšre des couleurs vaporwave en damier. + + Args: + square_color: Couleur de base (non utilisĂ©e) + x_norm: Position X normalisĂ©e (non utilisĂ©e) + y_norm: Position Y normalisĂ©e (non utilisĂ©e) + distance_to_center: Distance au centre (non utilisĂ©e) + index: Index du carrĂ© dans la grille + dimension: Dimension de la grille + + Returns: + Couleur vaporwave RGB + """ + lavender = (181, 126, 220) + cyan = (0, 255, 255) + peach = (255, 203, 164) + pink = (255, 105, 180) + + palette = [lavender, cyan, peach, pink] + + row = index // dimension + col = index % dimension + + # Checkerboard pattern cycling through palette + return palette[(row + col) % len(palette)] \ No newline at end of file diff --git a/distorsion_movement/deformed_grid.py b/distorsion_movement/deformed_grid.py index 30b5ecf..abfa380 100644 --- a/distorsion_movement/deformed_grid.py +++ b/distorsion_movement/deformed_grid.py @@ -15,13 +15,14 @@ from typing import Tuple, List from distorsion_movement.enums import DistortionType, ColorScheme, ShapeType -from distorsion_movement.audio_analyzer import AudioAnalyzer from distorsion_movement.colors import ColorGenerator from distorsion_movement.distortions import DistortionEngine from distorsion_movement.shapes import get_shape_renderer_function import os import imageio +import yaml +import glob class DeformedGrid: @@ -42,7 +43,6 @@ def __init__(self, square_color: Tuple[int, int, int] = (255, 255, 255), color_scheme: str = "monochrome", color_animation: bool = False, - audio_reactive: bool = False, shape_type: str = "square", mixed_shapes: bool = False): """ @@ -58,7 +58,6 @@ def __init__(self, square_color: Couleur des carrĂ©s RGB (utilisĂ©e pour monochrome) 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 shape_type: Type de forme Ă  dessiner ("square", "circle", "triangle", etc.) mixed_shapes: Si True, utilise diffĂ©rentes formes dans la grille """ @@ -71,12 +70,9 @@ def __init__(self, self.square_color = square_color self.color_scheme = color_scheme self.color_animation = color_animation - self.audio_reactive = audio_reactive self.shape_type = shape_type self.mixed_shapes = mixed_shapes - # Audio analyzer - self.audio_analyzer = AudioAnalyzer() if audio_reactive else None self.base_distortion_strength = distortion_strength # Sauvegarde de l'intensitĂ© de base # Calcul automatique du dĂ©calage pour centrer la grille @@ -107,6 +103,11 @@ def __init__(self, self.base_dimension = dimension # Sauvegarder la dimension originale self.grid_density_mode = False # Mode d'ajustement de la densitĂ© de grille + # Variables pour l'itĂ©ration Ă  travers les scĂšnes sauvegardĂ©es + self.saved_scenes = [] + self.current_scene_index = -1 + self.scene_iteration_mode = False + # Initialisation pygame pygame.init() # Get screen info for fullscreen @@ -141,11 +142,7 @@ def __init__(self, # GĂ©nĂ©ration des types de formes pour chaque cellule self._generate_shape_types() - - # DĂ©marrage de l'analyse audio si activĂ©e - if self.audio_reactive and self.audio_analyzer: - self.audio_analyzer.start_audio_capture() - + def _generate_base_positions(self): """GĂ©nĂšre les positions de base de la grille rĂ©guliĂšre""" self.base_positions = [] @@ -313,7 +310,7 @@ def _render_help_menu(self): ("D", "Afficher/masquer les infos de statut"), ]), ("Distorsion & Animation", [ - ("ESPACE", "Changer le type de distorsion"), + ("ESPACE / Shift+ESPACE", "Distorsion suivante / prĂ©cĂ©dente"), ("+/-", "Ajuster l'intensitĂ© de distorsion"), ("R", "RĂ©gĂ©nĂ©rer les paramĂštres alĂ©atoires"), ]), @@ -321,47 +318,114 @@ def _render_help_menu(self): ("T puis +/-", "Ajuster la densitĂ© de grille (nombre de cellules)"), ]), ("Couleurs", [ - ("C", "Changer le schĂ©ma de couleurs"), + ("C / Shift+C", "Couleur suivante / prĂ©cĂ©dente"), ("A", "Activer/dĂ©sactiver l'animation des couleurs"), ]), ("Formes", [ - ("H", "Changer le type de forme"), - ("Shift+H", "Basculer mode formes mixtes"), + ("H / Shift+H", "Forme suivante / prĂ©cĂ©dente"), + ("Ctrl+H", "Basculer mode formes mixtes"), ]), - ("Audio & Sauvegarde", [ - ("M", "Activer/dĂ©sactiver la rĂ©activitĂ© audio"), - ("S", "Sauvegarder l'image actuelle"), + ("Sauvegarde", [ + ("S", "Sauvegarder l'image actuelle (+ paramĂštres YAML)"), ("G", "DĂ©marrer/arrĂȘter l'enregistrement GIF"), + ]), + ("ScĂšnes SauvegardĂ©es", [ + ("L / Shift+L", "ScĂšne suivante / prĂ©cĂ©dente"), + ("P", "Actualiser la liste des scĂšnes"), ]) ] # Position de dĂ©part pour le texte y_offset = 100 - section_spacing = 40 - line_spacing = 25 - - for section_title, section_controls in controls: - # Titre de section - section_text = self.help_title_font.render(section_title, True, (255, 200, 100)) - section_rect = section_text.get_rect(center=(current_size[0] // 2, y_offset)) - self.screen.blit(section_text, section_rect) - y_offset += section_spacing - - # ContrĂŽles de la section - for key, description in section_controls: - # Afficher la touche en couleur - key_text = self.help_font.render(f"{key}:", True, (100, 255, 100)) - desc_text = self.help_font.render(description, True, (255, 255, 255)) + section_spacing = 35 + line_spacing = 22 + + # Diviser les contrĂŽles en deux colonnes + mid_index = len(controls) // 2 + left_column = controls[:mid_index] + right_column = controls[mid_index:] + + # Largeur des colonnes avec padding + padding = 40 + column_width = (current_size[0] - padding * 3) // 2 # 3 paddings: left, center, right + left_column_x = padding + right_column_x = current_size[0] // 2 + padding // 2 + + # Afficher les colonnes en parallĂšle + max_sections = max(len(left_column), len(right_column)) + + for i in range(max_sections): + current_y_left = y_offset + current_y_right = y_offset + + # Colonne de gauche + if i < len(left_column): + section_title, section_controls = left_column[i] - # Centrer horizontalement - total_width = key_text.get_width() + desc_text.get_width() + 10 - start_x = (current_size[0] - total_width) // 2 + # Titre de section (gauche) + section_text = self.help_title_font.render(section_title, True, (255, 200, 100)) + section_x = left_column_x + (column_width - section_text.get_width()) // 2 + self.screen.blit(section_text, (section_x, current_y_left)) + current_y_left += section_spacing - self.screen.blit(key_text, (start_x, y_offset)) - self.screen.blit(desc_text, (start_x + key_text.get_width() + 10, y_offset)) - y_offset += line_spacing + # ContrĂŽles de la section (gauche) + for key, description in section_controls: + # Afficher la touche en couleur + key_text = self.help_font.render(f"{key}:", True, (100, 255, 100)) + + # Calculer l'espace disponible pour la description + available_width = column_width - key_text.get_width() - 20 + + # Tronquer la description si nĂ©cessaire + if self.help_font.size(description)[0] > available_width: + # Tronquer progressivement jusqu'Ă  ce que ça rentre + truncated_desc = description + while self.help_font.size(truncated_desc + "...")[0] > available_width and len(truncated_desc) > 10: + truncated_desc = truncated_desc[:-1] + description = truncated_desc + "..." + + desc_text = self.help_font.render(description, True, (255, 255, 255)) + + # Alignement Ă  gauche dans la colonne + self.screen.blit(key_text, (left_column_x, current_y_left)) + self.screen.blit(desc_text, (left_column_x + key_text.get_width() + 10, current_y_left)) + current_y_left += line_spacing - y_offset += 10 # Espacement entre sections + # Colonne de droite + if i < len(right_column): + section_title, section_controls = right_column[i] + + # Titre de section (droite) + section_text = self.help_title_font.render(section_title, True, (255, 200, 100)) + section_x = right_column_x + (column_width - section_text.get_width()) // 2 + self.screen.blit(section_text, (section_x, current_y_right)) + current_y_right += section_spacing + + # ContrĂŽles de la section (droite) + for key, description in section_controls: + # Afficher la touche en couleur + key_text = self.help_font.render(f"{key}:", True, (100, 255, 100)) + + # Calculer l'espace disponible pour la description + available_width = column_width - key_text.get_width() - 20 + + # Tronquer la description si nĂ©cessaire + if self.help_font.size(description)[0] > available_width: + # Tronquer progressivement jusqu'Ă  ce que ça rentre + truncated_desc = description + while self.help_font.size(truncated_desc + "...")[0] > available_width and len(truncated_desc) > 10: + truncated_desc = truncated_desc[:-1] + description = truncated_desc + "..." + + desc_text = self.help_font.render(description, True, (255, 255, 255)) + + # Alignement Ă  gauche dans la colonne + self.screen.blit(key_text, (right_column_x, current_y_right)) + self.screen.blit(desc_text, (right_column_x + key_text.get_width() + 10, current_y_right)) + current_y_right += line_spacing + + # Prendre la plus grande hauteur des deux colonnes pour l'espacement + y_offset = max(current_y_left, current_y_right) + 15 # Instructions en bas footer_text = self.help_font.render("Appuyez sur 'I' ou TAB pour fermer cette aide", True, (200, 200, 200)) @@ -392,9 +456,7 @@ def _render_status_display(self): # Ajouter des informations supplĂ©mentaires si pertinentes if self.color_animation: - status_lines.append("Animation: ON") - if self.audio_reactive: - status_lines.append("Audio: ON") + status_lines.append("Color animation: ON") if self.mixed_shapes: status_lines.append("Formes: Mixtes") else: @@ -449,8 +511,7 @@ def render(self): # Obtenir la couleur de base et appliquer l'animation si nĂ©cessaire base_color = self.base_colors[i] final_color = ColorGenerator.get_animated_color( - base_color, i, self.time, self.color_animation, - self.audio_reactive, self.audio_analyzer + base_color, i, self.time, self.color_animation ) # Obtenir le type de forme pour cette cellule @@ -470,18 +531,6 @@ def update(self): """Met Ă  jour l'animation""" self.time += self.animation_speed - # Mise Ă  jour de l'intensitĂ© de distorsion basĂ©e sur l'audio - if self.audio_reactive and self.audio_analyzer: - audio_features = self.audio_analyzer.get_audio_features() - - # Les basses frĂ©quences contrĂŽlent l'intensitĂ© de distorsion - bass_boost = min(audio_features['bass_level'] * 0.8, 1.0) # Limiter Ă  1.0 - self.distortion_strength = min(self.base_distortion_strength + bass_boost, 2.0) # Max 2.0 - - # Le volume global contrĂŽle la vitesse d'animation - volume_speed = 1.0 + min(audio_features['overall_volume'] * 2.0, 3.0) # Max 4x speed - self.animation_speed = max(0.005, min(0.02 * volume_speed, 0.1)) # Entre 0.005 et 0.1 - def start_gif_recording(self): """DĂ©marre l'enregistrement GIF""" if self.is_recording: @@ -584,21 +633,21 @@ def run_interactive(self): print("ContrĂŽles:") print("- ESC: Quitter") - print("- I ou TAB: Afficher/masquer l'aide") + print("- I ou TAB: Afficher/masquer l'aide complĂšte") print("- D: Afficher/masquer les infos de statut") print("- F: Basculer plein Ă©cran/fenĂȘtrĂ©") - print("- SPACE: Changer le type de distorsion") - print("- C: Changer le schĂ©ma de couleurs") - + print("- SPACE/Shift+SPACE: Distorsion suivante/prĂ©cĂ©dente") + print("- C/Shift+C: Couleur suivante/prĂ©cĂ©dente") + print("- H/Shift+H: Forme suivante/prĂ©cĂ©dente") + print("- Ctrl+H: Basculer mode formes mixtes") print("- A: Activer/dĂ©sactiver l'animation des couleurs") - print("- M: Activer/dĂ©sactiver la rĂ©activitĂ© audio") - print("- H: Changer le type de forme") - print("- Shift+H: Basculer mode formes mixtes") print("- +/-: Ajuster l'intensitĂ© de distorsion") print("- T puis +/-: Ajuster la densitĂ© de grille (nombre de cellules)") print("- R: RĂ©gĂ©nĂ©rer les paramĂštres alĂ©atoires") print("- S: Sauvegarder l'image") print("- G: DĂ©marrer/arrĂȘter l'enregistrement GIF") + print("- L/Shift+L: ScĂšne suivante/prĂ©cĂ©dente") + print("💡 Utilisez Shift pour naviguer dans le sens inverse!") distortion_types = [t.value for t in DistortionType] current_distortion_index = 0 @@ -634,38 +683,34 @@ def run_interactive(self): status = "affichĂ©" if self.show_status else "masquĂ©" print(f"Affichage du statut: {status}") elif event.key == pygame.K_SPACE: - # Changer le type de distorsion - current_distortion_index = (current_distortion_index + 1) % len(distortion_types) + # Navigation dans les types de distorsion + if pygame.key.get_pressed()[pygame.K_LSHIFT] or pygame.key.get_pressed()[pygame.K_RSHIFT]: + # Shift+SPACE: type de distorsion prĂ©cĂ©dent + current_distortion_index = (current_distortion_index - 1) % len(distortion_types) + else: + # SPACE: type de distorsion suivant + current_distortion_index = (current_distortion_index + 1) % len(distortion_types) self.distortion_fn = distortion_types[current_distortion_index] - print(f"Distorsion: {self.distortion_fn}") + direction = "←" if pygame.key.get_pressed()[pygame.K_LSHIFT] or pygame.key.get_pressed()[pygame.K_RSHIFT] else "→" + print(f"Distorsion {direction}: {self.distortion_fn}") elif event.key == pygame.K_c: - # Changer le schĂ©ma de couleurs - current_color_index = (current_color_index + 1) % len(color_schemes) + # Navigation dans les schĂ©mas de couleurs + if pygame.key.get_pressed()[pygame.K_LSHIFT] or pygame.key.get_pressed()[pygame.K_RSHIFT]: + # Shift+C: schĂ©ma de couleurs prĂ©cĂ©dent + current_color_index = (current_color_index - 1) % len(color_schemes) + else: + # C: schĂ©ma de couleurs suivant + current_color_index = (current_color_index + 1) % len(color_schemes) self.color_scheme = color_schemes[current_color_index] self._generate_base_colors() # RĂ©gĂ©nĂ©rer les couleurs - print(f"SchĂ©ma de couleurs: {self.color_scheme}") + direction = "←" if pygame.key.get_pressed()[pygame.K_LSHIFT] or pygame.key.get_pressed()[pygame.K_RSHIFT] else "→" + print(f"Couleurs {direction}: {self.color_scheme}") elif event.key == pygame.K_a: # Activer/dĂ©sactiver l'animation des couleurs self.color_animation = not self.color_animation status = "activĂ©e" if self.color_animation else "dĂ©sactivĂ©e" print(f"Animation des couleurs: {status}") - elif event.key == pygame.K_m: - # Activer/dĂ©sactiver la rĂ©activitĂ© audio - from distorsion_movement.audio_analyzer import AUDIO_AVAILABLE - if not AUDIO_AVAILABLE: - print("Audio non disponible - installez pyaudio et scipy") - else: - self.audio_reactive = not self.audio_reactive - if self.audio_reactive: - if not self.audio_analyzer: - self.audio_analyzer = AudioAnalyzer() - self.audio_analyzer.start_audio_capture() - print("đŸŽ” Mode audio-rĂ©actif activĂ©!") - else: - if self.audio_analyzer: - self.audio_analyzer.stop_audio_capture() - print("🔇 Mode audio-rĂ©actif dĂ©sactivĂ©") elif event.key == pygame.K_t: # Basculer le mode d'ajustement de la densitĂ© de grille self.grid_density_mode = not self.grid_density_mode @@ -700,20 +745,28 @@ def run_interactive(self): # Sauvegarder self.save_image(f"deformed_grid_{self.distortion_fn}_{int(self.time*100)}.png") print("Image sauvegardĂ©e") - elif event.key == pygame.K_h and (pygame.key.get_pressed()[pygame.K_LSHIFT] or pygame.key.get_pressed()[pygame.K_RSHIFT]): - # Basculer le mode formes mixtes (Shift+H) - doit ĂȘtre testĂ© en PREMIER + elif event.key == pygame.K_h and (pygame.key.get_pressed()[pygame.K_LCTRL] or pygame.key.get_pressed()[pygame.K_RCTRL]): + # Basculer le mode formes mixtes (Ctrl+H) self.mixed_shapes = not self.mixed_shapes self._generate_shape_types() # RĂ©gĂ©nĂ©rer les formes mode = "formes mixtes" if self.mixed_shapes else "forme unique" print(f"Mode: {mode} ({self.shape_type})") elif event.key == pygame.K_h: - # Changer le type de forme (H seul) + # Navigation dans les types de formes shape_types = [s.value for s in ShapeType] current_shape_index = shape_types.index(self.shape_type) if self.shape_type in shape_types else 0 - current_shape_index = (current_shape_index + 1) % len(shape_types) + + if pygame.key.get_pressed()[pygame.K_LSHIFT] or pygame.key.get_pressed()[pygame.K_RSHIFT]: + # Shift+H: type de forme prĂ©cĂ©dent + current_shape_index = (current_shape_index - 1) % len(shape_types) + else: + # H: type de forme suivant + current_shape_index = (current_shape_index + 1) % len(shape_types) + self.shape_type = shape_types[current_shape_index] self._generate_shape_types() # RĂ©gĂ©nĂ©rer les formes - print(f"Forme: {self.shape_type}") + direction = "←" if pygame.key.get_pressed()[pygame.K_LSHIFT] or pygame.key.get_pressed()[pygame.K_RSHIFT] else "→" + print(f"Forme {direction}: {self.shape_type}") elif event.key == pygame.K_f: # Basculer plein Ă©cran self.toggle_fullscreen() @@ -725,6 +778,17 @@ def run_interactive(self): self.stop_gif_recording() else: self.start_gif_recording() + elif event.key == pygame.K_l: + # Navigation dans les scĂšnes sauvegardĂ©es + if pygame.key.get_pressed()[pygame.K_LSHIFT] or pygame.key.get_pressed()[pygame.K_RSHIFT]: + # Shift+L: scĂšne prĂ©cĂ©dente + self.load_previous_scene() + else: + # L: scĂšne suivante + self.load_next_scene() + elif event.key == pygame.K_p: + # Actualiser la liste des scĂšnes sauvegardĂ©es + self.refresh_saved_scenes() self.update() self.render() @@ -742,10 +806,6 @@ def run_interactive(self): import time time.sleep(2) - # Cleanup audio - if self.audio_analyzer: - self.audio_analyzer.stop_audio_capture() - pygame.quit() def toggle_fullscreen(self): @@ -769,10 +829,181 @@ def toggle_fullscreen(self): self._generate_base_positions() def save_image(self, filename: str): - """Sauvegarde l'image actuelle""" + """Sauvegarde l'image actuelle avec ses paramĂštres""" - # Create the folder if it doesn't exist + # Create the folders if they don't exist os.makedirs("images", exist_ok=True) - filename = os.path.join("images", filename) - pygame.image.save(self.screen, filename) - print(f"Image sauvegardĂ©e: {filename}") \ No newline at end of file + os.makedirs("saved_params", exist_ok=True) + + # Save the image + image_path = os.path.join("images", filename) + pygame.image.save(self.screen, image_path) + + # Save the parameters in YAML format + base_name = os.path.splitext(filename)[0] # Remove extension + param_filename = f"{base_name}.yaml" + param_path = os.path.join("saved_params", param_filename) + + self.save_parameters(param_path) + + print(f"Image sauvegardĂ©e: {image_path}") + print(f"ParamĂštres sauvegardĂ©s: {param_path}") + + def get_current_parameters(self) -> dict: + """Retourne tous les paramĂštres actuels de la grille""" + return { + # Core parameters + "dimension": self.dimension, + "cell_size": self.cell_size, + "canvas_size": list(self.canvas_size), + "distortion_strength": self.distortion_strength, + "distortion_fn": self.distortion_fn, + "background_color": list(self.background_color), + "square_color": list(self.square_color), + "color_scheme": self.color_scheme, + "color_animation": self.color_animation, + "shape_type": self.shape_type, + "mixed_shapes": self.mixed_shapes, + + # Animation parameters + "time": self.time, + "animation_speed": self.animation_speed, + + # Display settings + "show_help": self.show_help, + "show_status": self.show_status, + "is_fullscreen": self.is_fullscreen, + "windowed_size": list(self.windowed_size), + + # Grid density settings + "base_dimension": self.base_dimension, + "grid_density_mode": self.grid_density_mode, + + # Grid positioning (calculated automatically but saved for reference) + "offset_x": self.offset_x, + "offset_y": self.offset_y, + + # Metadata + "saved_at": datetime.datetime.now().isoformat(), + "saved_filename": None # Will be set when saving + } + + def save_parameters(self, filepath: str): + """Sauvegarde les paramĂštres actuels dans un fichier YAML""" + params = self.get_current_parameters() + params["saved_filename"] = os.path.basename(filepath) + + with open(filepath, 'w', encoding='utf-8') as f: + yaml.dump(params, f, default_flow_style=False, indent=2, allow_unicode=True) + + def load_parameters(self, filepath: str): + """Charge les paramĂštres depuis un fichier YAML et les applique""" + if not os.path.exists(filepath): + print(f"Fichier de paramĂštres non trouvĂ©: {filepath}") + return False + + try: + with open(filepath, 'r', encoding='utf-8') as f: + params = yaml.safe_load(f) + + # Apply core parameters + self.dimension = params.get("dimension", self.dimension) + self.cell_size = params.get("cell_size", self.cell_size) + self.canvas_size = tuple(params.get("canvas_size", self.canvas_size)) + self.distortion_strength = params.get("distortion_strength", self.distortion_strength) + self.distortion_fn = params.get("distortion_fn", self.distortion_fn) + self.background_color = tuple(params.get("background_color", self.background_color)) + self.square_color = tuple(params.get("square_color", self.square_color)) + self.color_scheme = params.get("color_scheme", self.color_scheme) + self.color_animation = params.get("color_animation", self.color_animation) + self.shape_type = params.get("shape_type", self.shape_type) + self.mixed_shapes = params.get("mixed_shapes", self.mixed_shapes) + + # Apply animation parameters + self.time = params.get("time", self.time) + self.animation_speed = params.get("animation_speed", self.animation_speed) + + # Apply display settings + self.show_help = params.get("show_help", self.show_help) + self.show_status = params.get("show_status", self.show_status) + + # Apply grid density settings + self.base_dimension = params.get("base_dimension", self.base_dimension) + self.grid_density_mode = params.get("grid_density_mode", self.grid_density_mode) + + # Update base distortion strength + self.base_distortion_strength = self.distortion_strength + + # Recalculate offsets to center the grid with new dimensions + grid_total_size = self.dimension * self.cell_size + self.offset_x = (self.canvas_size[0] - grid_total_size) // 2 + self.offset_y = (self.canvas_size[1] - grid_total_size) // 2 + + # Regenerate everything with new parameters + self._generate_base_positions() + self._generate_distortions() + self._generate_base_colors() + self._generate_shape_types() + + print(f"ParamĂštres chargĂ©s depuis: {filepath}") + print(f"Scene: {params.get('distortion_fn', 'N/A')} | {params.get('color_scheme', 'N/A')} | {params.get('shape_type', 'N/A')}") + return True + + except Exception as e: + print(f"Erreur lors du chargement des paramĂštres: {e}") + return False + + def get_saved_scenes(self) -> list: + """Retourne la liste des scĂšnes sauvegardĂ©es (fichiers YAML)""" + if not os.path.exists("saved_params"): + return [] + + yaml_files = glob.glob(os.path.join("saved_params", "*.yaml")) + random.shuffle(yaml_files) + return yaml_files + + def initialize_scene_iteration(self): + """Initialise l'itĂ©ration des scĂšnes sauvegardĂ©es""" + self.saved_scenes = self.get_saved_scenes() + if self.saved_scenes: + self.current_scene_index = 0 + self.scene_iteration_mode = True + print(f"🎬 Mode itĂ©ration de scĂšnes activĂ©! {len(self.saved_scenes)} scĂšne(s) trouvĂ©e(s)") + print("🎼 Utilisez L (suivant) et K (prĂ©cĂ©dent) pour naviguer") + return True + else: + print("đŸš« Aucune scĂšne sauvegardĂ©e trouvĂ©e dans le dossier saved_params/") + return False + + def load_next_scene(self): + """Charge la prochaine scĂšne sauvegardĂ©e""" + if not self.scene_iteration_mode or not self.saved_scenes: + if not self.initialize_scene_iteration(): + return + + self.current_scene_index = (self.current_scene_index + 1) % len(self.saved_scenes) + scene_file = self.saved_scenes[self.current_scene_index] + scene_name = os.path.splitext(os.path.basename(scene_file))[0] + + if self.load_parameters(scene_file): + print(f"🎬 ScĂšne {self.current_scene_index + 1}/{len(self.saved_scenes)}: {scene_name}") + + def load_previous_scene(self): + """Charge la scĂšne prĂ©cĂ©dente sauvegardĂ©e""" + if not self.scene_iteration_mode or not self.saved_scenes: + if not self.initialize_scene_iteration(): + return + + self.current_scene_index = (self.current_scene_index - 1) % len(self.saved_scenes) + scene_file = self.saved_scenes[self.current_scene_index] + scene_name = os.path.splitext(os.path.basename(scene_file))[0] + + if self.load_parameters(scene_file): + print(f"🎬 ScĂšne {self.current_scene_index + 1}/{len(self.saved_scenes)}: {scene_name}") + + def refresh_saved_scenes(self): + """Actualise la liste des scĂšnes sauvegardĂ©es""" + self.saved_scenes = self.get_saved_scenes() + if self.saved_scenes and self.current_scene_index >= len(self.saved_scenes): + self.current_scene_index = 0 + print(f"🔄 Liste des scĂšnes actualisĂ©e: {len(self.saved_scenes)} scĂšne(s) trouvĂ©e(s)") \ No newline at end of file diff --git a/distorsion_movement/demos.py b/distorsion_movement/demos.py index cfd773a..2a6a1d1 100644 --- a/distorsion_movement/demos.py +++ b/distorsion_movement/demos.py @@ -16,7 +16,6 @@ def create_deformed_grid(dimension: int = 64, distortion_fn: str = "random", color_scheme: str = "rainbow", color_animation: bool = False, - audio_reactive: bool = False, fullscreen: bool = False, shape_type: str = "square", mixed_shapes: bool = False) -> DeformedGrid: @@ -30,7 +29,6 @@ def create_deformed_grid(dimension: int = 64, distortion_fn: Type de distorsion ("random", "sine", "perlin", "circular") color_scheme: SchĂ©ma de couleurs ("monochrome", "gradient", "rainbow", etc.) 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 shape_type: Type de forme Ă  utiliser ("square", "circle", "triangle", etc.) mixed_shapes: Si True, utilise diffĂ©rentes formes dans la grille @@ -42,7 +40,8 @@ def create_deformed_grid(dimension: int = 64, # Pour le plein Ă©cran, utiliser une taille de fenĂȘtre temporaire canvas_size = (900, 900) else: - canvas_size = (dimension * cell_size + 100, dimension * cell_size + 100) + grow_factor = 1.3 + canvas_size = (dimension * cell_size * grow_factor, dimension * cell_size * grow_factor) grid = DeformedGrid( dimension=dimension, @@ -52,7 +51,6 @@ def create_deformed_grid(dimension: int = 64, distortion_fn=distortion_fn, color_scheme=color_scheme, color_animation=color_animation, - audio_reactive=audio_reactive, shape_type=shape_type, mixed_shapes=mixed_shapes ) @@ -77,9 +75,9 @@ def fullscreen_demo(): dimension=32, cell_size=20, distortion_strength=1, - distortion_fn="circular", - color_scheme="infrared_thermal", - color_animation=True, + distortion_fn="hypno_spiral_pulse", + color_scheme="candy_shop", + color_animation=True, fullscreen=False, shape_type="square", # Commencer avec des carrĂ©s mixed_shapes=False # Formes mixtes activĂ©es @@ -91,119 +89,9 @@ def fullscreen_demo(): print("🎹 Les nouvelles formes rĂ©agissent aux couleurs et distorsions!") grid.run_interactive() - -def star_demo(): - """DĂ©monstration avec seulement des Ă©toiles - Magique! ⭐""" - grid = create_deformed_grid( - dimension=64, - cell_size=18, - distortion_strength=0.7, - distortion_fn="circular", - color_scheme="fire", - color_animation=True, - fullscreen=False, - shape_type="star", # SEULEMENT des Ă©toiles - mixed_shapes=False # Forme unique - ) - print("\n⭐ DÉMO ÉTOILES SEULEMENT!") - print("🌟 Toutes les cellules sont des Ă©toiles dorĂ©es") - print("🎼 Utilisez 'H' pour changer vers d'autres formes") - grid.run_interactive() - - -def hexagon_demo(): - """DĂ©monstration avec seulement des hexagones - GĂ©omĂ©trique! đŸ”¶""" - grid = create_deformed_grid( - dimension=60, - cell_size=16, - distortion_strength=0.5, - distortion_fn="perlin", - color_scheme="ocean", - color_animation=True, - fullscreen=False, - shape_type="hexagon", # SEULEMENT des hexagones - mixed_shapes=False # Forme unique - ) - print("\nđŸ”¶ DÉMO HEXAGONES SEULEMENT!") - print("🏯 Pattern gĂ©omĂ©trique uniforme avec hexagones") - print("🎼 Utilisez 'H' pour explorer d'autres formes") - grid.run_interactive() - - -def triangle_demo(): - """DĂ©monstration avec seulement des triangles - Tribal! đŸ”ș""" - grid = create_deformed_grid( - dimension=70, - cell_size=14, - distortion_strength=0.9, - distortion_fn="random", - color_scheme="complementary", - color_animation=True, - fullscreen=False, - shape_type="triangle", # SEULEMENT des triangles - mixed_shapes=False # Forme unique - ) - print("\nđŸ”ș DÉMO TRIANGLES SEULEMENT!") - print("⚡ Style tribal avec triangles dynamiques") - print("🎼 Utilisez 'H' pour tester autres formes") - grid.run_interactive() - - -def shapes_showcase_demo(): - """DĂ©monstration spĂ©ciale pour mettre en valeur toutes les formes disponibles 🎭""" - grid = create_deformed_grid( - dimension=64, - cell_size=18, - distortion_strength=0.6, - distortion_fn="circular", - color_scheme="rainbow", - color_animation=True, - fullscreen=False, - shape_type="star", - mixed_shapes=True - ) - print("\n🎭 VITRINE DES FORMES!") - print("🔄 Cette dĂ©mo affiche toutes les formes en mode mixte") - print("🎹 Essayez 'H' pour changer de forme principale") - print("đŸŽČ 'Shift+H' pour basculer en mode forme unique") - print("🌈 Toutes les formes supportent couleurs et animations!") - grid.run_interactive() - - -def audio_reactive_demo(): - """DĂ©monstration avec rĂ©activitĂ© audio - PARFAIT POUR LA MUSIQUE! đŸŽ”""" - grid = create_deformed_grid( - dimension=64, - cell_size=18, - distortion_strength=1, # Distorsion de base plus faible (l'audio l'augmente) - distortion_fn="sine", - color_scheme="neon", - color_animation=True, - audio_reactive=True, - fullscreen=True, - shape_type="hexagon", # Commencer avec des hexagones - mixed_shapes=False # Forme unique pour un effet cohĂ©rent - ) - 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!") - print("đŸ”· Forme: hexagones (utilisez 'H' pour changer)") - 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() - Cercles en plein Ă©cran") - print("3. star_demo() - Seulement des Ă©toiles ⭐") - print("4. hexagon_demo() - Seulement des hexagones đŸ”¶") - print("5. triangle_demo() - Seulement des triangles đŸ”ș") - print("6. shapes_showcase_demo() - Vitrine formes mixtes") - print("7. audio_reactive_demo() - DĂ©monstration audio-rĂ©active") - print("\nđŸ”· FORMES UNIQUES vs MIXTES:") - print(" ✹ DĂ©mos 2-5: UNE SEULE forme (mixed_shapes=False)") - print(" đŸŽČ DĂ©mo 6: FORMES MIXTES (mixed_shapes=True)") - print(" 🎼 Dans toutes les dĂ©mos: 'H' change la forme, 'Shift+H' bascule le mode") - print("\nLancement de la dĂ©monstration Ă©toiles...") fullscreen_demo() \ No newline at end of file diff --git a/distorsion_movement/distortions.py b/distorsion_movement/distortions.py index 7e1d307..2be8d32 100644 --- a/distorsion_movement/distortions.py +++ b/distorsion_movement/distortions.py @@ -1234,6 +1234,133 @@ def n2(px, py, tt): return (new_x, new_y, rotation) + @staticmethod + def apply_distortion_hypno_spiral_pulse( + base_pos: Tuple[float, float], + params: dict, + cell_size: int, + distortion_strength: float, + time: float, + canvas_size: Tuple[int, int] + ) -> Tuple[float, float, float]: + """ + Hypno Spiral Pulse: + - Base spiral (radius-based angular twist). + - Pulse modulates ANGLE (not radius) so the whole spiral "breathes". + - Spin reverses direction halfway through the pulse cycle (smoothly). + - Optional micro-noise for a slight jelly feel. + """ + import math, random + + x, y = base_pos + cx, cy = canvas_size[0] * 0.5, canvas_size[1] * 0.5 + + # ---------- Defaults (override via params) ---------- + p = { + "spin_speed": 0.18, # overall spin intensity (radians per sec scale) + "twist_amount": 1.05, # spiral twist strength (radius -> angle) + "pulse_freq": 0.6, # pulses per second + "pulse_strength": 0.85, # how strongly the pulse pushes the angle + "pulse_rad_pow": 0.85, # how pulse scales with radius (0..1 softer, >1 stronger at edges) + "noise_amount": 0.12, # 0..1 tiny displacement to soften banding + "noise_scale": 0.02, # spatial scale for the micro-noise + "noise_speed": 0.25, # temporal speed for noise + "max_disp_frac": 0.30 # cap for noise displacement as frac of cell_size + } + if params: + p.update(params) + + spin_speed = p["spin_speed"] + twist_amount = p["twist_amount"] + pulse_freq = max(0.01, p["pulse_freq"]) + pulse_strength = p["pulse_strength"] + pulse_rad_pow = max(0.0, p["pulse_rad_pow"]) + noise_amount = max(0.0, min(1.0, p["noise_amount"])) + noise_scale = p["noise_scale"] + noise_speed = p["noise_speed"] + max_disp_frac = p["max_disp_frac"] + + # Stable per-cell noise offsets + if "noise_ox" not in p: + p["noise_ox"] = random.uniform(0, 1000) + p["noise_oy"] = random.uniform(0, 1000) + ox, oy = p["noise_ox"], p["noise_oy"] + + # ---------- Polar coords ---------- + dx, dy = x - cx, y - cy + r = math.hypot(dx, dy) + if r == 0: + return (x, y, 0.0) + + theta = math.atan2(dy, dx) # [-pi, pi] + if theta < 0: + theta += 2 * math.pi # [0, 2pi) + + # ---------- Spiral twist ---------- + # radial grows from center to corner-diagonal + max_r = math.hypot(cx, cy) + radial = min(1.0, r / (max_r + 1e-6)) + twist = twist_amount * distortion_strength * radial # base spiral warp + + # ---------- Pulse on ANGLE + Spin reversal ---------- + # phase: 0..2pi every pulse + phase = 2 * math.pi * pulse_freq * time + + # Smooth "flip": use sin(phase) as an integrated spin term. + # This produces spin that moves one way, then reverses halfway (phase=pi). + # It’s smooth (no discontinuity) and hits zero velocity at the turning points. + spin = (2 * math.pi * spin_speed) * math.sin(phase) * distortion_strength + + # Pulse envelope 0..1 (ease-in-out via cosine). Drives *angle* displacement. + pulse_env = 0.5 * (1.0 - math.cos(phase)) # 0 at start, 1 mid-pulse, 0 end + angle_pulse = ( + pulse_strength + * distortion_strength + * (radial ** pulse_rad_pow) + * pulse_env + ) + + # Total new angle + new_theta = theta + twist + spin + angle_pulse + + # Keep radius (pulse only affects angle) + base_new_x = cx + math.cos(new_theta) * r + base_new_y = cy + math.sin(new_theta) * r + + # ---------- Micro-noise (subtle jelly to avoid too-clean bands) ---------- + def n2(px, py, tt): + return ( + math.sin(px) * math.cos(py * 1.31) + + math.sin(px * 0.7 + tt) * math.cos(py * 0.9 - tt * 1.1) + ) + + tt = time * noise_speed + px = (base_new_x + ox) * noise_scale + py = (base_new_y + oy) * noise_scale + + nx = n2(px, py, tt) * 0.7 + n2(px * 2.0, py * 2.0, tt * 1.7) * 0.25 + n2(px * 4.0, py * 4.0, tt * 2.5) * 0.08 + ny = n2(py + 5.2, px - 3.7, tt + 1.3) * 0.7 + n2(py * 2.0 + 5.2, px * 2.0 - 3.7, tt * 1.7) * 0.25 + n2(py * 4.0 + 5.2, px * 4.0 - 3.7, tt * 2.5) * 0.08 + + mag = math.hypot(nx, ny) + if mag > 1e-6: + nx /= mag + ny /= mag + + max_offset = cell_size * max_disp_frac * noise_amount * distortion_strength + new_x = base_new_x + nx * max_offset + new_y = base_new_y + ny * max_offset + + # ---------- Rotation of the square ---------- + # Tie to instantaneous angular motion: base twist + spin rate proxy (cos phase ~ d/dt sin) + # plus a touch of pulse envelope. + rotation = ( + 0.18 * twist + + 0.14 * (2 * math.pi * spin_speed) * math.cos(phase) * distortion_strength + + 0.12 * angle_pulse + ) + + return (new_x, new_y, rotation) + @staticmethod @@ -1342,6 +1469,10 @@ def get_distorted_positions(base_positions: List[Tuple[float, float]], pos = DistortionEngine.apply_distortion_kaleidoscope_twist( base_pos, params, cell_size, distortion_strength, time, canvas_size ) + elif distortion_fn == DistortionType.HYPNO_SPIRAL_PULSE.value: + pos = DistortionEngine.apply_distortion_hypno_spiral_pulse( + base_pos, params, cell_size, distortion_strength, time, canvas_size + ) 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 31890b6..149c848 100644 --- a/distorsion_movement/enums.py +++ b/distorsion_movement/enums.py @@ -3,6 +3,7 @@ """ from enum import Enum +from token import ELLIPSIS class DistortionType(Enum): @@ -27,6 +28,8 @@ class DistortionType(Enum): FRACTAL_NOISE = "fractal_noise" MOIRE = "moire" KALEIDOSCOPE_TWIST = "kaleidoscope_twist" + HYPNO_SPIRAL_PULSE = "hypno_spiral_pulse" + class ColorScheme(Enum): @@ -37,16 +40,20 @@ class ColorScheme(Enum): GRADIENT = "gradient" RAINBOW = "rainbow" COMPLEMENTARY = "complementary" - # TEMPERATURE = "temperature" PASTEL = "pastel" NEON = "neon" - # OCEAN = "ocean" - # FIRE = "fire" - # FOREST = "forest" ANALOGUOUS = "analogous" CYBERPUNK = "cyberpunk" AURORA_BOREALIS = "aurora_borealis" INFRARED_THERMAL = "infrared_thermal" + DUOTONE_ACCENT = "duotone_accent" + DESERT = "desert" + METALLICS = "metallics" + REGGAE = "reggae" + SUNSET = "sunset" + POP_ART = "pop_art" + VAPORWAVE = "vaporwave" + CANDY_SHOP = "candy_shop" class ShapeType(Enum): @@ -58,4 +65,8 @@ class ShapeType(Enum): STAR = "star" PENTAGON = "pentagon" DIAMOND = "diamond" - KOCH_SNOWFLAKE = "koch_snowflake" \ No newline at end of file + KOCH_SNOWFLAKE = "koch_snowflake" + RING = "ring" + YIN_YANG = "yin_yang" + LEAF = "leaf" + ELLIPSIS = "ellipsis" \ No newline at end of file diff --git a/distorsion_movement/shapes.py b/distorsion_movement/shapes.py deleted file mode 100644 index 0524612..0000000 --- a/distorsion_movement/shapes.py +++ /dev/null @@ -1,359 +0,0 @@ -""" -Moteur de rendu pour diffĂ©rents types de formes gĂ©omĂ©triques. - -Ce module fournit des fonctions pour dessiner diverses formes gĂ©omĂ©triques -avec support de la rotation, du dimensionnement et du positionnement. -""" - -import pygame -import math -import numpy as np -from typing import Tuple, List - - -class ShapeRenderer: - """Moteur de rendu pour diffĂ©rentes formes gĂ©omĂ©triques.""" - - @staticmethod - def draw_square(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): - """ - Dessine un carrĂ© avec rotation. - - Args: - surface: Surface pygame oĂč dessiner - x, y: Position du centre - rotation: Rotation en radians - size: Taille du carrĂ© - color: Couleur RGB - """ - half_size = size // 2 - corners = [ - (-half_size, -half_size), - (half_size, -half_size), - (half_size, half_size), - (-half_size, half_size) - ] - - rotated_corners = ShapeRenderer._rotate_points(corners, rotation, x, y) - - if len(rotated_corners) >= 3: - try: - pygame.draw.polygon(surface, color, rotated_corners) - except (TypeError, ValueError): - # Fallback: dessiner un petit rectangle centrĂ© - rect = pygame.Rect(int(x) - 2, int(y) - 2, 4, 4) - pygame.draw.rect(surface, color, rect) - - @staticmethod - def draw_circle(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): - """ - Dessine un cercle (la rotation n'affecte pas un cercle parfait). - - Args: - surface: Surface pygame oĂč dessiner - x, y: Position du centre - rotation: Rotation en radians (ignorĂ©e pour les cercles) - size: DiamĂštre du cercle - color: Couleur RGB - """ - try: - radius = max(1, size // 2) - pygame.draw.circle(surface, color, (int(x), int(y)), radius) - except (TypeError, ValueError): - # Fallback: dessiner un petit rectangle centrĂ© - rect = pygame.Rect(int(x) - 2, int(y) - 2, 4, 4) - pygame.draw.rect(surface, color, rect) - - @staticmethod - def draw_triangle(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): - """ - Dessine un triangle Ă©quilatĂ©ral avec rotation. - - Args: - surface: Surface pygame oĂč dessiner - x, y: Position du centre - rotation: Rotation en radians - size: Taille du triangle (rayon du cercle circonscrit) - color: Couleur RGB - """ - radius = size // 2 - # Triangle Ă©quilatĂ©ral avec un sommet vers le haut - corners = [] - for i in range(3): - angle = (2 * math.pi * i / 3) - math.pi/2 # Commencer par le haut - corner_x = radius * math.cos(angle) - corner_y = radius * math.sin(angle) - corners.append((corner_x, corner_y)) - - rotated_corners = ShapeRenderer._rotate_points(corners, rotation, x, y) - - if len(rotated_corners) >= 3: - try: - pygame.draw.polygon(surface, color, rotated_corners) - except (TypeError, ValueError): - # Fallback - rect = pygame.Rect(int(x) - 2, int(y) - 2, 4, 4) - pygame.draw.rect(surface, color, rect) - - @staticmethod - def draw_hexagon(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): - """ - Dessine un hexagone rĂ©gulier avec rotation. - - Args: - surface: Surface pygame oĂč dessiner - x, y: Position du centre - rotation: Rotation en radians - size: Taille de l'hexagone (rayon du cercle circonscrit) - color: Couleur RGB - """ - radius = size // 2 - corners = [] - for i in range(6): - angle = 2 * math.pi * i / 6 - corner_x = radius * math.cos(angle) - corner_y = radius * math.sin(angle) - corners.append((corner_x, corner_y)) - - rotated_corners = ShapeRenderer._rotate_points(corners, rotation, x, y) - - if len(rotated_corners) >= 3: - try: - pygame.draw.polygon(surface, color, rotated_corners) - except (TypeError, ValueError): - # Fallback - rect = pygame.Rect(int(x) - 2, int(y) - 2, 4, 4) - pygame.draw.rect(surface, color, rect) - - @staticmethod - def draw_pentagon(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): - """ - Dessine un pentagone rĂ©gulier avec rotation. - - Args: - surface: Surface pygame oĂč dessiner - x, y: Position du centre - rotation: Rotation en radians - size: Taille du pentagone (rayon du cercle circonscrit) - color: Couleur RGB - """ - radius = size // 2 - corners = [] - for i in range(5): - angle = (2 * math.pi * i / 5) - math.pi/2 # Commencer par le haut - corner_x = radius * math.cos(angle) - corner_y = radius * math.sin(angle) - corners.append((corner_x, corner_y)) - - rotated_corners = ShapeRenderer._rotate_points(corners, rotation, x, y) - - if len(rotated_corners) >= 3: - try: - pygame.draw.polygon(surface, color, rotated_corners) - except (TypeError, ValueError): - # Fallback - rect = pygame.Rect(int(x) - 2, int(y) - 2, 4, 4) - pygame.draw.rect(surface, color, rect) - - @staticmethod - def draw_star(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): - """ - Dessine une Ă©toile Ă  5 branches avec rotation. - - Args: - surface: Surface pygame oĂč dessiner - x, y: Position du centre - rotation: Rotation en radians - size: Taille de l'Ă©toile (rayon du cercle circonscrit externe) - color: Couleur RGB - """ - outer_radius = size // 2 - inner_radius = outer_radius * 0.4 # Rayon interne plus petit - - corners = [] - for i in range(10): # 5 points externes + 5 points internes - angle = (2 * math.pi * i / 10) - math.pi/2 # Commencer par le haut - if i % 2 == 0: # Points externes - radius = outer_radius - else: # Points internes - radius = inner_radius - - corner_x = radius * math.cos(angle) - corner_y = radius * math.sin(angle) - corners.append((corner_x, corner_y)) - - rotated_corners = ShapeRenderer._rotate_points(corners, rotation, x, y) - - if len(rotated_corners) >= 3: - try: - pygame.draw.polygon(surface, color, rotated_corners) - except (TypeError, ValueError): - # Fallback - rect = pygame.Rect(int(x) - 2, int(y) - 2, 4, 4) - pygame.draw.rect(surface, color, rect) - - @staticmethod - def draw_diamond(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): - """ - Dessine un losange (carrĂ© Ă  45°) avec rotation. - - Args: - surface: Surface pygame oĂč dessiner - x, y: Position du centre - rotation: Rotation en radians - size: Taille du losange - color: Couleur RGB - """ - half_size = size // 2 - # Losange = carrĂ© tournĂ© de 45° - base_rotation = math.pi / 4 # 45 degrĂ©s - total_rotation = rotation + base_rotation - - corners = [ - (-half_size, -half_size), - (half_size, -half_size), - (half_size, half_size), - (-half_size, half_size) - ] - - rotated_corners = ShapeRenderer._rotate_points(corners, total_rotation, x, y) - - if len(rotated_corners) >= 3: - try: - pygame.draw.polygon(surface, color, rotated_corners) - except (TypeError, ValueError): - # Fallback - rect = pygame.Rect(int(x) - 2, int(y) - 2, 4, 4) - pygame.draw.rect(surface, color, rect) - - @staticmethod - def _rotate_points(points: List[Tuple[float, float]], rotation: float, - center_x: float, center_y: float) -> List[Tuple[int, int]]: - """ - Applique une rotation Ă  une liste de points autour d'un centre. - - Args: - points: Liste de tuples (x, y) relatifs au centre - rotation: Angle de rotation en radians - center_x, center_y: Centre de rotation - - Returns: - Liste de points rotĂ©s en coordonnĂ©es absolues (entiers) - """ - rotated_points = [] - cos_r = math.cos(rotation) - sin_r = math.sin(rotation) - - for point_x, point_y in points: - new_x = point_x * cos_r - point_y * sin_r + center_x - new_y = point_x * sin_r + point_y * cos_r + center_y - - # Validation des coordonnĂ©es (Ă©viter NaN/Inf) - if math.isfinite(new_x) and math.isfinite(new_y): - rotated_points.append((int(new_x), int(new_y))) - else: - # Fallback vers la position centrale si coordonnĂ©es invalides - rotated_points.append((int(center_x), int(center_y))) - - return rotated_points - - @staticmethod - def draw_koch_snowflake(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int], depth: int = 3): - """ - Dessine un flocon de Koch (contour) autour d'un triangle Ă©quilatĂ©ral. - - Args: - surface: Surface pygame oĂč dessiner - x, y: Position du centre - rotation: Rotation en radians (appliquĂ©e Ă  la forme finale) - size: Taille (rayon du cercle circonscrit du triangle de base) - color: Couleur RGB - depth: Profondeur de rĂ©cursion (0-6 recommandĂ©) - """ - # SĂ©curitĂ© / limites - depth = max(0, min(int(depth), 6)) # profondeur raisonnable pour Ă©viter trop de points - radius = max(2, size // 2) - - # Triangle Ă©quilatĂ©ral centrĂ©, pointant vers le haut (comme vos autres formes) - base_angles = [ - -math.pi / 2, - -math.pi / 2 + 2 * math.pi / 3, - -math.pi / 2 + 4 * math.pi / 3, - ] - base_pts = [(radius * math.cos(a), radius * math.sin(a)) for a in base_angles] - - # GĂ©nĂšre la courbe de Koch pour chaque cĂŽtĂ© du triangle - def koch_curve(p1, p2, d): - if d == 0: - return [p1, p2] - # points 1/3 et 2/3 sur le segment - x1, y1 = p1 - x2, y2 = p2 - dx, dy = (x2 - x1), (y2 - y1) - pA = (x1 + dx / 3.0, y1 + dy / 3.0) - pB = (x1 + 2.0 * dx / 3.0, y1 + 2.0 * dy / 3.0) - # sommet du "pic" (Ă©quilateral) : rotation +60° du segment pA->pB - angle60 = math.pi / 3.0 - ux, uy = (pB[0] - pA[0]), (pB[1] - pA[1]) - # rotation (ux,uy) de +60° - vx = ux * math.cos(angle60) - uy * math.sin(angle60) - vy = ux * math.sin(angle60) + uy * math.cos(angle60) - pC = (pA[0] + vx, pA[1] + vy) - - # rĂ©curse sur 4 segments - seg1 = koch_curve(p1, pA, d - 1) - seg2 = koch_curve(pA, pC, d - 1) - seg3 = koch_curve(pC, pB, d - 1) - seg4 = koch_curve(pB, p2, d - 1) - - # concatĂ©ner en Ă©vitant de dupliquer les points de jonction - return seg1[:-1] + seg2[:-1] + seg3[:-1] + seg4 - - # Construire la liste de points pour le flocon (fermĂ©) - pts = [] - for i in range(3): - p1 = base_pts[i] - p2 = base_pts[(i + 1) % 3] - edge_pts = koch_curve(p1, p2, depth) - if i < 2: - pts += edge_pts[:-1] # Ă©viter duplication du dernier point - else: - pts += edge_pts # dernier cĂŽtĂ© garde le dernier point pour fermer - - # Appliquer rotation et translation via votre helper - rotated = ShapeRenderer._rotate_points(pts, rotation, x, y) - - # Dessin : polyligne fermĂ©e (contour 1 px) - try: - if len(rotated) >= 2: - pygame.draw.lines(surface, color, True, rotated, 1) - else: - rect = pygame.Rect(int(x) - 2, int(y) - 2, 4, 4) - pygame.draw.rect(surface, color, rect) - except (TypeError, ValueError): - rect = pygame.Rect(int(x) - 2, int(y) - 2, 4, 4) - pygame.draw.rect(surface, color, rect) - - -def get_shape_renderer_function(shape_type: str): - """ - Retourne la fonction de rendu correspondant au type de forme. - - Args: - shape_type: Type de forme (valeur de ShapeType) - - Returns: - Fonction de rendu appropriĂ©e - """ - shape_functions = { - "square": ShapeRenderer.draw_square, - "circle": ShapeRenderer.draw_circle, - "triangle": ShapeRenderer.draw_triangle, - "hexagon": ShapeRenderer.draw_hexagon, - "pentagon": ShapeRenderer.draw_pentagon, - "star": ShapeRenderer.draw_star, - "diamond": ShapeRenderer.draw_diamond, - "koch_snowflake": ShapeRenderer.draw_koch_snowflake, - } - - return shape_functions.get(shape_type, ShapeRenderer.draw_square) \ No newline at end of file diff --git a/distorsion_movement/shapes/__init__.py b/distorsion_movement/shapes/__init__.py new file mode 100644 index 0000000..c4507f7 --- /dev/null +++ b/distorsion_movement/shapes/__init__.py @@ -0,0 +1,58 @@ +""" +Package de formes gĂ©omĂ©triques. + +Ce package contient tous les moteurs de rendu pour diffĂ©rentes formes +gĂ©omĂ©triques utilisĂ©es dans le systĂšme de grilles dĂ©formĂ©es. +""" + +from .base_shape import BaseShape +from .square import Square +from .circle import Circle +from .triangle import Triangle +from .hexagon import Hexagon +from .pentagon import Pentagon +from .star import Star +from .diamond import Diamond +from .koch_snowflake import KochSnowflake +from .ring import Ring +from .yin_yang import YinYang +from .leaf import Leaf +from .ellipsis import Ellipsis + +# Registre des formes disponibles +SHAPE_REGISTRY = { + "square": Square.draw, + "circle": Circle.draw, + "triangle": Triangle.draw, + "hexagon": Hexagon.draw, + "pentagon": Pentagon.draw, + "star": Star.draw, + "diamond": Diamond.draw, + "koch_snowflake": KochSnowflake.draw, + "ring": Ring.draw, + "yin_yang": YinYang.draw, + "leaf": Leaf.draw, + "ellipsis": Ellipsis.draw, +} + + +def get_shape_renderer_function(shape_type: str): + """ + Retourne la fonction de rendu correspondant au type de forme. + + Args: + shape_type: Type de forme (valeur de ShapeType) + + Returns: + Fonction de rendu appropriĂ©e + """ + return SHAPE_REGISTRY.get(shape_type, Square.draw) + +__all__ = [ + 'BaseShape', + 'Square', 'Circle', 'Triangle', 'Hexagon', 'Pentagon', + 'Star', 'Diamond', 'KochSnowflake', 'Ring', 'YinYang', 'Leaf', + 'Ellipsis', + 'get_shape_renderer_function', + 'SHAPE_REGISTRY' +] \ No newline at end of file diff --git a/distorsion_movement/shapes/base_shape.py b/distorsion_movement/shapes/base_shape.py new file mode 100644 index 0000000..2c04aeb --- /dev/null +++ b/distorsion_movement/shapes/base_shape.py @@ -0,0 +1,58 @@ +""" +Module de base pour les formes gĂ©omĂ©triques. + +Ce module contient les fonctionnalitĂ©s communes Ă  toutes les formes, +notamment la rotation des points et les fonctions utilitaires. +""" + +import pygame +import math +from typing import Tuple, List + + +class BaseShape: + """Classe de base pour toutes les formes gĂ©omĂ©triques.""" + + @staticmethod + def _rotate_points(points: List[Tuple[float, float]], rotation: float, + center_x: float, center_y: float) -> List[Tuple[int, int]]: + """ + Applique une rotation Ă  une liste de points autour d'un centre. + + Args: + points: Liste de tuples (x, y) relatifs au centre + rotation: Angle de rotation en radians + center_x, center_y: Centre de rotation + + Returns: + Liste de points rotĂ©s en coordonnĂ©es absolues (entiers) + """ + rotated_points = [] + cos_r = math.cos(rotation) + sin_r = math.sin(rotation) + + for point_x, point_y in points: + new_x = point_x * cos_r - point_y * sin_r + center_x + new_y = point_x * sin_r + point_y * cos_r + center_y + + # Validation des coordonnĂ©es (Ă©viter NaN/Inf) + if math.isfinite(new_x) and math.isfinite(new_y): + rotated_points.append((int(new_x), int(new_y))) + else: + # Fallback vers la position centrale si coordonnĂ©es invalides + rotated_points.append((int(center_x), int(center_y))) + + return rotated_points + + @staticmethod + def _draw_fallback(surface, x: float, y: float, color: Tuple[int, int, int]): + """ + Dessine un petit rectangle comme fallback en cas d'erreur. + + Args: + surface: Surface pygame oĂč dessiner + x, y: Position du centre + color: Couleur RGB + """ + rect = pygame.Rect(int(x) - 2, int(y) - 2, 4, 4) + pygame.draw.rect(surface, color, rect) \ No newline at end of file diff --git a/distorsion_movement/shapes/circle.py b/distorsion_movement/shapes/circle.py new file mode 100644 index 0000000..5662790 --- /dev/null +++ b/distorsion_movement/shapes/circle.py @@ -0,0 +1,30 @@ +""" +Rendu de forme circulaire. +""" + +import pygame +from typing import Tuple +from .base_shape import BaseShape + + +class Circle(BaseShape): + """Moteur de rendu pour les cercles.""" + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): + """ + Dessine un cercle (la rotation n'affecte pas un cercle parfait). + + Args: + surface: Surface pygame oĂč dessiner + x, y: Position du centre + rotation: Rotation en radians (ignorĂ©e pour les cercles) + size: DiamĂštre du cercle + color: Couleur RGB + """ + try: + radius = max(1, size // 2) + pygame.draw.circle(surface, color, (int(x), int(y)), radius) + except (TypeError, ValueError): + # Fallback: dessiner un petit rectangle centrĂ© + Circle._draw_fallback(surface, x, y, color) \ No newline at end of file diff --git a/distorsion_movement/shapes/diamond.py b/distorsion_movement/shapes/diamond.py new file mode 100644 index 0000000..8ccc6bf --- /dev/null +++ b/distorsion_movement/shapes/diamond.py @@ -0,0 +1,45 @@ +""" +Rendu de forme en losange. +""" + +import pygame +import math +from typing import Tuple +from .base_shape import BaseShape + + +class Diamond(BaseShape): + """Moteur de rendu pour les losanges.""" + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): + """ + Dessine un losange (carrĂ© Ă  45°) avec rotation. + + Args: + surface: Surface pygame oĂč dessiner + x, y: Position du centre + rotation: Rotation en radians + size: Taille du losange + color: Couleur RGB + """ + half_size = size // 2 + # Losange = carrĂ© tournĂ© de 45° + base_rotation = math.pi / 4 # 45 degrĂ©s + total_rotation = rotation + base_rotation + + corners = [ + (-half_size, -half_size), + (half_size, -half_size), + (half_size, half_size), + (-half_size, half_size) + ] + + rotated_corners = Diamond._rotate_points(corners, total_rotation, x, y) + + if len(rotated_corners) >= 3: + try: + pygame.draw.polygon(surface, color, rotated_corners) + except (TypeError, ValueError): + # Fallback + Diamond._draw_fallback(surface, x, y, color) \ No newline at end of file diff --git a/distorsion_movement/shapes/ellipsis.py b/distorsion_movement/shapes/ellipsis.py new file mode 100644 index 0000000..b0caf56 --- /dev/null +++ b/distorsion_movement/shapes/ellipsis.py @@ -0,0 +1,53 @@ +import math +import pygame +from typing import Tuple +from .base_shape import BaseShape + + +class Ellipsis(BaseShape): + """Moteur de rendu pour une ellipse (cercle Ă©tirĂ©) avec rotation.""" + + @staticmethod + def _invert_color(c: Tuple[int, int, int]) -> Tuple[int, int, int]: + r, g, b = c + return (255 - r, 255 - g, 255 - b) + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): + """ + Dessine une ellipse centrĂ©e en (x, y). + + Args: + surface: Surface pygame oĂč dessiner. + x, y: Position du centre. + rotation: Rotation en radians. + size: Axe majeur (diamĂštre principal) de l’ellipse. + color: Couleur de remplissage (RGB). + """ + try: + major = max(2, int(size)) # axe majeur + # Ratio d’aspect (minor/major). 1.0 = cercle, 0.6 = ellipse aplatie + # Si ton moteur a un systĂšme d'extras (ex: BaseShape.extras.get("aspect")), + # tu peux le brancher ici. + aspect = 0.6 + minor = max(1, int(major * aspect)) + + # surface temporaire pile Ă  la taille de l'ellipse + temp = pygame.Surface((major, minor), pygame.SRCALPHA).convert_alpha() + + # ellipse pleine + rect = pygame.Rect(0, 0, major, minor) + pygame.draw.ellipse(temp, color, rect) + + # (optionnel) lĂ©ger contour anti-alias pour un bord plus net + outline = Ellipsis._invert_color(color) + pygame.draw.ellipse(temp, outline, rect, width=1) + + # rotation + blit centrĂ© + deg = math.degrees(rotation or 0.0) + rotated = pygame.transform.rotozoom(temp, -deg, 1.0) + rect_out = rotated.get_rect(center=(int(x), int(y))) + surface.blit(rotated, rect_out.topleft) + + except (TypeError, ValueError): + Ellipsis._draw_fallback(surface, x, y, color) diff --git a/distorsion_movement/shapes/hexagon.py b/distorsion_movement/shapes/hexagon.py new file mode 100644 index 0000000..3dbf5fa --- /dev/null +++ b/distorsion_movement/shapes/hexagon.py @@ -0,0 +1,41 @@ +""" +Rendu de forme hexagonale. +""" + +import pygame +import math +from typing import Tuple +from .base_shape import BaseShape + + +class Hexagon(BaseShape): + """Moteur de rendu pour les hexagones.""" + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): + """ + Dessine un hexagone rĂ©gulier avec rotation. + + Args: + surface: Surface pygame oĂč dessiner + x, y: Position du centre + rotation: Rotation en radians + size: Taille de l'hexagone (rayon du cercle circonscrit) + color: Couleur RGB + """ + radius = size // 2 + corners = [] + for i in range(6): + angle = 2 * math.pi * i / 6 + corner_x = radius * math.cos(angle) + corner_y = radius * math.sin(angle) + corners.append((corner_x, corner_y)) + + rotated_corners = Hexagon._rotate_points(corners, rotation, x, y) + + if len(rotated_corners) >= 3: + try: + pygame.draw.polygon(surface, color, rotated_corners) + except (TypeError, ValueError): + # Fallback + Hexagon._draw_fallback(surface, x, y, color) \ No newline at end of file diff --git a/distorsion_movement/shapes/koch_snowflake.py b/distorsion_movement/shapes/koch_snowflake.py new file mode 100644 index 0000000..873c9b2 --- /dev/null +++ b/distorsion_movement/shapes/koch_snowflake.py @@ -0,0 +1,87 @@ +""" +Rendu de flocon de Koch. +""" + +import pygame +import math +from typing import Tuple +from .base_shape import BaseShape + + +class KochSnowflake(BaseShape): + """Moteur de rendu pour les flocons de Koch.""" + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int], depth: int = 3): + """ + Dessine un flocon de Koch (contour) autour d'un triangle Ă©quilatĂ©ral. + + Args: + surface: Surface pygame oĂč dessiner + x, y: Position du centre + rotation: Rotation en radians (appliquĂ©e Ă  la forme finale) + size: Taille (rayon du cercle circonscrit du triangle de base) + color: Couleur RGB + depth: Profondeur de rĂ©cursion (0-6 recommandĂ©) + """ + # SĂ©curitĂ© / limites + depth = max(0, min(int(depth), 6)) # profondeur raisonnable pour Ă©viter trop de points + radius = max(2, size // 2) + + # Triangle Ă©quilatĂ©ral centrĂ©, pointant vers le haut (comme vos autres formes) + base_angles = [ + -math.pi / 2, + -math.pi / 2 + 2 * math.pi / 3, + -math.pi / 2 + 4 * math.pi / 3, + ] + base_pts = [(radius * math.cos(a), radius * math.sin(a)) for a in base_angles] + + # GĂ©nĂšre la courbe de Koch pour chaque cĂŽtĂ© du triangle + def koch_curve(p1, p2, d): + if d == 0: + return [p1, p2] + # points 1/3 et 2/3 sur le segment + x1, y1 = p1 + x2, y2 = p2 + dx, dy = (x2 - x1), (y2 - y1) + pA = (x1 + dx / 3.0, y1 + dy / 3.0) + pB = (x1 + 2.0 * dx / 3.0, y1 + 2.0 * dy / 3.0) + # sommet du "pic" (Ă©quilateral) : rotation +60° du segment pA->pB + angle60 = math.pi / 3.0 + ux, uy = (pB[0] - pA[0]), (pB[1] - pA[1]) + # rotation (ux,uy) de +60° + vx = ux * math.cos(angle60) - uy * math.sin(angle60) + vy = ux * math.sin(angle60) + uy * math.cos(angle60) + pC = (pA[0] + vx, pA[1] + vy) + + # rĂ©curse sur 4 segments + seg1 = koch_curve(p1, pA, d - 1) + seg2 = koch_curve(pA, pC, d - 1) + seg3 = koch_curve(pC, pB, d - 1) + seg4 = koch_curve(pB, p2, d - 1) + + # concatĂ©ner en Ă©vitant de dupliquer les points de jonction + return seg1[:-1] + seg2[:-1] + seg3[:-1] + seg4 + + # Construire la liste de points pour le flocon (fermĂ©) + pts = [] + for i in range(3): + p1 = base_pts[i] + p2 = base_pts[(i + 1) % 3] + edge_pts = koch_curve(p1, p2, depth) + if i < 2: + pts += edge_pts[:-1] # Ă©viter duplication du dernier point + else: + pts += edge_pts # dernier cĂŽtĂ© garde le dernier point pour fermer + + # Appliquer rotation et translation via votre helper + rotated = KochSnowflake._rotate_points(pts, rotation, x, y) + + # Dessin : polyligne fermĂ©e (contour 1 px) + try: + if len(rotated) >= 2: + pygame.draw.lines(surface, color, True, rotated, 1) + else: + KochSnowflake._draw_fallback(surface, x, y, color) + except (TypeError, ValueError): + KochSnowflake._draw_fallback(surface, x, y, color) \ No newline at end of file diff --git a/distorsion_movement/shapes/leaf.py b/distorsion_movement/shapes/leaf.py new file mode 100644 index 0000000..a6c3256 --- /dev/null +++ b/distorsion_movement/shapes/leaf.py @@ -0,0 +1,56 @@ +import math +import pygame +from typing import Tuple +from .base_shape import BaseShape + + +class Leaf(BaseShape): + """Feuille = intersection (vesica) de deux disques identiques.""" + + @staticmethod + def _invert_color(c: Tuple[int, int, int]) -> Tuple[int, int, int]: + r, g, b = c + return (255 - r, 255 - g, 255 - b) + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): + try: + diameter = max(6, int(size)) + R = diameter / 2.0 + cx = cy = R + + # finesse: 0.35 (ronde) → 0.75 (pointue). Doit rester < 1.0 + offset_ratio = 0.55 + d = R * offset_ratio + + # 1) deux surfaces alpha avec cercles pleins + s1 = pygame.Surface((diameter, diameter), pygame.SRCALPHA).convert_alpha() + s2 = pygame.Surface((diameter, diameter), pygame.SRCALPHA).convert_alpha() + + pygame.draw.circle(s1, (255, 255, 255, 255), (int(cx - d), int(cy)), int(R)) + pygame.draw.circle(s2, (255, 255, 255, 255), (int(cx + d), int(cy)), int(R)) + + # 2) conversion en masques + intersection + m1 = pygame.mask.from_surface(s1) + m2 = pygame.mask.from_surface(s2) + inter = m1.overlap_mask(m2, (0, 0)) # mĂȘme surface: offset (0,0) + + # 3) rendre le masque d’intersection sur une surface colorĂ©e + temp = pygame.Surface((diameter, diameter), pygame.SRCALPHA).convert_alpha() + leaf_surface = inter.to_surface(setcolor=color + (255,), unsetcolor=(0, 0, 0, 0)) + temp.blit(leaf_surface, (0, 0)) + + # 4) (optionnel) petite nervure centrale & contour anti-alias + pygame.draw.aaline(temp, Leaf._invert_color(color), (cx, cy - R), (cx, cy + R)) + outline_pts = inter.outline() # liste de points bord du masque + if len(outline_pts) > 2: + pygame.draw.aalines(temp, Leaf._invert_color(color), True, outline_pts) + + # 5) rotation + blit + deg = math.degrees(rotation or 0.0) + rotated = pygame.transform.rotozoom(temp, -deg, 1.0) + rect = rotated.get_rect(center=(int(x), int(y))) + surface.blit(rotated, rect.topleft) + + except (TypeError, ValueError): + Leaf._draw_fallback(surface, x, y, color) diff --git a/distorsion_movement/shapes/pentagon.py b/distorsion_movement/shapes/pentagon.py new file mode 100644 index 0000000..680db30 --- /dev/null +++ b/distorsion_movement/shapes/pentagon.py @@ -0,0 +1,41 @@ +""" +Rendu de forme pentagonale. +""" + +import pygame +import math +from typing import Tuple +from .base_shape import BaseShape + + +class Pentagon(BaseShape): + """Moteur de rendu pour les pentagones.""" + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): + """ + Dessine un pentagone rĂ©gulier avec rotation. + + Args: + surface: Surface pygame oĂč dessiner + x, y: Position du centre + rotation: Rotation en radians + size: Taille du pentagone (rayon du cercle circonscrit) + color: Couleur RGB + """ + radius = size // 2 + corners = [] + for i in range(5): + angle = (2 * math.pi * i / 5) - math.pi/2 # Commencer par le haut + corner_x = radius * math.cos(angle) + corner_y = radius * math.sin(angle) + corners.append((corner_x, corner_y)) + + rotated_corners = Pentagon._rotate_points(corners, rotation, x, y) + + if len(rotated_corners) >= 3: + try: + pygame.draw.polygon(surface, color, rotated_corners) + except (TypeError, ValueError): + # Fallback + Pentagon._draw_fallback(surface, x, y, color) \ No newline at end of file diff --git a/distorsion_movement/shapes/ring.py b/distorsion_movement/shapes/ring.py new file mode 100644 index 0000000..8e9d901 --- /dev/null +++ b/distorsion_movement/shapes/ring.py @@ -0,0 +1,32 @@ +""" +Rendu de forme en anneau. +""" + +import pygame +from typing import Tuple +from .base_shape import BaseShape + + +class Ring(BaseShape): + """Moteur de rendu pour les anneaux.""" + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, + color: Tuple[int, int, int], thickness_ratio: float = 0.2): + """ + Dessine un anneau (cercle creux) avec un certain Ă©paisseur. + + Args: + surface: Surface pygame oĂč dessiner + x, y: Position du centre + rotation: Rotation en radians (ignorĂ©e pour un cercle parfait) + size: DiamĂštre extĂ©rieur de l'anneau + color: Couleur RGB de l'anneau + thickness_ratio: Proportion de l'Ă©paisseur par rapport au rayon + """ + try: + outer_radius = max(1, size // 2) + thickness = max(1, int(outer_radius * thickness_ratio)) * 2 + pygame.draw.circle(surface, color, (int(x), int(y)), outer_radius, thickness) + except (TypeError, ValueError): + Ring._draw_fallback(surface, x, y, color) \ No newline at end of file diff --git a/distorsion_movement/shapes/square.py b/distorsion_movement/shapes/square.py new file mode 100644 index 0000000..08497dd --- /dev/null +++ b/distorsion_movement/shapes/square.py @@ -0,0 +1,40 @@ +""" +Rendu de forme carrĂ©e. +""" + +import pygame +from typing import Tuple +from .base_shape import BaseShape + + +class Square(BaseShape): + """Moteur de rendu pour les carrĂ©s.""" + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): + """ + Dessine un carrĂ© avec rotation. + + Args: + surface: Surface pygame oĂč dessiner + x, y: Position du centre + rotation: Rotation en radians + size: Taille du carrĂ© + color: Couleur RGB + """ + half_size = size // 2 + corners = [ + (-half_size, -half_size), + (half_size, -half_size), + (half_size, half_size), + (-half_size, half_size) + ] + + rotated_corners = Square._rotate_points(corners, rotation, x, y) + + if len(rotated_corners) >= 3: + try: + pygame.draw.polygon(surface, color, rotated_corners) + except (TypeError, ValueError): + # Fallback: dessiner un petit rectangle centrĂ© + Square._draw_fallback(surface, x, y, color) \ No newline at end of file diff --git a/distorsion_movement/shapes/star.py b/distorsion_movement/shapes/star.py new file mode 100644 index 0000000..9a318d9 --- /dev/null +++ b/distorsion_movement/shapes/star.py @@ -0,0 +1,48 @@ +""" +Rendu de forme en Ă©toile. +""" + +import pygame +import math +from typing import Tuple +from .base_shape import BaseShape + + +class Star(BaseShape): + """Moteur de rendu pour les Ă©toiles.""" + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): + """ + Dessine une Ă©toile Ă  5 branches avec rotation. + + Args: + surface: Surface pygame oĂč dessiner + x, y: Position du centre + rotation: Rotation en radians + size: Taille de l'Ă©toile (rayon du cercle circonscrit externe) + color: Couleur RGB + """ + outer_radius = size // 2 + inner_radius = outer_radius * 0.4 # Rayon interne plus petit + + corners = [] + for i in range(10): # 5 points externes + 5 points internes + angle = (2 * math.pi * i / 10) - math.pi/2 # Commencer par le haut + if i % 2 == 0: # Points externes + radius = outer_radius + else: # Points internes + radius = inner_radius + + corner_x = radius * math.cos(angle) + corner_y = radius * math.sin(angle) + corners.append((corner_x, corner_y)) + + rotated_corners = Star._rotate_points(corners, rotation, x, y) + + if len(rotated_corners) >= 3: + try: + pygame.draw.polygon(surface, color, rotated_corners) + except (TypeError, ValueError): + # Fallback + Star._draw_fallback(surface, x, y, color) \ No newline at end of file diff --git a/distorsion_movement/shapes/triangle.py b/distorsion_movement/shapes/triangle.py new file mode 100644 index 0000000..fabbea2 --- /dev/null +++ b/distorsion_movement/shapes/triangle.py @@ -0,0 +1,42 @@ +""" +Rendu de forme triangulaire. +""" + +import pygame +import math +from typing import Tuple +from .base_shape import BaseShape + + +class Triangle(BaseShape): + """Moteur de rendu pour les triangles.""" + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): + """ + Dessine un triangle Ă©quilatĂ©ral avec rotation. + + Args: + surface: Surface pygame oĂč dessiner + x, y: Position du centre + rotation: Rotation en radians + size: Taille du triangle (rayon du cercle circonscrit) + color: Couleur RGB + """ + radius = size // 2 + # Triangle Ă©quilatĂ©ral avec un sommet vers le haut + corners = [] + for i in range(3): + angle = (2 * math.pi * i / 3) - math.pi/2 # Commencer par le haut + corner_x = radius * math.cos(angle) + corner_y = radius * math.sin(angle) + corners.append((corner_x, corner_y)) + + rotated_corners = Triangle._rotate_points(corners, rotation, x, y) + + if len(rotated_corners) >= 3: + try: + pygame.draw.polygon(surface, color, rotated_corners) + except (TypeError, ValueError): + # Fallback + Triangle._draw_fallback(surface, x, y, color) \ No newline at end of file diff --git a/distorsion_movement/shapes/yin_yang.py b/distorsion_movement/shapes/yin_yang.py new file mode 100644 index 0000000..86e9145 --- /dev/null +++ b/distorsion_movement/shapes/yin_yang.py @@ -0,0 +1,103 @@ +import math +import pygame +from typing import Tuple +from .base_shape import BaseShape + + +class YinYang(BaseShape): + """Moteur de rendu pour le symbole Yin-Yang.""" + + @staticmethod + def _invert_color(color: Tuple[int, int, int]) -> Tuple[int, int, int]: + r, g, b = color + return (255 - r, 255 - g, 255 - b) + + @staticmethod + def _draw_filled_sector(surf: pygame.Surface, center: Tuple[float, float], radius: float, + start_angle: float, end_angle: float, color: Tuple[int, int, int], steps: int = 72): + """ + Dessine un secteur plein (disque partiel) en reliant des points sur l'arc Ă  son centre. + Angles en radians (0 = axe +x, sens anti-horaire). + """ + cx, cy = center + points = [(cx, cy)] + # Assurer start < end en gĂ©rant les tours complets + if end_angle < start_angle: + end_angle += 2 * math.pi + for i in range(steps + 1): + t = start_angle + (end_angle - start_angle) * (i / steps) + x = cx + radius * math.cos(t) + y = cy + radius * math.sin(t) + points.append((x, y)) + pygame.draw.polygon(surf, color, points) + + @staticmethod + def draw(surface, x: float, y: float, rotation: float, size: int, color: Tuple[int, int, int]): + """ + Dessine un Yin-Yang centrĂ© en (x, y). + + Args: + surface: Surface pygame oĂč dessiner. + x, y: Position du centre. + rotation: Rotation en radians (appliquĂ©e Ă  l'ensemble du symbole). + size: DiamĂštre du symbole. + color: Couleur principale (l'autre couleur est son inverse RGB). + """ + try: + diameter = max(2, int(size)) + radius = diameter / 2.0 + + primary = color + secondary = YinYang._invert_color(color) + + # Surface temporaire avec alpha pour faciliter la rotation et le clipping circulaire + temp = pygame.Surface((diameter, diameter), pygame.SRCALPHA) + temp = temp.convert_alpha() + + center = (radius, radius) + + # 1) Disque extĂ©rieur entiĂšrement en 'primary' + pygame.draw.circle(temp, primary, (int(center[0]), int(center[1])), int(radius)) + + # 2) Peindre une demi-disque (gauche) en 'secondary' pour initier la sĂ©paration. + # Demi-cercle gauche = angles 90° -> 270° (en radians: pi/2 -> 3pi/2) + YinYang._draw_filled_sector( + temp, center, radius, + start_angle=math.pi / 2, + end_angle=3 * math.pi / 2, + color=secondary, + steps=90 + ) + + # 3) Les deux lobes (disques de rayon R/2) le long de l'axe vertical : + # - lobe haut en 'primary' + # - lobe bas en 'secondary' + lobe_r = radius / 2.0 + top_center = (radius, radius / 2.0) + bot_center = (radius, radius + radius / 2.0) + + pygame.draw.circle(temp, primary, (int(top_center[0]), int(top_center[1])), int(lobe_r)) + pygame.draw.circle(temp, secondary, (int(bot_center[0]), int(bot_center[1])), int(lobe_r)) + + # 4) Les deux petits points (traditionnellement ~R/8) + dot_r = max(1, int(radius / 8.0)) + # Point dans le lobe haut (couleur opposĂ©e = secondary) + pygame.draw.circle(temp, secondary, (int(top_center[0]), int(top_center[1])), dot_r) + # Point dans le lobe bas (couleur opposĂ©e = primary) + pygame.draw.circle(temp, primary, (int(bot_center[0]), int(bot_center[1])), dot_r) + + # 5) Re-clipping au disque extĂ©rieur pour Ă©viter tout dĂ©bordement (dessine un "cookie cutter") + # (optionnel ici car tout est dĂ©jĂ  interne, mais on garantit la propretĂ© des bords) + mask = pygame.Surface((diameter, diameter), pygame.SRCALPHA) + pygame.draw.circle(mask, (255, 255, 255, 255), (int(center[0]), int(center[1])), int(radius)) + temp.blit(mask, (0, 0), special_flags=pygame.BLEND_RGBA_MIN) + + # 6) Appliquer la rotation demandĂ©e et blitter au centre (x, y) + deg = math.degrees(rotation or 0.0) + rotated = pygame.transform.rotozoom(temp, -deg, 1.0) # pygame: angle positif = sens horaire inversĂ© + rect = rotated.get_rect(center=(int(x), int(y))) + surface.blit(rotated, rect.topleft) + + except (TypeError, ValueError): + # Fallback: petit rectangle centrĂ©, comme dans Circle + YinYang._draw_fallback(surface, x, y, color) diff --git a/distorsion_movement/tests/conftest.py b/distorsion_movement/tests/conftest.py index 9c19a96..e4ab22a 100644 --- a/distorsion_movement/tests/conftest.py +++ b/distorsion_movement/tests/conftest.py @@ -62,7 +62,6 @@ def pytest_configure(config): """Configure custom pytest markers.""" config.addinivalue_line("markers", "unit: Unit tests") config.addinivalue_line("markers", "integration: Integration tests") - config.addinivalue_line("markers", "audio: Tests requiring audio hardware") config.addinivalue_line("markers", "slow: Slow tests that take more than a few seconds") @@ -72,15 +71,11 @@ def pytest_collection_modifyitems(config, items): # Mark integration tests if "test_integration" in item.nodeid: item.add_marker(pytest.mark.integration) - - # Mark audio tests - if "audio" in item.name.lower() or "test_audio" in item.nodeid: - item.add_marker(pytest.mark.audio) - + # Mark slow tests if "large" in item.name.lower() or "performance" in item.name.lower(): item.add_marker(pytest.mark.slow) # Mark remaining tests as unit tests if not already marked - if not any(mark.name in ['integration', 'audio', 'slow'] for mark in item.iter_markers()): + if not any(mark.name in ['integration', 'slow'] for mark in item.iter_markers()): item.add_marker(pytest.mark.unit) \ No newline at end of file diff --git a/distorsion_movement/tests/test_audio_analyzer.py b/distorsion_movement/tests/test_audio_analyzer.py deleted file mode 100644 index 54a26c9..0000000 --- a/distorsion_movement/tests/test_audio_analyzer.py +++ /dev/null @@ -1,214 +0,0 @@ -""" -Unit tests for audio_analyzer module. -""" - -import pytest -import numpy as np -from unittest.mock import patch, MagicMock -from distorsion_movement.audio_analyzer import AudioAnalyzer, AUDIO_AVAILABLE - - -class TestAudioAnalyzer: - """Test cases for AudioAnalyzer class.""" - - def test_init(self): - """Test AudioAnalyzer initialization.""" - analyzer = AudioAnalyzer() - - assert analyzer.sample_rate == 44100 - assert analyzer.chunk_size == 1024 - assert analyzer.bass_range == (20, 250) - assert analyzer.mid_range == (250, 4000) - assert analyzer.high_range == (4000, 20000) - assert analyzer.bass_level == 0.0 - assert analyzer.mid_level == 0.0 - assert analyzer.high_level == 0.0 - assert analyzer.overall_volume == 0.0 - assert analyzer.beat_detected is False - assert analyzer.is_running is False - assert analyzer.audio_thread is None - assert analyzer.beat_threshold == 1.3 - assert analyzer.smoothing_factor == 0.8 - - def test_init_custom_params(self): - """Test AudioAnalyzer initialization with custom parameters.""" - sample_rate = 48000 - chunk_size = 2048 - - analyzer = AudioAnalyzer(sample_rate=sample_rate, chunk_size=chunk_size) - - assert analyzer.sample_rate == sample_rate - assert analyzer.chunk_size == chunk_size - - def test_detect_beat_logic(self): - """Test beat detection logic.""" - analyzer = AudioAnalyzer() - - # Fill volume history with low values - for _ in range(15): - analyzer._detect_beat(0.1) - - assert analyzer.beat_detected is False - - # Send a high volume spike - analyzer._detect_beat(1.0) - - # Should detect beat if spike is significant enough - # (depends on threshold and history) - assert isinstance(analyzer.beat_detected, bool) - - def test_detect_beat_cooldown(self): - """Test beat detection cooldown mechanism.""" - analyzer = AudioAnalyzer() - - # Fill history with baseline - for _ in range(15): - analyzer._detect_beat(0.1) - - # Trigger beat detection - analyzer._detect_beat(1.0) - - # If beat was detected, cooldown should be active - if analyzer.beat_detected: - assert analyzer.beat_cooldown > 0 - - # During cooldown, another spike shouldn't trigger immediately - old_cooldown = analyzer.beat_cooldown - analyzer._detect_beat(1.0) - assert analyzer.beat_cooldown <= old_cooldown - - def test_get_audio_features_no_audio(self): - """Test getting audio features when no audio is running.""" - analyzer = AudioAnalyzer() - - features = analyzer.get_audio_features() - - expected_features = { - 'bass_level': 0.0, - 'mid_level': 0.0, - 'high_level': 0.0, - 'overall_volume': 0.0, - 'beat_detected': False - } - - assert features == expected_features - - @pytest.mark.skipif(not AUDIO_AVAILABLE, reason="Audio libraries not available") - @patch('distorsion_movement.audio_analyzer.pyaudio.PyAudio') - def test_start_stop_audio_capture(self, mock_pyaudio): - """Test starting and stopping audio capture.""" - # Mock PyAudio - mock_audio = MagicMock() - mock_stream = MagicMock() - mock_pyaudio.return_value = mock_audio - mock_audio.open.return_value = mock_stream - - analyzer = AudioAnalyzer() - - # Test start - analyzer.start_audio_capture() - assert analyzer.is_running is True - assert analyzer.audio_thread is not None - - # Test stop - analyzer.stop_audio_capture() - assert analyzer.is_running is False - - # Verify PyAudio was called correctly - mock_audio.open.assert_called_once() - mock_stream.stop_stream.assert_called_once() - mock_stream.close.assert_called_once() - mock_audio.terminate.assert_called_once() - - @pytest.mark.skipif(AUDIO_AVAILABLE, reason="Test only when audio is not available") - def test_start_audio_capture_no_audio_libs(self): - """Test starting audio capture when audio libraries are not available.""" - analyzer = AudioAnalyzer() - - # Should not raise an exception, but should not start either - analyzer.start_audio_capture() - assert analyzer.is_running is False - assert analyzer.audio_thread is None - - def test_audio_feature_normalization(self): - """Test that audio features are properly normalized.""" - analyzer = AudioAnalyzer() - - # Set some raw levels - analyzer.bass_level = 0.01 # Will be multiplied by 100 in get_audio_features - analyzer.mid_level = 0.02 # Will be multiplied by 50 - analyzer.high_level = 0.05 # Will be multiplied by 20 - analyzer.overall_volume = 0.03 # Will be multiplied by 30 - - features = analyzer.get_audio_features() - - # Check normalization - assert features['bass_level'] == min(0.01 * 100, 1.0) - assert features['mid_level'] == min(0.02 * 50, 1.0) - assert features['high_level'] == min(0.05 * 20, 1.0) - assert features['overall_volume'] == min(0.03 * 30, 1.0) - - # All should be clamped to 1.0 max - for level in ['bass_level', 'mid_level', 'high_level', 'overall_volume']: - assert 0.0 <= features[level] <= 1.0 - - def test_frequency_range_boundaries(self): - """Test that frequency ranges are properly defined.""" - analyzer = AudioAnalyzer() - - # Check that ranges don't overlap incorrectly - assert analyzer.bass_range[1] == analyzer.mid_range[0] - assert analyzer.mid_range[1] == analyzer.high_range[0] - - # Check that ranges are within reasonable bounds - assert analyzer.bass_range[0] >= 0 - assert analyzer.high_range[1] <= analyzer.sample_rate / 2 - - def test_thread_safety(self): - """Test that audio levels can be read safely.""" - analyzer = AudioAnalyzer() - - # Set some levels - analyzer.bass_level = 0.005 # Will be 0.5 after normalization - analyzer.mid_level = 0.006 # Will be 0.3 after normalization - analyzer.high_level = 0.035 # Will be 0.7 after normalization - - # Get features multiple times - features1 = analyzer.get_audio_features() - features2 = analyzer.get_audio_features() - - # Should be consistent - assert features1['bass_level'] == features2['bass_level'] - assert features1['mid_level'] == features2['mid_level'] - assert features1['high_level'] == features2['high_level'] - - def test_volume_history_management(self): - """Test that volume history is properly managed.""" - analyzer = AudioAnalyzer() - - # Add more than 20 values - for i in range(25): - analyzer._detect_beat(i * 0.1) - - # History should be capped at 20 - assert len(analyzer.volume_history) == 20 - - # Should contain the most recent values - assert analyzer.volume_history[-1] == 24 * 0.1 - - def test_smoothing_factor_effect(self): - """Test that smoothing factor affects level updates.""" - analyzer = AudioAnalyzer() - - # Set initial levels - analyzer.bass_level = 0.5 - old_bass = analyzer.bass_level - - # Simulate processing with new data - # (This would normally happen in _process_audio, but we'll simulate) - new_value = 1.0 - expected_smoothed = old_bass * analyzer.smoothing_factor + new_value * (1 - analyzer.smoothing_factor) - - # Manual smoothing calculation to verify the concept - assert analyzer.smoothing_factor == 0.8 - assert abs(expected_smoothed - (0.5 * 0.8 + 1.0 * 0.2)) < 0.001 \ No newline at end of file diff --git a/distorsion_movement/tests/test_deformed_grid.py b/distorsion_movement/tests/test_deformed_grid.py index 2eed0d9..75693c8 100644 --- a/distorsion_movement/tests/test_deformed_grid.py +++ b/distorsion_movement/tests/test_deformed_grid.py @@ -41,8 +41,6 @@ def test_init_default_params(self): assert grid.square_color == (255, 255, 255) assert grid.color_scheme == "monochrome" assert grid.color_animation is False - assert grid.audio_reactive is False - assert grid.audio_analyzer is None assert grid.time == 0.0 assert grid.animation_speed == 0.02 assert grid.is_fullscreen is False @@ -59,7 +57,6 @@ def test_init_custom_params(self): square_color=(255, 0, 0), color_scheme="rainbow", color_animation=True, - audio_reactive=False ) assert grid.dimension == 32 @@ -71,20 +68,7 @@ def test_init_custom_params(self): assert grid.square_color == (255, 0, 0) assert grid.color_scheme == "rainbow" assert grid.color_animation is True - assert grid.audio_reactive is False - - @patch('distorsion_movement.deformed_grid.AudioAnalyzer') - def test_init_with_audio_reactive(self, mock_audio_analyzer): - """Test DeformedGrid initialization with audio reactivity.""" - mock_analyzer_instance = MagicMock() - mock_audio_analyzer.return_value = mock_analyzer_instance - - grid = DeformedGrid(audio_reactive=True) - - assert grid.audio_reactive is True - assert grid.audio_analyzer is not None - mock_analyzer_instance.start_audio_capture.assert_called_once() - + def test_generate_base_positions(self): """Test generation of base grid positions.""" grid = DeformedGrid(dimension=3, cell_size=10, canvas_size=(100, 100)) diff --git a/distorsion_movement/tests/test_integration.py b/distorsion_movement/tests/test_integration.py index 9ceeca6..945a8fe 100644 --- a/distorsion_movement/tests/test_integration.py +++ b/distorsion_movement/tests/test_integration.py @@ -6,7 +6,7 @@ import pygame from unittest.mock import patch, MagicMock from distorsion_movement import ( - DeformedGrid, DistortionType, ColorScheme, AudioAnalyzer, + DeformedGrid, DistortionType, ColorScheme, ColorGenerator, DistortionEngine, create_deformed_grid ) from distorsion_movement.demos import quick_demo @@ -36,7 +36,6 @@ def test_package_imports(self): assert DeformedGrid is not None assert DistortionType is not None assert ColorScheme is not None - assert AudioAnalyzer is not None assert ColorGenerator is not None assert DistortionEngine is not None assert create_deformed_grid is not None @@ -113,34 +112,6 @@ def test_distortion_engine_color_generator_integration(self): assert len(distorted_positions) == len(colors) assert len(distorted_positions) == 3 - @patch('distorsion_movement.deformed_grid.AudioAnalyzer') - def test_audio_reactive_integration(self, mock_audio_analyzer): - """Test audio-reactive functionality integration.""" - mock_analyzer = MagicMock() - mock_analyzer.get_audio_features.return_value = { - 'bass_level': 0.8, - 'mid_level': 0.5, - 'high_level': 0.3, - 'overall_volume': 0.6, - 'beat_detected': True - } - mock_audio_analyzer.return_value = mock_analyzer - - grid = DeformedGrid( - dimension=4, - audio_reactive=True, - distortion_strength=0.2 - ) - - # Should have created audio analyzer - assert grid.audio_analyzer is not None - mock_analyzer.start_audio_capture.assert_called_once() - - # Should be able to get audio features - features = grid.audio_analyzer.get_audio_features() - assert features['bass_level'] == 0.8 - assert features['beat_detected'] is True - def test_create_deformed_grid_function(self): """Test the convenience function for creating grids.""" with patch('pygame.display.set_mode'), \ @@ -264,10 +235,6 @@ def test_performance_large_grid(self): def test_module_cleanup(self): """Test that modules clean up resources properly.""" - # Test audio analyzer cleanup - analyzer = AudioAnalyzer() - analyzer.stop_audio_capture() # Should not raise error even if not started - # Test grid cleanup (implicit through garbage collection) grid = DeformedGrid(dimension=2) del grid # Should not cause issues \ No newline at end of file diff --git a/distorsion_movement/tests/test_shapes.py b/distorsion_movement/tests/test_shapes.py index 9412045..ef2cd25 100644 --- a/distorsion_movement/tests/test_shapes.py +++ b/distorsion_movement/tests/test_shapes.py @@ -7,7 +7,10 @@ import math from unittest.mock import Mock, patch, MagicMock -from distorsion_movement.shapes import ShapeRenderer, get_shape_renderer_function +from distorsion_movement.shapes import ( + get_shape_renderer_function, + BaseShape, Square, Circle, Triangle, Hexagon, Pentagon, Star, Diamond, KochSnowflake, Ring +) @pytest.fixture @@ -19,13 +22,13 @@ def mock_surface(): return surface -class TestShapeRenderer: - """Tests pour la classe ShapeRenderer.""" +class TestBaseShape: + """Tests pour la classe BaseShape.""" def test_rotate_points_no_rotation(self): """Test de rotation avec angle zĂ©ro.""" points = [(10, 0), (0, 10), (-10, 0), (0, -10)] - result = ShapeRenderer._rotate_points(points, 0, 100, 100) + result = BaseShape._rotate_points(points, 0, 100, 100) expected = [(110, 100), (100, 110), (90, 100), (100, 90)] assert result == expected @@ -33,7 +36,7 @@ def test_rotate_points_no_rotation(self): def test_rotate_points_90_degrees(self): """Test de rotation de 90 degrĂ©s.""" points = [(10, 0), (0, 10)] - result = ShapeRenderer._rotate_points(points, math.pi/2, 0, 0) + result = BaseShape._rotate_points(points, math.pi/2, 0, 0) # AprĂšs rotation de 90°: (10,0) -> (0,10), (0,10) -> (-10,0) expected = [(0, 10), (-10, 0)] @@ -42,16 +45,19 @@ def test_rotate_points_90_degrees(self): def test_rotate_points_with_invalid_coordinates(self): """Test de gestion des coordonnĂ©es invalides.""" points = [(float('inf'), 0), (0, float('nan'))] - result = ShapeRenderer._rotate_points(points, 0, 50, 50) + result = BaseShape._rotate_points(points, 0, 50, 50) # CoordonnĂ©es invalides doivent ĂȘtre remplacĂ©es par le centre expected = [(50, 50), (50, 50)] assert result == expected +class TestSquareShape: + """Tests pour la forme carrĂ©.""" + @patch('pygame.draw.polygon') def test_draw_square_success(self, mock_polygon, mock_surface): """Test du dessin d'un carrĂ© rĂ©ussi.""" - ShapeRenderer.draw_square(mock_surface, 100, 100, 0, 20, (255, 0, 0)) + Square.draw(mock_surface, 100, 100, 0, 20, (255, 0, 0)) # VĂ©rifier que pygame.draw.polygon a Ă©tĂ© appelĂ© mock_polygon.assert_called_once() @@ -64,16 +70,39 @@ def test_draw_square_success(self, mock_polygon, mock_surface): @patch('pygame.draw.polygon', side_effect=ValueError("Invalid polygon")) def test_draw_square_fallback(self, mock_polygon, mock_rect, mock_surface): """Test du fallback en cas d'erreur lors du dessin d'un carrĂ©.""" - ShapeRenderer.draw_square(mock_surface, 100, 100, 0, 20, (255, 0, 0)) + Square.draw(mock_surface, 100, 100, 0, 20, (255, 0, 0)) # VĂ©rifier que le fallback (rectangle) a Ă©tĂ© utilisĂ© mock_polygon.assert_called_once() mock_rect.assert_called_once() + @patch('pygame.draw.polygon') + def test_draw_square_with_rotation(self, mock_polygon, mock_surface): + """Test du dessin d'un carrĂ© avec rotation.""" + rotation = math.pi / 4 # 45 degrĂ©s + Square.draw(mock_surface, 100, 100, rotation, 20, (255, 0, 0)) + + # VĂ©rifier que pygame.draw.polygon a Ă©tĂ© appelĂ© + mock_polygon.assert_called_once() + args = mock_polygon.call_args[0] + + # Les points doivent ĂȘtre diffĂ©rents de ceux sans rotation + rotated_points = args[2] + assert len(rotated_points) == 4 + + # VĂ©rifier que les points ne sont pas aux positions "normales" d'un carrĂ© + # (ça confirme que la rotation a Ă©tĂ© appliquĂ©e) + expected_unrotated = [(90, 90), (110, 90), (110, 110), (90, 110)] + assert rotated_points != expected_unrotated + + +class TestCircleShape: + """Tests pour la forme cercle.""" + @patch('pygame.draw.circle') def test_draw_circle_success(self, mock_circle, mock_surface): """Test du dessin d'un cercle rĂ©ussi.""" - ShapeRenderer.draw_circle(mock_surface, 100, 100, 0, 20, (0, 255, 0)) + Circle.draw(mock_surface, 100, 100, 0, 20, (0, 255, 0)) # VĂ©rifier que pygame.draw.circle a Ă©tĂ© appelĂ© avec les bons paramĂštres mock_circle.assert_called_once_with(mock_surface, (0, 255, 0), (100, 100), 10) @@ -82,16 +111,34 @@ def test_draw_circle_success(self, mock_circle, mock_surface): @patch('pygame.draw.circle', side_effect=ValueError("Invalid circle")) def test_draw_circle_fallback(self, mock_circle, mock_rect, mock_surface): """Test du fallback en cas d'erreur lors du dessin d'un cercle.""" - ShapeRenderer.draw_circle(mock_surface, 100, 100, 0, 20, (0, 255, 0)) + Circle.draw(mock_surface, 100, 100, 0, 20, (0, 255, 0)) # VĂ©rifier que le fallback (rectangle) a Ă©tĂ© utilisĂ© mock_circle.assert_called_once() mock_rect.assert_called_once() + def test_draw_with_zero_size(self, mock_surface): + """Test du comportement avec une taille nulle.""" + with patch('pygame.draw.circle') as mock_circle: + Circle.draw(mock_surface, 100, 100, 0, 0, (255, 0, 0)) + # Le rayon doit ĂȘtre au minimum 1 + mock_circle.assert_called_once_with(mock_surface, (255, 0, 0), (100, 100), 1) + + def test_draw_with_negative_size(self, mock_surface): + """Test du comportement avec une taille nĂ©gative.""" + with patch('pygame.draw.circle') as mock_circle: + Circle.draw(mock_surface, 100, 100, 0, -10, (255, 0, 0)) + # Le rayon doit ĂȘtre au minimum 1 + mock_circle.assert_called_once_with(mock_surface, (255, 0, 0), (100, 100), 1) + + +class TestPolygonShapes: + """Tests pour les formes polygonales.""" + @patch('pygame.draw.polygon') def test_draw_triangle_success(self, mock_polygon, mock_surface): """Test du dessin d'un triangle rĂ©ussi.""" - ShapeRenderer.draw_triangle(mock_surface, 100, 100, 0, 30, (0, 0, 255)) + Triangle.draw(mock_surface, 100, 100, 0, 30, (0, 0, 255)) # VĂ©rifier que pygame.draw.polygon a Ă©tĂ© appelĂ© mock_polygon.assert_called_once() @@ -103,7 +150,7 @@ def test_draw_triangle_success(self, mock_polygon, mock_surface): @patch('pygame.draw.polygon') def test_draw_hexagon_success(self, mock_polygon, mock_surface): """Test du dessin d'un hexagone rĂ©ussi.""" - ShapeRenderer.draw_hexagon(mock_surface, 100, 100, 0, 24, (255, 255, 0)) + Hexagon.draw(mock_surface, 100, 100, 0, 24, (255, 255, 0)) # VĂ©rifier que pygame.draw.polygon a Ă©tĂ© appelĂ© mock_polygon.assert_called_once() @@ -115,7 +162,7 @@ def test_draw_hexagon_success(self, mock_polygon, mock_surface): @patch('pygame.draw.polygon') def test_draw_pentagon_success(self, mock_polygon, mock_surface): """Test du dessin d'un pentagone rĂ©ussi.""" - ShapeRenderer.draw_pentagon(mock_surface, 100, 100, 0, 25, (255, 0, 255)) + Pentagon.draw(mock_surface, 100, 100, 0, 25, (255, 0, 255)) # VĂ©rifier que pygame.draw.polygon a Ă©tĂ© appelĂ© mock_polygon.assert_called_once() @@ -127,7 +174,7 @@ def test_draw_pentagon_success(self, mock_polygon, mock_surface): @patch('pygame.draw.polygon') def test_draw_star_success(self, mock_polygon, mock_surface): """Test du dessin d'une Ă©toile rĂ©ussie.""" - ShapeRenderer.draw_star(mock_surface, 100, 100, 0, 30, (128, 128, 128)) + Star.draw(mock_surface, 100, 100, 0, 30, (128, 128, 128)) # VĂ©rifier que pygame.draw.polygon a Ă©tĂ© appelĂ© mock_polygon.assert_called_once() @@ -139,7 +186,7 @@ def test_draw_star_success(self, mock_polygon, mock_surface): @patch('pygame.draw.polygon') def test_draw_diamond_success(self, mock_polygon, mock_surface): """Test du dessin d'un losange rĂ©ussi.""" - ShapeRenderer.draw_diamond(mock_surface, 100, 100, 0, 20, (64, 64, 64)) + Diamond.draw(mock_surface, 100, 100, 0, 20, (64, 64, 64)) # VĂ©rifier que pygame.draw.polygon a Ă©tĂ© appelĂ© mock_polygon.assert_called_once() @@ -147,39 +194,6 @@ def test_draw_diamond_success(self, mock_polygon, mock_surface): assert args[0] == mock_surface # surface assert args[1] == (64, 64, 64) # couleur assert len(args[2]) == 4 # 4 coins pour un losange - - @patch('pygame.draw.polygon') - def test_draw_square_with_rotation(self, mock_polygon, mock_surface): - """Test du dessin d'un carrĂ© avec rotation.""" - rotation = math.pi / 4 # 45 degrĂ©s - ShapeRenderer.draw_square(mock_surface, 100, 100, rotation, 20, (255, 0, 0)) - - # VĂ©rifier que pygame.draw.polygon a Ă©tĂ© appelĂ© - mock_polygon.assert_called_once() - args = mock_polygon.call_args[0] - - # Les points doivent ĂȘtre diffĂ©rents de ceux sans rotation - rotated_points = args[2] - assert len(rotated_points) == 4 - - # VĂ©rifier que les points ne sont pas aux positions "normales" d'un carrĂ© - # (ça confirme que la rotation a Ă©tĂ© appliquĂ©e) - expected_unrotated = [(90, 90), (110, 90), (110, 110), (90, 110)] - assert rotated_points != expected_unrotated - - def test_draw_with_zero_size(self, mock_surface): - """Test du comportement avec une taille nulle.""" - with patch('pygame.draw.circle') as mock_circle: - ShapeRenderer.draw_circle(mock_surface, 100, 100, 0, 0, (255, 0, 0)) - # Le rayon doit ĂȘtre au minimum 1 - mock_circle.assert_called_once_with(mock_surface, (255, 0, 0), (100, 100), 1) - - def test_draw_with_negative_size(self, mock_surface): - """Test du comportement avec une taille nĂ©gative.""" - with patch('pygame.draw.circle') as mock_circle: - ShapeRenderer.draw_circle(mock_surface, 100, 100, 0, -10, (255, 0, 0)) - # Le rayon doit ĂȘtre au minimum 1 - mock_circle.assert_called_once_with(mock_surface, (255, 0, 0), (100, 100), 1) class TestShapeRendererFunctionGetter: @@ -188,52 +202,62 @@ class TestShapeRendererFunctionGetter: def test_get_square_function(self): """Test de rĂ©cupĂ©ration de la fonction carrĂ©.""" func = get_shape_renderer_function("square") - assert func == ShapeRenderer.draw_square + assert func == Square.draw def test_get_circle_function(self): """Test de rĂ©cupĂ©ration de la fonction cercle.""" func = get_shape_renderer_function("circle") - assert func == ShapeRenderer.draw_circle + assert func == Circle.draw def test_get_triangle_function(self): """Test de rĂ©cupĂ©ration de la fonction triangle.""" func = get_shape_renderer_function("triangle") - assert func == ShapeRenderer.draw_triangle + assert func == Triangle.draw def test_get_hexagon_function(self): """Test de rĂ©cupĂ©ration de la fonction hexagone.""" func = get_shape_renderer_function("hexagon") - assert func == ShapeRenderer.draw_hexagon + assert func == Hexagon.draw def test_get_pentagon_function(self): """Test de rĂ©cupĂ©ration de la fonction pentagone.""" func = get_shape_renderer_function("pentagon") - assert func == ShapeRenderer.draw_pentagon + assert func == Pentagon.draw def test_get_star_function(self): """Test de rĂ©cupĂ©ration de la fonction Ă©toile.""" func = get_shape_renderer_function("star") - assert func == ShapeRenderer.draw_star + assert func == Star.draw def test_get_diamond_function(self): """Test de rĂ©cupĂ©ration de la fonction losange.""" func = get_shape_renderer_function("diamond") - assert func == ShapeRenderer.draw_diamond + assert func == Diamond.draw + + def test_get_koch_snowflake_function(self): + """Test de rĂ©cupĂ©ration de la fonction flocon de Koch.""" + func = get_shape_renderer_function("koch_snowflake") + assert func == KochSnowflake.draw + + def test_get_ring_function(self): + """Test de rĂ©cupĂ©ration de la fonction anneau.""" + func = get_shape_renderer_function("ring") + assert func == Ring.draw def test_get_unknown_function_returns_square(self): """Test que les formes inconnues retournent la fonction carrĂ© par dĂ©faut.""" func = get_shape_renderer_function("unknown_shape") - assert func == ShapeRenderer.draw_square + assert func == Square.draw def test_get_empty_string_returns_square(self): """Test que chaĂźne vide retourne la fonction carrĂ© par dĂ©faut.""" func = get_shape_renderer_function("") - assert func == ShapeRenderer.draw_square + assert func == Square.draw def test_get_none_returns_square(self): """Test que None retourne la fonction carrĂ© par dĂ©faut.""" func = get_shape_renderer_function(None) - assert func == ShapeRenderer.draw_square + assert func == Square.draw @pytest.mark.integration @@ -243,9 +267,10 @@ class TestShapeIntegration: @patch('pygame.draw.polygon') @patch('pygame.draw.circle') @patch('pygame.draw.rect') - def test_all_shapes_can_be_drawn(self, mock_rect, mock_circle, mock_polygon, mock_surface): + @patch('pygame.draw.lines') # pour koch_snowflake + def test_all_shapes_can_be_drawn(self, mock_lines, mock_rect, mock_circle, mock_polygon, mock_surface): """Test que toutes les formes peuvent ĂȘtre dessinĂ©es sans erreur.""" - shapes = ["square", "circle", "triangle", "hexagon", "pentagon", "star", "diamond"] + shapes = ["square", "circle", "triangle", "hexagon", "pentagon", "star", "diamond", "koch_snowflake", "ring"] for shape_type in shapes: func = get_shape_renderer_function(shape_type) @@ -258,7 +283,8 @@ def test_all_shapes_can_be_drawn(self, mock_rect, mock_circle, mock_polygon, moc @patch('pygame.draw.polygon') @patch('pygame.draw.circle') @patch('pygame.draw.rect') - def test_shapes_with_various_parameters(self, mock_rect, mock_circle, mock_polygon, mock_surface): + @patch('pygame.draw.lines') # pour koch_snowflake + def test_shapes_with_various_parameters(self, mock_lines, mock_rect, mock_circle, mock_polygon, mock_surface): """Test des formes avec diffĂ©rents paramĂštres.""" test_cases = [ (50, 50, 0, 10, (255, 0, 0)), @@ -268,7 +294,7 @@ def test_shapes_with_various_parameters(self, mock_rect, mock_circle, mock_polyg ] for x, y, rotation, size, color in test_cases: - for shape_type in ["square", "circle", "triangle", "star"]: + for shape_type in ["square", "circle", "triangle", "star", "ring"]: func = get_shape_renderer_function(shape_type) # Ne doit pas lever d'exception try: diff --git a/ideas_to_implement/features_ideas.md b/ideas_to_implement/features_ideas.md new file mode 100644 index 0000000..88c5c6d --- /dev/null +++ b/ideas_to_implement/features_ideas.md @@ -0,0 +1,175 @@ +# 🎹 Deformed Grid - Epic Improvements Roadmap + +## đŸŽ” **Audio-Reactive Features** - Not working that well +- **Real-time audio analysis** using FFT and pyaudio +- **Frequency separation**: Bass, mids, highs control different visual aspects +- **Beat detection** with synchronized flash effects +- **Audio → Visual mapping**: + - đŸ„ Bass (20-250Hz) → Distortion intensity + - 🎾 Mids (250Hz-4kHz) → Color hue rotation + - ✹ Highs (4kHz+) → Brightness boosts + - đŸ’„ Beat detection → White flash effects + - 📱 Volume → Animation speed +- **Interactive controls**: Press 'M' to toggle audio reactivity + +## 🎹 **Advanced Visual Effects** + +### **Particle Systems** +- **Trailing sparks** behind moving squares +- **Glowing halos** around squares based on audio intensity +- **Energy fields** that connect nearby squares +- **Particle explosions** on beat detection +- **Floating particles** that react to distortions + +### **Motion Blur & Glow Effects** +- **Motion blur trails** as squares move +- **Neon glow effects** that actually bleed light +- **Bloom/HDR effects** for bright colors +- **Ghost trails** showing previous positions +- **Chromatic aberration** for psychedelic effects + +### **Post-Processing Filters** +- **Real-time blur/sharpen** filters +- **Color correction** and saturation boost +- **Vintage film effects** (grain, vignette) +- **Kaleidoscope/mirror** effects +- **Pixelation/retro** filters + +## đŸ–±ïž **Interactive Features** + +### **Mouse Interaction** +- **Attraction/repulsion** - squares follow or flee from cursor +- **Paint mode** - click and drag to paint colors in real-time +- **Gravity wells** - create distortion fields with mouse clicks +- **Magnetic fields** - squares align like iron filings around cursor +- **Ripple effects** - wave propagation from click points + +### **Touch/Multi-touch Support** +- **Tablet compatibility** for touch devices +- **Pinch-to-zoom** for closer inspection +- **Multi-finger gestures** for complex interactions +- **Pressure sensitivity** for drawing effects + +## đŸ”ș **Shape Variety & Morphing** + +### **Multiple Shapes** +- **Circles, triangles, hexagons, stars** +- **Custom polygons** with variable sides +- **Organic shapes** using bezier curves +- **3D-looking shapes** with perspective +- **Text characters** as shapes + +### **Shape Morphing** +- **Squares → circles** transformation +- **Size variation** for depth perception +- **Shape interpolation** between different forms +- **Breathing/pulsing** shape animations +- **Fractal shapes** with recursive patterns + +## đŸŒȘ **Advanced Distortion Types** + +### **New Distortion Algorithms** +- **Spiral/vortex** effects with rotation fields +- **Fluid dynamics** simulation +- **Magnetic field** distortions +- **Gravitational** lensing effects +- **Turbulence** and noise-based distortions + +### **Compound Distortions** +- **Multiple distortions** applied simultaneously +- **Distortion layering** with different intensities +- **Time-based distortion** sequences +- **Audio-reactive distortion** switching + +## 🎼 **Gaming & Interactive Elements** + +### **Preset Scenes** +- **Curated combinations** of colors, distortions, and effects +- **Genre-specific presets** (Electronic, Classical, Rock, etc.) +- **Mood-based themes** (Chill, Energetic, Psychedelic) +- **Time-of-day** adaptive themes + +### **Randomization & AI** +- **"Surprise Me"** button for random combinations +- **Genetic algorithms** for evolving patterns +- **AI-generated** color palettes +- **Smart recommendations** based on music genre + +## đŸ’Ÿ **Export & Sharing Features** + +### **Media Export** +- **GIF/MP4 export** of animations +- **High-resolution rendering** (4K, 8K) +- **Frame-by-frame** export for video editing +- **Live streaming** integration +- **Screenshot burst mode** + +### **Data Management** +- **Save/load presets** as JSON files +- **Color palette export** to Adobe/Figma formats +- **Batch generation** of hundreds of variations +- **Session recording** and playback +- **Cloud sync** for settings + +## 🚀 **Performance & Technical** + +### **Optimization** +- **GPU acceleration** using OpenGL/Metal +- **Multi-threading** for audio processing +- **Level-of-detail** rendering for performance +- **Memory optimization** for large grids +- **60fps guarantee** even with thousands of squares + +### **Platform Support** +- **Web version** using WebGL +- **Mobile apps** (iOS/Android) +- **VR/AR support** for immersive experiences +- **Hardware controller** support (MIDI, game controllers) + +## 🎯 **Special Effects** + +### **Fractal & Mathematical Patterns** +- **Mandelbrot/Julia sets** integration +- **L-systems** for organic growth patterns +- **Cellular automata** (Conway's Game of Life) +- **Strange attractors** (Lorenz, Rössler) +- **Fibonacci spirals** and golden ratio patterns + +### **Physics Simulation** +- **Collision detection** between squares +- **Spring systems** connecting squares +- **Fluid dynamics** for liquid-like behavior +- **Gravity simulation** with realistic physics +- **Electromagnetic** field visualization + +## 🌈 **Extended Color Systems** + +### **Advanced Color Schemes** +- **Perceptually uniform** color spaces (LAB, LUV) +- **Color harmony** rules (triadic, complementary, etc.) +- **Seasonal palettes** that change over time +- **Emotion-based** color mapping +- **Cultural color** themes from around the world + +### **Dynamic Color Effects** +- **Color bleeding** between adjacent squares +- **Chromatic aberration** for retro effects +- **Color temperature** shifts +- **Saturation breathing** effects +- **Hue cycling** with musical harmony + +## đŸŽȘ **Experimental Features** + +### **Generative Art Integration** +- **Style transfer** using neural networks +- **Procedural textures** on squares +- **Algorithmic composition** of patterns +- **Evolutionary art** that improves over time +- **Collaborative evolution** with user feedback + +### **Data Visualization** +- **Real-time data** integration (stock prices, weather, etc.) +- **Social media sentiment** visualization +- **Network topology** representation +- **Scientific data** visualization modes +- **Biometric integration** (heart rate, brain waves) diff --git a/implementation_plans/new_colors.md b/ideas_to_implement/new_colors.md similarity index 59% rename from implementation_plans/new_colors.md rename to ideas_to_implement/new_colors.md index 2de4a9a..dd36580 100644 --- a/implementation_plans/new_colors.md +++ b/ideas_to_implement/new_colors.md @@ -4,27 +4,27 @@ - Triadic → three equally spaced hues (e.g., cyan–magenta–yellow). - Split-complementary → a base color + the two colors adjacent to its complement. -- Duotone + Accent → two main colors and a single pop color appearing rarely. -- Infrared / Thermal camera → deep blue → green → yellow → red → white. +- ✅ Duotone + Accent → two main colors and a single pop color appearing rarely. +- ✅ Infrared / Thermal camera → deep blue → green → yellow → red → white. ## Mood-based themes - ✅ Cyberpunk → neon magenta, cyan, and deep purple. -- Sunset → warm yellow → orange → pink → purple gradient. +- ✅ Sunset → warm yellow → orange → pink → purple gradient. - ✅ Aurora Borealis → teal → green → purple with soft blending. -- Desert → sandy beige, warm oranges, muted browns. +- ✅ Desert → sandy beige, warm oranges, muted browns. ## Texture & subtle contrast - Earthy tones → muted greens, browns, ochres. - Muted pastel rainbow → same hue spread as rainbow, but low saturation. -- Metallics → gold, silver, bronze gradients. +- ✅ Metallics → gold, silver, bronze gradients. ## Playful & abstract -- Pop art → bright primaries with sharp black/white outlines. -- Vaporwave → lavender, cyan, peach, pink. -- Candy shop → bubblegum pink, mint green, lemon yellow. +- ✅ Pop art → bright primaries with sharp black/white outlines. +- ✅ Vaporwave → lavender, cyan, peach, pink. +- ✅ Candy shop → bubblegum pink, mint green, lemon yellow. ## Experimental diff --git a/implementation_plans/new_distortions_again.md b/ideas_to_implement/new_distortions.md similarity index 99% rename from implementation_plans/new_distortions_again.md rename to ideas_to_implement/new_distortions.md index d77d6e9..bf739a5 100644 --- a/implementation_plans/new_distortions_again.md +++ b/ideas_to_implement/new_distortions.md @@ -76,7 +76,7 @@ Effect: Edges split into rainbow fringes that shift over time. Twist: Vary offsets radially for a “sunburst” prism look. -4. Hypno Spiral Pulse +4. Hypno Spiral Pulse ✅ Idea: Spiral Warp + Pulse, but the pulse affects angle instead of radius. Effect: Feels like the entire spiral is breathing and twisting at the same time, pulling you in. diff --git a/ideas_to_implement/new_shapes.md b/ideas_to_implement/new_shapes.md new file mode 100644 index 0000000..63e9eb1 --- /dev/null +++ b/ideas_to_implement/new_shapes.md @@ -0,0 +1,46 @@ +1. More Regular Polygons +You already have 3–6 sides covered; adding more could allow for patterns that feel more “tiling-like” or mandala-like: + +- Octagon (8 sides) — great for symmetric, radial compositions. +- Heptagon (7 sides) — slightly asymmetrical feel compared to hexagon/octagon. +- Nonagon (9 sides) — nice when combined with rotation animations. + +2. Curved / Organic Shapes +These can make your generative art look less rigid: + +- ✅ Ellipse — a stretched circle; rotation makes it feel dynamic. +- Arc / Semi-Circle — partial curves work beautifully in repetitive patterns. +- ✅ Leaf Shape — two mirrored arcs meeting at sharp tips. + +3. Complex Geometric Stars +You have a 5-point star — but variations can really spice things up: + +- 6, 7, or 8-point stars — by alternating inner/outer radii in the same pattern you used. +- Spirograph-like stars — computed from sine/cosine with more complex frequency ratios. + +4. Crosses & Plus Signs +Minimalist but striking when repeated: + +- Cross / X-shape — can be rotated in interesting ways. +- “Asterisk” — a star made from straight lines instead of polygons. + + +5. Fractal / Iterative Shapes +These are great for generative, especially with variable recursion depth: + +- ✅ Koch Snowflake — fractal triangle-based snowflake. +- Sierpinski Triangle — hollow, recursive triangles. +- Fractal Tree — if you want organic growth patterns. + +6. Pattern & Optical Illusion Shapes +Shapes that play tricks on the eye: + +- Polygon with inner cut-out — e.g., hexagon ring. +- Concentric circles / polygons — repeating, shrinking versions. +- ✅ Yin-Yang style curves. + +7. Parametric / Wavy Shapes +Using sinusoidal variation in radius: + +- Flower / Petal Shapes — r(Ξ) = base + sin(kΞ) * amplitude. +- Gear Shapes — like a circle but with teeth. \ No newline at end of file diff --git a/images/deformed_grid_checkerboard_35655.png b/images/deformed_grid_checkerboard_35655.png new file mode 100644 index 0000000..49b282c Binary files /dev/null and b/images/deformed_grid_checkerboard_35655.png differ diff --git a/images/deformed_grid_checkerboard_diagonal_21112.png b/images/deformed_grid_checkerboard_diagonal_21112.png new file mode 100644 index 0000000..7eb7b35 Binary files /dev/null and b/images/deformed_grid_checkerboard_diagonal_21112.png differ diff --git a/images/deformed_grid_checkerboard_diagonal_38601.png b/images/deformed_grid_checkerboard_diagonal_38601.png new file mode 100644 index 0000000..a13df37 Binary files /dev/null and b/images/deformed_grid_checkerboard_diagonal_38601.png differ diff --git a/images/deformed_grid_circular_14968.png b/images/deformed_grid_circular_14968.png new file mode 100644 index 0000000..a502876 Binary files /dev/null and b/images/deformed_grid_circular_14968.png differ diff --git a/images/deformed_grid_circular_7194.png b/images/deformed_grid_circular_7194.png new file mode 100644 index 0000000..e25bb0d Binary files /dev/null and b/images/deformed_grid_circular_7194.png differ diff --git a/images/deformed_grid_circular_77685.png b/images/deformed_grid_circular_77685.png new file mode 100644 index 0000000..87c37a7 Binary files /dev/null and b/images/deformed_grid_circular_77685.png differ diff --git a/images/deformed_grid_circular_80267.png b/images/deformed_grid_circular_80267.png new file mode 100644 index 0000000..83a0e0b Binary files /dev/null and b/images/deformed_grid_circular_80267.png differ diff --git a/images/deformed_grid_circular_8196.png b/images/deformed_grid_circular_8196.png new file mode 100644 index 0000000..9313e8f Binary files /dev/null and b/images/deformed_grid_circular_8196.png differ diff --git a/images/deformed_grid_curl_warp_63993.png b/images/deformed_grid_curl_warp_63993.png new file mode 100644 index 0000000..430c35c Binary files /dev/null and b/images/deformed_grid_curl_warp_63993.png differ diff --git a/images/deformed_grid_curl_warp_71945.png b/images/deformed_grid_curl_warp_71945.png new file mode 100644 index 0000000..065f270 Binary files /dev/null and b/images/deformed_grid_curl_warp_71945.png differ diff --git a/images/deformed_grid_fractal_noise_72943.png b/images/deformed_grid_fractal_noise_72943.png new file mode 100644 index 0000000..bb0a0a3 Binary files /dev/null and b/images/deformed_grid_fractal_noise_72943.png differ diff --git a/images/deformed_grid_fractal_noise_75369.png b/images/deformed_grid_fractal_noise_75369.png new file mode 100644 index 0000000..26dc767 Binary files /dev/null and b/images/deformed_grid_fractal_noise_75369.png differ diff --git a/images/deformed_grid_kaleidoscope_twist_5836.png b/images/deformed_grid_kaleidoscope_twist_5836.png new file mode 100644 index 0000000..2e3d621 Binary files /dev/null and b/images/deformed_grid_kaleidoscope_twist_5836.png differ diff --git a/images/deformed_grid_lens_55635.png b/images/deformed_grid_lens_55635.png new file mode 100644 index 0000000..3565a9b Binary files /dev/null and b/images/deformed_grid_lens_55635.png differ diff --git a/images/deformed_grid_noise_rotation_63039.png b/images/deformed_grid_noise_rotation_63039.png new file mode 100644 index 0000000..5c4d50e Binary files /dev/null and b/images/deformed_grid_noise_rotation_63039.png differ diff --git a/images/deformed_grid_perlin_85211.png b/images/deformed_grid_perlin_85211.png new file mode 100644 index 0000000..bb95627 Binary files /dev/null and b/images/deformed_grid_perlin_85211.png differ diff --git a/images/deformed_grid_pulse_10165.png b/images/deformed_grid_pulse_10165.png new file mode 100644 index 0000000..fb4931b Binary files /dev/null and b/images/deformed_grid_pulse_10165.png differ diff --git a/images/deformed_grid_shear_49571.png b/images/deformed_grid_shear_49571.png new file mode 100644 index 0000000..2e68739 Binary files /dev/null and b/images/deformed_grid_shear_49571.png differ diff --git a/images/deformed_grid_spiral_wave_1433.png b/images/deformed_grid_spiral_wave_1433.png deleted file mode 100644 index e711525..0000000 Binary files a/images/deformed_grid_spiral_wave_1433.png and /dev/null differ diff --git a/images/deformed_grid_spiral_wave_16786.png b/images/deformed_grid_spiral_wave_16786.png new file mode 100644 index 0000000..f2955af Binary files /dev/null and b/images/deformed_grid_spiral_wave_16786.png differ diff --git a/images/deformed_grid_spiral_wave_59903.png b/images/deformed_grid_spiral_wave_59903.png new file mode 100644 index 0000000..503e0a4 Binary files /dev/null and b/images/deformed_grid_spiral_wave_59903.png differ diff --git a/images/deformed_grid_swirl_23258.png b/images/deformed_grid_swirl_23258.png new file mode 100644 index 0000000..9419bd7 Binary files /dev/null and b/images/deformed_grid_swirl_23258.png differ diff --git a/images/deformed_grid_swirl_78117.png b/images/deformed_grid_swirl_78117.png new file mode 100644 index 0000000..66f1259 Binary files /dev/null and b/images/deformed_grid_swirl_78117.png differ diff --git a/images/deformed_grid_swirl_9005.png b/images/deformed_grid_swirl_9005.png new file mode 100644 index 0000000..2e1ddec Binary files /dev/null and b/images/deformed_grid_swirl_9005.png differ diff --git a/images/deformed_grid_tornado_3399.png b/images/deformed_grid_tornado_3399.png new file mode 100644 index 0000000..06261cc Binary files /dev/null and b/images/deformed_grid_tornado_3399.png differ diff --git a/images/deformed_grid_tornado_4306.png b/images/deformed_grid_tornado_4306.png new file mode 100644 index 0000000..8361f7b Binary files /dev/null and b/images/deformed_grid_tornado_4306.png differ diff --git a/images/deformed_grid_tornado_46839.png b/images/deformed_grid_tornado_46839.png new file mode 100644 index 0000000..5736575 Binary files /dev/null and b/images/deformed_grid_tornado_46839.png differ diff --git a/images/deformed_grid_tornado_7286.png b/images/deformed_grid_tornado_7286.png new file mode 100644 index 0000000..aee4c66 Binary files /dev/null and b/images/deformed_grid_tornado_7286.png differ diff --git a/implementation_plans/new_distortions.md b/implementation_plans/new_distortions.md deleted file mode 100644 index 7d6b3a8..0000000 --- a/implementation_plans/new_distortions.md +++ /dev/null @@ -1,40 +0,0 @@ -## New Distortion Types – Implementation Plan - -### Goal -Add a few visually compelling distortion types to `DistortionType` and implement them in `DistortionEngine`, with comprehensive unit tests and integration coverage. - -### Proposed New Distortion Types -- **SWIRL (`"swirl"`)**: Rotational swirl around the canvas center. Angle depends on distance to center and time, creating vortex-like motion. -- **RIPPLE (`"ripple"`)**: Concentric waves emanating from the center, but applied along the tangential direction (perpendicular to radius) to differentiate from radial displacement. -- **FLOW (`"flow"`)**: Time-evolving flow-field (pseudo curl-noise) that advects points using a smooth, coherent vector field of sin/cos combinations. - -### Acceptance Criteria -- `DistortionType` includes the new values: `swirl`, `ripple`, `flow`. -- `DistortionEngine` implements: `apply_distortion_swirl`, `apply_distortion_ripple`, `apply_distortion_flow`. -- `get_distorted_positions` routes the new types correctly. -- Unit tests: - - Updated enum tests for `DistortionType` (values and count). - - New tests that validate each new distortion returns a valid `(x, y, rotation)` and handles edge cases (e.g., center point for swirl/ripple). - - Parametrized tests include new types. -- Integration: iteration over all `DistortionType` works with `DeformedGrid`. -- ColorScheme enum aligns with existing color generation functionality and tests. - -### Steps -1. Update `enums.py`: - - Add `SWIRL`, `RIPPLE`, `FLOW` to `DistortionType`. - - Align `ColorScheme` members to match tested set (10 values): `monochrome`, `gradient`, `rainbow`, `complementary`, `temperature`, `pastel`, `neon`, `ocean`, `fire`, `forest`. -2. Implement new distortion functions in `distorsion_movement/distortions.py`: - - `apply_distortion_swirl(base_pos, params, cell_size, distortion_strength, time, canvas_size)` - - `apply_distortion_ripple(base_pos, params, cell_size, distortion_strength, time, canvas_size)` - - `apply_distortion_flow(base_pos, params, cell_size, distortion_strength, time)` - - Update `get_distorted_positions` to route new types. -3. Update tests: - - Modify `tests/test_enums.py` to reflect new `DistortionType` values and count; ensure `ColorScheme` count/values match. - - Extend `tests/test_distortions.py` with tests for `swirl`, `ripple`, `flow`, and expand parametrized coverage. -4. Run test suite and fix any issues. -5. Document briefly in `README.md` (optional in this iteration). - -### Notes -- Keep distortion outputs bounded using `cell_size * distortion_strength` as the primary magnitude scale, consistent with existing patterns. -- Handle singularities gracefully: at exact canvas center for center-based effects, return original position and zero rotation. - diff --git a/implementation_plans/new_shapes.md b/implementation_plans/new_shapes.md deleted file mode 100644 index db106db..0000000 --- a/implementation_plans/new_shapes.md +++ /dev/null @@ -1,151 +0,0 @@ -# New Shapes Implementation Plan - -## Overview -We're extending the DeformedGrid system to support multiple shape types beyond just squares. This will add visual variety and enable shape morphing animations. - -## Current System Analysis -- **Current Shape**: Only squares (drawn as rotated polygons) -- **Rendering Method**: `_draw_deformed_square()` in `DeformedGrid` class -- **Drawing**: Uses `pygame.draw.polygon()` for rotated squares -- **Position System**: Each cell has (x, y, rotation) from distortion engine -- **Colors**: Handled by `ColorGenerator` with various schemes - -## Implementation Plan - -### Phase 1: Core Shape System -1. **Add ShapeType Enum** (`enums.py`) - - SQUARE (current default) - - CIRCLE - - TRIANGLE - - HEXAGON - - STAR - - PENTAGON - - DIAMOND - -2. **Create Shape Renderer Module** (`shapes.py`) - - Abstract base class for shape rendering - - Concrete implementations for each shape type - - Support for rotation, size, and position - - Consistent interface: `draw(surface, x, y, rotation, size, color)` - -### Phase 2: Grid Integration -3. **Extend DeformedGrid Class** - - Add `shape_type` parameter to constructor - - Replace `_draw_deformed_square()` with `_draw_shape()` - - Support shape mixing (different shapes per cell) - - Add shape change controls to interactive mode - -4. **Update Demos** - - Add shape showcase to fullscreen demo - - Allow cycling through shape types - - Show mixed shape grids - -### Phase 3: Advanced Features -5. **Shape Morphing System** - - Interpolation between different shapes - - Breathing/pulsing animations - - Size variation for depth perception - - Time-based morphing patterns - -6. **Shape Variations** - - Variable polygon sides (3-12) - - Star shapes with different point counts - - Organic shapes using bezier curves - - Custom polygon definitions - -## Technical Details - -### Shape Renderer Architecture -```python -class ShapeRenderer: - @staticmethod - def draw_circle(surface, x, y, rotation, size, color): - # pygame.draw.circle() implementation - - @staticmethod - def draw_triangle(surface, x, y, rotation, size, color): - # Calculate triangle vertices with rotation - - @staticmethod - def draw_hexagon(surface, x, y, rotation, size, color): - # Calculate hexagon vertices with rotation - - # ... other shapes -``` - -### Grid Integration -- Replace `_draw_deformed_square()` with `_draw_shape()` -- Add shape selection logic -- Support per-cell shape types (for mixed grids) -- Maintain backward compatibility - -### Controls Extension -- **H**: Cycle through shape types (H for sHapes) -- **Shift+H**: Toggle mixed shape mode -- **Ctrl+H**: Enable shape morphing -- Existing controls remain unchanged (S still saves images) - -## Implementation Steps - -### Step 1: Shape Type Enum -- Add `ShapeType` enum to `enums.py` -- Write unit tests for enum - -### Step 2: Shape Renderer Module -- Create `shapes.py` with all shape drawing functions -- Implement 7 basic shapes with rotation support -- Write comprehensive unit tests - -### Step 3: Grid Modification -- Add shape_type parameter to DeformedGrid -- Replace square-specific code with generic shape rendering -- Add shape cycling to interactive controls -- Update constructor and parameter handling - -### Step 4: Demo Updates -- Modify fullscreen demo to showcase shapes -- Add shape cycling demonstration -- Show mixed shape grids - -### Step 5: Advanced Features -- Implement shape morphing system -- Add size variation algorithms -- Create breathing/pulsing animations - -## Testing Strategy -- Unit tests for each shape renderer function -- Integration tests for grid with different shapes -- Visual regression tests (save/compare images) -- Performance tests (rendering speed with different shapes) -- Interactive testing of all new controls - -## File Structure Changes -``` -distorsion_movement/ -├── enums.py # Add ShapeType enum -├── shapes.py # NEW: Shape rendering module -├── deformed_grid.py # Modified: Add shape support -├── demos.py # Modified: Add shape demos -└── tests/ - ├── test_enums.py # Add ShapeType tests - ├── test_shapes.py # NEW: Shape renderer tests - ├── test_deformed_grid.py # Modified: Add shape tests - └── test_integration.py # Modified: Add shape integration tests -``` - -## Success Criteria -1. ✅ All 7 basic shapes render correctly with rotation -2. ✅ Shape cycling works in interactive mode -3. ✅ Mixed shape grids display properly -4. ✅ Performance remains acceptable (60 FPS target) -5. ✅ All unit tests pass with 95%+ coverage -6. ✅ Backward compatibility maintained -7. ✅ Demo showcases all new features effectively - -## Future Enhancements (Beyond Initial Implementation) -- 3D-looking shapes with perspective -- Text characters as shapes -- Fractal shapes with recursive patterns -- Shape physics (collision, gravity) -- User-defined custom shapes -- Shape particle systems \ No newline at end of file diff --git a/implementation_plans/new_shapes_again.md b/implementation_plans/new_shapes_again.md deleted file mode 100644 index e158fd4..0000000 --- a/implementation_plans/new_shapes_again.md +++ /dev/null @@ -1,56 +0,0 @@ -1. More Regular Polygons -You already have 3–6 sides covered; adding more could allow for patterns that feel more “tiling-like” or mandala-like: - -Octagon (8 sides) — great for symmetric, radial compositions. - -Heptagon (7 sides) — slightly asymmetrical feel compared to hexagon/octagon. - -Nonagon (9 sides) — nice when combined with rotation animations. - -2. Curved / Organic Shapes -These can make your generative art look less rigid: - -Ellipse — a stretched circle; rotation makes it feel dynamic. - -Arc / Semi-Circle — partial curves work beautifully in repetitive patterns. - -Leaf Shape — two mirrored arcs meeting at sharp tips. - -3. Complex Geometric Stars -You have a 5-point star — but variations can really spice things up: - -6, 7, or 8-point stars — by alternating inner/outer radii in the same pattern you used. - -Spirograph-like stars — computed from sine/cosine with more complex frequency ratios. - -4. Crosses & Plus Signs -Minimalist but striking when repeated: - -Cross / X-shape — can be rotated in interesting ways. - -“Asterisk” — a star made from straight lines instead of polygons. - -5. Fractal / Iterative Shapes -These are great for generative, especially with variable recursion depth: - -Koch Snowflake — fractal triangle-based snowflake. - -Sierpinski Triangle — hollow, recursive triangles. - -Fractal Tree — if you want organic growth patterns. - -6. Pattern & Optical Illusion Shapes -Shapes that play tricks on the eye: - -Polygon with inner cut-out — e.g., hexagon ring. - -Concentric circles / polygons — repeating, shrinking versions. - -Yin-Yang style curves. - -7. Parametric / Wavy Shapes -Using sinusoidal variation in radius: - -Flower / Petal Shapes — r(Ξ) = base + sin(kΞ) * amplitude. - -Gear Shapes — like a circle but with teeth. \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index a497bb4..029bb66 100644 --- a/pytest.ini +++ b/pytest.ini @@ -13,5 +13,4 @@ addopts = markers = unit: Unit tests integration: Integration tests - audio: Tests requiring audio hardware (may be skipped) slow: Slow tests that take more than a few seconds \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 67a6ae2..f7dd0bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,23 @@ contourpy==1.3.3 +coverage==7.10.2 cycler==0.12.1 fonttools==4.59.0 +imageio==2.37.0 +iniconfig==2.1.0 kiwisolver==1.4.8 matplotlib==3.10.5 numpy==2.3.2 packaging==25.0 pillow==11.3.0 +pluggy==1.6.0 +pygame==2.6.1 +pygbag==0.9.2 +Pygments==2.19.2 pyparsing==3.2.3 +pytest==8.4.1 +pytest-cov==6.2.1 +pytest-mock==3.14.1 python-dateutil==2.9.0.post0 +PyYAML==6.0.2 +scipy==1.16.1 six==1.17.0 -pygame==2.6.1 -# Audio reactivity dependencies (optional) -pyaudio>=0.2.11 -scipy>=1.9.0 -# GIF creation dependencies -imageio>=2.34.0 diff --git a/saved_params/deformed_grid_checkerboard_35655.yaml b/saved_params/deformed_grid_checkerboard_35655.yaml new file mode 100644 index 0000000..989cca7 --- /dev/null +++ b/saved_params/deformed_grid_checkerboard_35655.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 18 +color_animation: true +color_scheme: desert +dimension: 32 +distortion_fn: checkerboard +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:49:50.465801' +saved_filename: deformed_grid_checkerboard_35655.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 356.55999999996567 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_checkerboard_diagonal_21112.yaml b/saved_params/deformed_grid_checkerboard_diagonal_21112.yaml new file mode 100644 index 0000000..c3da580 --- /dev/null +++ b/saved_params/deformed_grid_checkerboard_diagonal_21112.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 24 +color_animation: true +color_scheme: desert +dimension: 24 +distortion_fn: checkerboard_diagonal +distortion_strength: 1.0 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T12:41:41.283030' +saved_filename: deformed_grid_checkerboard_diagonal_21112.yaml +shape_type: square +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 211.1200000000342 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_checkerboard_diagonal_38601.yaml b/saved_params/deformed_grid_checkerboard_diagonal_38601.yaml new file mode 100644 index 0000000..c968830 --- /dev/null +++ b/saved_params/deformed_grid_checkerboard_diagonal_38601.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 24 +color_animation: true +color_scheme: candy_shop +dimension: 24 +distortion_fn: checkerboard_diagonal +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:50:15.557595' +saved_filename: deformed_grid_checkerboard_diagonal_38601.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 386.0199999999389 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_circular_14968.yaml b/saved_params/deformed_grid_circular_14968.yaml new file mode 100644 index 0000000..aa57681 --- /dev/null +++ b/saved_params/deformed_grid_circular_14968.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 9 +color_animation: true +color_scheme: gradient +dimension: 64 +distortion_fn: circular +distortion_strength: 1.0 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:46:53.127177' +saved_filename: deformed_grid_circular_14968.yaml +shape_type: square +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 149.68000000000276 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_circular_7194.yaml b/saved_params/deformed_grid_circular_7194.yaml new file mode 100644 index 0000000..c639d50 --- /dev/null +++ b/saved_params/deformed_grid_circular_7194.yaml @@ -0,0 +1,31 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 24 +color_animation: true +color_scheme: cyberpunk +dimension: 24 +distortion_fn: circular +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +saved_at: '2025-08-09T12:01:06.749172' +saved_filename: deformed_grid_circular_7194.yaml +shape_type: square +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 71.94000000000283 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_circular_77685.yaml b/saved_params/deformed_grid_circular_77685.yaml new file mode 100644 index 0000000..67fd73e --- /dev/null +++ b/saved_params/deformed_grid_circular_77685.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 5 +color_animation: true +color_scheme: monochrome +dimension: 104 +distortion_fn: circular +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 110 +offset_y: 110 +saved_at: '2025-08-09T14:04:34.053634' +saved_filename: deformed_grid_circular_77685.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 776.8599999995835 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_circular_80267.yaml b/saved_params/deformed_grid_circular_80267.yaml new file mode 100644 index 0000000..3855a61 --- /dev/null +++ b/saved_params/deformed_grid_circular_80267.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 74 +color_animation: true +color_scheme: cyberpunk +dimension: 8 +distortion_fn: circular +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 74 +offset_y: 74 +saved_at: '2025-08-09T13:56:24.721553' +saved_filename: deformed_grid_circular_80267.yaml +shape_type: koch_snowflake +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 802.67999999956 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_circular_8196.yaml b/saved_params/deformed_grid_circular_8196.yaml new file mode 100644 index 0000000..30e146e --- /dev/null +++ b/saved_params/deformed_grid_circular_8196.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 12 +color_animation: true +color_scheme: black_white_radial +dimension: 48 +distortion_fn: circular +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:59:33.658944' +saved_filename: deformed_grid_circular_8196.yaml +shape_type: square +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 81.96000000000083 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_curl_warp_63993.yaml b/saved_params/deformed_grid_curl_warp_63993.yaml new file mode 100644 index 0000000..3b74271 --- /dev/null +++ b/saved_params/deformed_grid_curl_warp_63993.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 12 +color_animation: true +color_scheme: analogous +dimension: 48 +distortion_fn: curl_warp +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:54:06.339613' +saved_filename: deformed_grid_curl_warp_63993.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 639.939999999708 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_fractal_noise_75369.yaml b/saved_params/deformed_grid_fractal_noise_75369.yaml new file mode 100644 index 0000000..15fec64 --- /dev/null +++ b/saved_params/deformed_grid_fractal_noise_75369.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 18 +color_animation: true +color_scheme: pastel +dimension: 32 +distortion_fn: fractal_noise +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:55:41.736090' +saved_filename: deformed_grid_fractal_noise_75369.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 753.6999999996045 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_kaleidoscope_twist_5836.yaml b/saved_params/deformed_grid_kaleidoscope_twist_5836.yaml new file mode 100644 index 0000000..09e1057 --- /dev/null +++ b/saved_params/deformed_grid_kaleidoscope_twist_5836.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 9 +color_animation: true +color_scheme: black_white_radial +dimension: 64 +distortion_fn: kaleidoscope_twist +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:59:13.646300' +saved_filename: deformed_grid_kaleidoscope_twist_5836.yaml +shape_type: square +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 58.360000000003524 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_lens_55635.yaml b/saved_params/deformed_grid_lens_55635.yaml new file mode 100644 index 0000000..1b95792 --- /dev/null +++ b/saved_params/deformed_grid_lens_55635.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 18 +color_animation: true +color_scheme: pastel +dimension: 32 +distortion_fn: lens +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:52:55.566557' +saved_filename: deformed_grid_lens_55635.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 556.359999999784 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_noise_rotation_63039.yaml b/saved_params/deformed_grid_noise_rotation_63039.yaml new file mode 100644 index 0000000..8501477 --- /dev/null +++ b/saved_params/deformed_grid_noise_rotation_63039.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 37 +color_animation: true +color_scheme: analogous +dimension: 16 +distortion_fn: noise_rotation +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 74 +offset_y: 74 +saved_at: '2025-08-09T13:53:58.327354' +saved_filename: deformed_grid_noise_rotation_63039.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 630.3999999997167 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_perlin_85211.yaml b/saved_params/deformed_grid_perlin_85211.yaml new file mode 100644 index 0000000..7c6ad13 --- /dev/null +++ b/saved_params/deformed_grid_perlin_85211.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 24 +color_animation: true +color_scheme: monochrome +dimension: 24 +distortion_fn: perlin +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T14:05:46.599695' +saved_filename: deformed_grid_perlin_85211.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 852.119999999515 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_pulse_10165.yaml b/saved_params/deformed_grid_pulse_10165.yaml new file mode 100644 index 0000000..7916b4c --- /dev/null +++ b/saved_params/deformed_grid_pulse_10165.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 12 +color_animation: true +color_scheme: rainbow +dimension: 48 +distortion_fn: pulse +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:59:50.263496' +saved_filename: deformed_grid_pulse_10165.yaml +shape_type: square +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 101.65999999999691 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_shear_49571.yaml b/saved_params/deformed_grid_shear_49571.yaml new file mode 100644 index 0000000..7cbb25d --- /dev/null +++ b/saved_params/deformed_grid_shear_49571.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 24 +color_animation: true +color_scheme: cyberpunk +dimension: 24 +distortion_fn: shear +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:52:04.002622' +saved_filename: deformed_grid_shear_49571.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 495.7199999998391 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_spiral_wave_16786.yaml b/saved_params/deformed_grid_spiral_wave_16786.yaml new file mode 100644 index 0000000..be41140 --- /dev/null +++ b/saved_params/deformed_grid_spiral_wave_16786.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 74 +color_animation: true +color_scheme: metallics +dimension: 8 +distortion_fn: spiral_wave +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 74 +offset_y: 74 +saved_at: '2025-08-09T14:00:46.353038' +saved_filename: deformed_grid_spiral_wave_16786.yaml +shape_type: square +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 167.86000000001206 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_spiral_wave_59903.yaml b/saved_params/deformed_grid_spiral_wave_59903.yaml new file mode 100644 index 0000000..2573e17 --- /dev/null +++ b/saved_params/deformed_grid_spiral_wave_59903.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 18 +color_animation: true +color_scheme: sunset +dimension: 32 +distortion_fn: spiral_wave +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:53:31.686425' +saved_filename: deformed_grid_spiral_wave_59903.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 599.0399999997452 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_swirl_23258.yaml b/saved_params/deformed_grid_swirl_23258.yaml new file mode 100644 index 0000000..ca66b00 --- /dev/null +++ b/saved_params/deformed_grid_swirl_23258.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 12 +color_animation: true +color_scheme: gradient +dimension: 48 +distortion_fn: swirl +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:48:02.362928' +saved_filename: deformed_grid_swirl_23258.yaml +shape_type: square +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 232.58000000004517 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_swirl_78117.yaml b/saved_params/deformed_grid_swirl_78117.yaml new file mode 100644 index 0000000..c77d97e --- /dev/null +++ b/saved_params/deformed_grid_swirl_78117.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 5 +color_animation: true +color_scheme: monochrome +dimension: 104 +distortion_fn: swirl +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 110 +offset_y: 110 +saved_at: '2025-08-09T14:04:41.250547' +saved_filename: deformed_grid_swirl_78117.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 781.1799999995795 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_swirl_9005.yaml b/saved_params/deformed_grid_swirl_9005.yaml new file mode 100644 index 0000000..c76a056 --- /dev/null +++ b/saved_params/deformed_grid_swirl_9005.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 12 +color_animation: true +color_scheme: gradient +dimension: 48 +distortion_fn: swirl +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:59:40.514531' +saved_filename: deformed_grid_swirl_9005.yaml +shape_type: square +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 90.05999999999922 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_tornado_3399.yaml b/saved_params/deformed_grid_tornado_3399.yaml new file mode 100644 index 0000000..88fa21d --- /dev/null +++ b/saved_params/deformed_grid_tornado_3399.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 9 +color_animation: true +color_scheme: monochrome +dimension: 64 +distortion_fn: tornado +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:58:51.864892' +saved_filename: deformed_grid_tornado_3399.yaml +shape_type: square +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 33.999999999999716 +windowed_size: +- 740 +- 740 diff --git a/saved_params/deformed_grid_tornado_46839.yaml b/saved_params/deformed_grid_tornado_46839.yaml new file mode 100644 index 0000000..7a9ca4c --- /dev/null +++ b/saved_params/deformed_grid_tornado_46839.yaml @@ -0,0 +1,33 @@ +animation_speed: 0.02 +background_color: +- 20 +- 20 +- 30 +base_dimension: 32 +canvas_size: +- 740 +- 740 +cell_size: 18 +color_animation: true +color_scheme: complementary +dimension: 32 +distortion_fn: tornado +distortion_strength: 1 +grid_density_mode: true +is_fullscreen: false +mixed_shapes: false +offset_x: 82 +offset_y: 82 +saved_at: '2025-08-09T13:51:40.776699' +saved_filename: deformed_grid_tornado_46839.yaml +shape_type: diamond +show_help: false +show_status: false +square_color: +- 255 +- 255 +- 255 +time: 468.39999999986395 +windowed_size: +- 740 +- 740