├── .DS_Store ├── simulation ├── .DS_Store ├── sample-images │ ├── cat.jpg │ ├── dog.jpg │ └── .DS_Store ├── index.html ├── script.js └── style.css ├── dist ├── sample-images │ ├── cat.jpg │ ├── dog.jpg │ └── .DS_Store ├── README.md ├── server.py ├── index.html ├── script.js └── style.css ├── image-augmentation-playground.tar.gz ├── README.md └── server.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/learn_simulation-augmentation/main/.DS_Store -------------------------------------------------------------------------------- /simulation/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/learn_simulation-augmentation/main/simulation/.DS_Store -------------------------------------------------------------------------------- /dist/sample-images/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/learn_simulation-augmentation/main/dist/sample-images/cat.jpg -------------------------------------------------------------------------------- /dist/sample-images/dog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/learn_simulation-augmentation/main/dist/sample-images/dog.jpg -------------------------------------------------------------------------------- /dist/sample-images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/learn_simulation-augmentation/main/dist/sample-images/.DS_Store -------------------------------------------------------------------------------- /simulation/sample-images/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/learn_simulation-augmentation/main/simulation/sample-images/cat.jpg -------------------------------------------------------------------------------- /simulation/sample-images/dog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/learn_simulation-augmentation/main/simulation/sample-images/dog.jpg -------------------------------------------------------------------------------- /image-augmentation-playground.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/learn_simulation-augmentation/main/image-augmentation-playground.tar.gz -------------------------------------------------------------------------------- /simulation/sample-images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/learn_simulation-augmentation/main/simulation/sample-images/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image Augmentation Playground 2 | 3 | An interactive, web-based simulation for exploring image augmentation techniques used in machine learning and computer vision. This simulation helps learners understand how different augmentation methods transform images and their impact on model training. 4 | 5 | ## Features 6 | 7 | ### 🖼️ Interactive Image Display 8 | - **Side-by-side comparison**: Original and augmented images displayed simultaneously 9 | - **Real-time updates**: See changes instantly as you adjust parameters 10 | - **Multiple sample images**: Choose from different image types to experiment with 11 | 12 | ### 🔧 Augmentation Techniques 13 | 14 | #### Geometric Transformations 15 | - **Flip Horizontal/Vertical**: Mirror images across axes 16 | - **Rotation**: Rotate images by any angle (-180° to 180°) 17 | - **Cropping**: Simulate center cropping with adjustable intensity 18 | 19 | #### Color Adjustments 20 | - **Brightness**: Adjust image brightness (0% to 200%) 21 | - **Contrast**: Modify image contrast for better visibility 22 | - **Saturation**: Control color intensity and vibrancy 23 | 24 | #### Noise & Blur Effects 25 | - **Noise**: Add random noise to simulate real-world imperfections 26 | - **Blur**: Apply Gaussian blur effects 27 | 28 | ### 🎮 Interactive Controls 29 | - **Slider controls**: Fine-tune augmentation parameters with real-time feedback 30 | - **Toggle buttons**: Quick on/off for flip transformations 31 | - **Reset functionality**: Return all parameters to default values 32 | - **Image selector**: Switch between different sample images 33 | 34 | ### 📚 Educational Features 35 | - **Learning tips**: Built-in guidance on when and why to use each technique 36 | - **Visual feedback**: Immediate understanding of parameter effects 37 | - **Best practices**: Information about combining techniques effectively 38 | 39 | ## Setup 40 | 41 | ### Prerequisites 42 | - Python 3.6 or higher 43 | - Optional: Pillow (PIL) for automatic sample image generation 44 | 45 | ### Installation 46 | 47 | 1. **Clone or navigate to the simulation directory**: 48 | ```bash 49 | cd image-augmentation-playground 50 | ``` 51 | 52 | 2. **Install optional dependencies** (for sample image generation): 53 | ```bash 54 | pip install Pillow 55 | ``` 56 | 57 | 3. **Start the server**: 58 | ```bash 59 | python server.py 60 | ``` 61 | 62 | 4. **Open your browser** and navigate to `http://localhost:3000` 63 | 64 | ## Usage 65 | 66 | ### Getting Started 67 | 1. **Choose an image**: Select from the dropdown menu (Cat, Dog, Building, Landscape) 68 | 2. **Apply augmentations**: Use the controls to transform the image 69 | 3. **Compare results**: See original vs augmented images side by side 70 | 4. **Experiment**: Try different combinations of techniques 71 | 5. **Reset**: Use the reset button to start over 72 | 73 | ### Understanding the Techniques 74 | 75 | #### When to Use Geometric Transformations 76 | - **Flipping**: When object orientation doesn't matter (e.g., faces, objects) 77 | - **Rotation**: For objects that can appear in any orientation 78 | - **Cropping**: To focus on important parts of the image 79 | 80 | #### When to Use Color Adjustments 81 | - **Brightness**: To handle different lighting conditions 82 | - **Contrast**: To improve feature visibility 83 | - **Saturation**: To handle different color conditions 84 | 85 | #### When to Use Noise & Blur 86 | - **Noise**: To make models robust to sensor noise 87 | - **Blur**: To handle motion blur or focus issues 88 | 89 | ### Best Practices 90 | - **Start simple**: Begin with one technique at a time 91 | - **Combine techniques**: Use multiple augmentations for more diverse training data 92 | - **Consider your domain**: Different techniques work better for different types of images 93 | - **Monitor performance**: Always validate that augmentations improve model performance 94 | 95 | ## Technical Details 96 | 97 | ### Architecture 98 | - **Backend**: Python HTTP server with CORS support 99 | - **Frontend**: Vanilla HTML, CSS, and JavaScript 100 | - **Image Processing**: HTML5 Canvas API for real-time transformations 101 | - **Port**: Serves on `localhost:3000` 102 | 103 | ### Browser Compatibility 104 | - Modern browsers with HTML5 Canvas support 105 | - Works on desktop and mobile devices 106 | - Responsive design for different screen sizes 107 | 108 | ### File Structure 109 | ``` 110 | image-augmentation-playground/ 111 | ├── server.py # Python HTTP server 112 | ├── README.md # This file 113 | └── simulation/ 114 | ├── index.html # Main HTML interface 115 | ├── style.css # Styling and responsive design 116 | ├── script.js # Augmentation logic and interactions 117 | └── sample-images/ # Sample images for experimentation 118 | ├── cat.jpg 119 | ├── dog.jpg 120 | ├── building.jpg 121 | └── landscape.jpg 122 | ``` 123 | 124 | ## Learning Outcomes 125 | 126 | After using this simulation, learners will understand: 127 | 128 | 1. **How different augmentation techniques work** and their visual effects 129 | 2. **When to apply specific techniques** based on the problem domain 130 | 3. **The impact of parameter tuning** on augmentation results 131 | 4. **How to combine multiple techniques** for effective data augmentation 132 | 5. **Real-world applications** of image augmentation in machine learning 133 | 134 | ## Contributing 135 | 136 | This simulation is part of the Learn Bespoke Simulations collection. To contribute: 137 | 138 | 1. Follow the existing code patterns and educational philosophy 139 | 2. Ensure all changes maintain the interactive, visual-first approach 140 | 3. Test on multiple browsers and devices 141 | 4. Update documentation for any new features 142 | 143 | ## License 144 | 145 | Part of the Learn Bespoke Simulations project for educational use. 146 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # Image Augmentation Playground 2 | 3 | An interactive, web-based simulation for exploring image augmentation techniques used in machine learning and computer vision. This simulation helps learners understand how different augmentation methods transform images and their impact on model training. 4 | 5 | ## Features 6 | 7 | ### 🖼️ Interactive Image Display 8 | - **Side-by-side comparison**: Original and augmented images displayed simultaneously 9 | - **Real-time updates**: See changes instantly as you adjust parameters 10 | - **Multiple sample images**: Choose from different image types to experiment with 11 | 12 | ### 🔧 Augmentation Techniques 13 | 14 | #### Geometric Transformations 15 | - **Flip Horizontal/Vertical**: Mirror images across axes 16 | - **Rotation**: Rotate images by any angle (-180° to 180°) 17 | - **Cropping**: Simulate center cropping with adjustable intensity 18 | 19 | #### Color Adjustments 20 | - **Brightness**: Adjust image brightness (0% to 200%) 21 | - **Contrast**: Modify image contrast for better visibility 22 | - **Saturation**: Control color intensity and vibrancy 23 | 24 | #### Noise & Blur Effects 25 | - **Noise**: Add random noise to simulate real-world imperfections 26 | - **Blur**: Apply Gaussian blur effects 27 | 28 | ### 🎮 Interactive Controls 29 | - **Slider controls**: Fine-tune augmentation parameters with real-time feedback 30 | - **Toggle buttons**: Quick on/off for flip transformations 31 | - **Reset functionality**: Return all parameters to default values 32 | - **Image selector**: Switch between different sample images 33 | 34 | ### 📚 Educational Features 35 | - **Learning tips**: Built-in guidance on when and why to use each technique 36 | - **Visual feedback**: Immediate understanding of parameter effects 37 | - **Best practices**: Information about combining techniques effectively 38 | 39 | ## Setup 40 | 41 | ### Prerequisites 42 | - Python 3.6 or higher 43 | 44 | ### Local Development 45 | 46 | 1. **Clone or navigate to the simulation directory**: 47 | ```bash 48 | cd image-augmentation-playground 49 | ``` 50 | 51 | 2. **Start the server**: 52 | ```bash 53 | python3 server.py 54 | ``` 55 | 56 | 3. **Open your browser** and navigate to `http://localhost:3000` 57 | 58 | ### CodeSignal Deployment 59 | 60 | For CodeSignal Learn platform deployment: 61 | 62 | 1. **Download the distribution package**: 63 | ```bash 64 | wget https://github.com/your-username/image-augmentation-playground/releases/latest/download/image-augmentation-playground.tar.gz 65 | ``` 66 | 67 | 2. **Extract the package**: 68 | ```bash 69 | tar xvzf image-augmentation-playground.tar.gz 70 | ``` 71 | 72 | 3. **Start the server**: 73 | ```bash 74 | python3 server.py 75 | ``` 76 | 77 | The simulation will be available at the provided URL. 78 | 79 | ## Usage 80 | 81 | ### Getting Started 82 | 1. **Choose an image**: Select from the dropdown menu (Cat, Dog, Building, Landscape) 83 | 2. **Apply augmentations**: Use the controls to transform the image 84 | 3. **Compare results**: See original vs augmented images side by side 85 | 4. **Experiment**: Try different combinations of techniques 86 | 5. **Reset**: Use the reset button to start over 87 | 88 | ### Understanding the Techniques 89 | 90 | #### When to Use Geometric Transformations 91 | - **Flipping**: When object orientation doesn't matter (e.g., faces, objects) 92 | - **Rotation**: For objects that can appear in any orientation 93 | - **Cropping**: To focus on important parts of the image 94 | 95 | #### When to Use Color Adjustments 96 | - **Brightness**: To handle different lighting conditions 97 | - **Contrast**: To improve feature visibility 98 | - **Saturation**: To handle different color conditions 99 | 100 | #### When to Use Noise & Blur 101 | - **Noise**: To make models robust to sensor noise 102 | - **Blur**: To handle motion blur or focus issues 103 | 104 | ### Best Practices 105 | - **Start simple**: Begin with one technique at a time 106 | - **Combine techniques**: Use multiple augmentations for more diverse training data 107 | - **Consider your domain**: Different techniques work better for different types of images 108 | - **Monitor performance**: Always validate that augmentations improve model performance 109 | 110 | ## Technical Details 111 | 112 | ### Architecture 113 | - **Backend**: Python HTTP server with CORS support 114 | - **Frontend**: Vanilla HTML, CSS, and JavaScript 115 | - **Image Processing**: HTML5 Canvas API for real-time transformations 116 | - **Port**: Serves on `localhost:3000` 117 | 118 | ### Browser Compatibility 119 | - Modern browsers with HTML5 Canvas support 120 | - Works on desktop and mobile devices 121 | - Responsive design for different screen sizes 122 | 123 | ### File Structure 124 | ``` 125 | image-augmentation-playground/ 126 | ├── server.py # Python HTTP server 127 | ├── README.md # This file 128 | └── simulation/ 129 | ├── index.html # Main HTML interface 130 | ├── style.css # Styling and responsive design 131 | ├── script.js # Augmentation logic and interactions 132 | └── sample-images/ # Sample images for experimentation 133 | ├── cat.jpg 134 | ├── dog.jpg 135 | ├── building.jpg 136 | └── landscape.jpg 137 | ``` 138 | 139 | ## Learning Outcomes 140 | 141 | After using this simulation, learners will understand: 142 | 143 | 1. **How different augmentation techniques work** and their visual effects 144 | 2. **When to apply specific techniques** based on the problem domain 145 | 3. **The impact of parameter tuning** on augmentation results 146 | 4. **How to combine multiple techniques** for effective data augmentation 147 | 5. **Real-world applications** of image augmentation in machine learning 148 | 149 | ## Contributing 150 | 151 | This simulation is part of the Learn Bespoke Simulations collection. To contribute: 152 | 153 | 1. Follow the existing code patterns and educational philosophy 154 | 2. Ensure all changes maintain the interactive, visual-first approach 155 | 3. Test on multiple browsers and devices 156 | 4. Update documentation for any new features 157 | 158 | ## License 159 | 160 | Part of the Learn Bespoke Simulations project for educational use. 161 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Image Augmentation Playground Server 4 | 5 | A simple HTTP server for the Image Augmentation Playground simulation. 6 | """ 7 | 8 | import http.server 9 | import socketserver 10 | import os 11 | import sys 12 | import json 13 | from pathlib import Path 14 | from urllib.parse import urlparse 15 | 16 | # Configuration 17 | PORT = 3000 18 | HOST = "0.0.0.0" 19 | 20 | class SimpleHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): 21 | """Simple request handler with CORS support.""" 22 | 23 | def end_headers(self): 24 | # Add CORS headers 25 | self.send_header('Access-Control-Allow-Origin', '*') 26 | self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 27 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 28 | super().end_headers() 29 | 30 | def do_GET(self): 31 | """Handle GET requests.""" 32 | parsed_path = urlparse(self.path) 33 | 34 | # API endpoints 35 | if parsed_path.path == '/api/sample-images': 36 | self.handle_sample_images() 37 | return 38 | elif parsed_path.path == '/api/health': 39 | self.handle_health() 40 | return 41 | 42 | # Serve static files 43 | super().do_GET() 44 | 45 | def handle_sample_images(self): 46 | """API endpoint to get available sample images.""" 47 | try: 48 | sample_images_dir = Path("sample-images") 49 | 50 | if not sample_images_dir.exists(): 51 | sample_images_dir.mkdir(parents=True, exist_ok=True) 52 | self.create_sample_images() 53 | 54 | # Get list of image files 55 | image_files = [] 56 | for file_path in sample_images_dir.iterdir(): 57 | if file_path.is_file() and file_path.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: 58 | image_files.append({ 59 | 'filename': file_path.name, 60 | 'name': file_path.stem.replace('_', ' ').replace('-', ' ').title(), 61 | 'url': f'/sample-images/{file_path.name}' 62 | }) 63 | 64 | # Sort by name 65 | image_files.sort(key=lambda x: x['name']) 66 | 67 | self.send_response(200) 68 | self.send_header('Content-Type', 'application/json') 69 | self.end_headers() 70 | self.wfile.write(json.dumps(image_files).encode()) 71 | 72 | except Exception as e: 73 | print(f"Error in handle_sample_images: {e}") 74 | self.send_error(500, "Internal server error") 75 | 76 | def handle_health(self): 77 | """Health check endpoint.""" 78 | health_data = { 79 | 'status': 'healthy', 80 | 'service': 'Image Augmentation Playground' 81 | } 82 | 83 | self.send_response(200) 84 | self.send_header('Content-Type', 'application/json') 85 | self.end_headers() 86 | self.wfile.write(json.dumps(health_data).encode()) 87 | 88 | def create_sample_images(self): 89 | """Check for existing sample images.""" 90 | sample_images_dir = Path("sample-images") 91 | 92 | # Just check what images exist - don't create any new ones 93 | existing_images = [] 94 | for file_path in sample_images_dir.iterdir(): 95 | if file_path.is_file() and file_path.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: 96 | existing_images.append(file_path.name) 97 | 98 | if existing_images: 99 | print(f"✅ Found existing images: {', '.join(existing_images)}") 100 | else: 101 | print("ℹ️ No sample images found. Add JPG/PNG files to sample-images/ directory") 102 | 103 | def log_message(self, format, *args): 104 | """Custom log format.""" 105 | print(f"[{self.log_date_time_string()}] {format % args}") 106 | 107 | def main(): 108 | """Main server function.""" 109 | print("🖼️ Image Augmentation Playground Server") 110 | print("=" * 50) 111 | 112 | # Change to the simulation directory 113 | simulation_dir = Path(__file__).parent / "simulation" 114 | if not simulation_dir.exists(): 115 | print(f"Error: Simulation directory not found: {simulation_dir}") 116 | sys.exit(1) 117 | 118 | # Remove the nested simulation directory if it exists 119 | nested_sim_dir = simulation_dir / "simulation" 120 | if nested_sim_dir.exists(): 121 | print(f"Removing nested simulation directory: {nested_sim_dir}") 122 | import shutil 123 | shutil.rmtree(nested_sim_dir) 124 | 125 | os.chdir(simulation_dir) 126 | print(f"Serving from: {simulation_dir.absolute()}") 127 | 128 | # Create the server 129 | try: 130 | with socketserver.TCPServer((HOST, PORT), SimpleHTTPRequestHandler) as httpd: 131 | print(f"🚀 Server running at http://{HOST}:{PORT}") 132 | print(f"🌐 Open your browser and navigate to: http://localhost:{PORT}") 133 | print("📱 The simulation works on desktop and mobile devices") 134 | print("\n💡 Features:") 135 | print(" • Interactive image augmentation techniques") 136 | print(" • Real-time visual feedback") 137 | print(" • Multiple sample images to experiment with") 138 | print(" • Geometric transformations (flip, rotate, crop)") 139 | print(" • Color adjustments (brightness, contrast, saturation)") 140 | print(" • Noise and blur effects") 141 | print("\n⌨️ Press Ctrl+C to stop the server") 142 | print("=" * 50) 143 | 144 | httpd.serve_forever() 145 | 146 | except KeyboardInterrupt: 147 | print("\n\n🛑 Server stopped by user") 148 | except OSError as e: 149 | if e.errno == 48: # Address already in use 150 | print(f"❌ Error: Port {PORT} is already in use") 151 | print(f" Try a different port or stop the process using port {PORT}") 152 | else: 153 | print(f"❌ Error starting server: {e}") 154 | sys.exit(1) 155 | except Exception as e: 156 | print(f"❌ Unexpected error: {e}") 157 | sys.exit(1) 158 | 159 | if __name__ == "__main__": 160 | main() -------------------------------------------------------------------------------- /dist/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Image Augmentation Playground Server 4 | 5 | A simple HTTP server for the Image Augmentation Playground simulation. 6 | """ 7 | 8 | import http.server 9 | import socketserver 10 | import os 11 | import sys 12 | import json 13 | from pathlib import Path 14 | from urllib.parse import urlparse 15 | 16 | # Configuration 17 | PORT = 3000 18 | HOST = "0.0.0.0" 19 | 20 | class SimpleHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): 21 | """Simple request handler with CORS support.""" 22 | 23 | def end_headers(self): 24 | # Add CORS headers 25 | self.send_header('Access-Control-Allow-Origin', '*') 26 | self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 27 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 28 | super().end_headers() 29 | 30 | def do_GET(self): 31 | """Handle GET requests.""" 32 | parsed_path = urlparse(self.path) 33 | 34 | # API endpoints 35 | if parsed_path.path == '/api/sample-images': 36 | self.handle_sample_images() 37 | return 38 | elif parsed_path.path == '/api/health': 39 | self.handle_health() 40 | return 41 | 42 | # Serve static files 43 | super().do_GET() 44 | 45 | def handle_sample_images(self): 46 | """API endpoint to get available sample images.""" 47 | try: 48 | sample_images_dir = Path("sample-images") 49 | 50 | if not sample_images_dir.exists(): 51 | sample_images_dir.mkdir(parents=True, exist_ok=True) 52 | self.create_sample_images() 53 | 54 | # Get list of image files 55 | image_files = [] 56 | for file_path in sample_images_dir.iterdir(): 57 | if file_path.is_file() and file_path.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: 58 | image_files.append({ 59 | 'filename': file_path.name, 60 | 'name': file_path.stem.replace('_', ' ').replace('-', ' ').title(), 61 | 'url': f'/sample-images/{file_path.name}' 62 | }) 63 | 64 | # Sort by name 65 | image_files.sort(key=lambda x: x['name']) 66 | 67 | self.send_response(200) 68 | self.send_header('Content-Type', 'application/json') 69 | self.end_headers() 70 | self.wfile.write(json.dumps(image_files).encode()) 71 | 72 | except Exception as e: 73 | print(f"Error in handle_sample_images: {e}") 74 | self.send_error(500, "Internal server error") 75 | 76 | def handle_health(self): 77 | """Health check endpoint.""" 78 | health_data = { 79 | 'status': 'healthy', 80 | 'service': 'Image Augmentation Playground' 81 | } 82 | 83 | self.send_response(200) 84 | self.send_header('Content-Type', 'application/json') 85 | self.end_headers() 86 | self.wfile.write(json.dumps(health_data).encode()) 87 | 88 | def create_sample_images(self): 89 | """Check for existing sample images.""" 90 | sample_images_dir = Path("sample-images") 91 | 92 | # Just check what images exist - don't create any new ones 93 | existing_images = [] 94 | for file_path in sample_images_dir.iterdir(): 95 | if file_path.is_file() and file_path.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: 96 | existing_images.append(file_path.name) 97 | 98 | if existing_images: 99 | print(f"✅ Found existing images: {', '.join(existing_images)}") 100 | else: 101 | print("ℹ️ No sample images found. Add JPG/PNG files to sample-images/ directory") 102 | 103 | def log_message(self, format, *args): 104 | """Custom log format.""" 105 | print(f"[{self.log_date_time_string()}] {format % args}") 106 | 107 | def main(): 108 | """Main server function.""" 109 | print("🖼️ Image Augmentation Playground Server") 110 | print("=" * 50) 111 | 112 | # Change to the simulation directory 113 | simulation_dir = Path(__file__).parent / "simulation" 114 | if not simulation_dir.exists(): 115 | print(f"Error: Simulation directory not found: {simulation_dir}") 116 | sys.exit(1) 117 | 118 | # Remove the nested simulation directory if it exists 119 | nested_sim_dir = simulation_dir / "simulation" 120 | if nested_sim_dir.exists(): 121 | print(f"Removing nested simulation directory: {nested_sim_dir}") 122 | import shutil 123 | shutil.rmtree(nested_sim_dir) 124 | 125 | os.chdir(simulation_dir) 126 | print(f"Serving from: {simulation_dir.absolute()}") 127 | 128 | # Create the server 129 | try: 130 | with socketserver.TCPServer((HOST, PORT), SimpleHTTPRequestHandler) as httpd: 131 | print(f"🚀 Server running at http://{HOST}:{PORT}") 132 | print(f"🌐 Open your browser and navigate to: http://localhost:{PORT}") 133 | print("📱 The simulation works on desktop and mobile devices") 134 | print("\n💡 Features:") 135 | print(" • Interactive image augmentation techniques") 136 | print(" • Real-time visual feedback") 137 | print(" • Multiple sample images to experiment with") 138 | print(" • Geometric transformations (flip, rotate, crop)") 139 | print(" • Color adjustments (brightness, contrast, saturation)") 140 | print(" • Noise and blur effects") 141 | print("\n⌨️ Press Ctrl+C to stop the server") 142 | print("=" * 50) 143 | 144 | httpd.serve_forever() 145 | 146 | except KeyboardInterrupt: 147 | print("\n\n🛑 Server stopped by user") 148 | except OSError as e: 149 | if e.errno == 48: # Address already in use 150 | print(f"❌ Error: Port {PORT} is already in use") 151 | print(f" Try a different port or stop the process using port {PORT}") 152 | else: 153 | print(f"❌ Error starting server: {e}") 154 | sys.exit(1) 155 | except Exception as e: 156 | print(f"❌ Unexpected error: {e}") 157 | sys.exit(1) 158 | 159 | if __name__ == "__main__": 160 | main() -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Image Augmentation Playground 7 | 8 | 9 | 10 |
11 |
12 |

Image Augmentation Playground

13 |

Explore how different augmentation techniques transform your images

14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 |

Original Image

23 |
24 | Original Image 25 |
26 |
27 | 28 |
29 |

Augmented Image

30 |
31 | Augmented Image 32 |
33 |
34 |
35 | 36 | 37 |
38 |

Data Augmentation Techniques

39 |
    40 |
  • Geometric Transformations: Flip, rotate, crop - make models invariant to orientation and position
  • 41 |
  • Color Adjustments: Brightness, contrast, saturation - handle different lighting and color conditions
  • 42 |
  • Noise & Blur: Add realistic imperfections that models encounter in real-world data
  • 43 |
  • Combination: Apply multiple techniques together for maximum data diversity
  • 44 |
  • Domain-Specific: Different techniques work better for different types of images (medical, satellite, etc.)
  • 45 |
46 |
47 |
48 | 49 | 50 |
51 |
52 | 53 | 56 |
57 | 58 |
59 |

Augmentation Techniques

60 | 61 | 62 |
63 |

Geometric Transformations

64 | 65 |
66 | 67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 | 75 | 76 |
77 | 78 |
79 | 80 | 81 |
82 |
83 | 84 | 85 |
86 |

Color Adjustments

87 | 88 |
89 | 90 | 91 |
92 | 93 |
94 | 95 | 96 |
97 | 98 |
99 | 100 | 101 |
102 |
103 | 104 | 105 |
106 |

Noise & Blur

107 | 108 |
109 | 110 | 111 |
112 | 113 |
114 | 115 | 116 |
117 |
118 | 119 | 120 | 121 | 122 |
123 | 124 |
125 |
126 |
127 |
128 | 129 |
130 | 131 | 132 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /simulation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Image Augmentation Playground 7 | 8 | 9 | 10 |
11 |
12 |

Image Augmentation Playground

13 |

Explore how different augmentation techniques transform your images

14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 |

Original Image

23 |
24 | Original Image 25 |
26 |
27 | 28 |
29 |

Augmented Image

30 |
31 | Augmented Image 32 |
33 |
34 |
35 | 36 | 37 |
38 |

Data Augmentation Techniques

39 |
    40 |
  • Geometric Transformations: Flip, rotate, crop - make models invariant to orientation and position
  • 41 |
  • Color Adjustments: Brightness, contrast, saturation - handle different lighting and color conditions
  • 42 |
  • Noise & Blur: Add realistic imperfections that models encounter in real-world data
  • 43 |
  • Combination: Apply multiple techniques together for maximum data diversity
  • 44 |
  • Domain-Specific: Different techniques work better for different types of images (medical, satellite, etc.)
  • 45 |
46 |
47 |
48 | 49 | 50 |
51 |
52 | 53 | 56 |
57 | 58 |
59 |

Augmentation Techniques

60 | 61 | 62 |
63 |

Geometric Transformations

64 | 65 |
66 | 67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 | 75 | 76 |
77 | 78 |
79 | 80 | 81 |
82 |
83 | 84 | 85 |
86 |

Color Adjustments

87 | 88 |
89 | 90 | 91 |
92 | 93 |
94 | 95 | 96 |
97 | 98 |
99 | 100 | 101 |
102 |
103 | 104 | 105 |
106 |

Noise & Blur

107 | 108 |
109 | 110 | 111 |
112 | 113 |
114 | 115 | 116 |
117 |
118 | 119 | 120 | 121 | 122 |
123 | 124 |
125 |
126 |
127 |
128 | 129 |
130 | 131 | 132 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /dist/script.js: -------------------------------------------------------------------------------- 1 | class ImageAugmentationPlayground { 2 | constructor() { 3 | this.originalImage = null; 4 | this.canvas = null; 5 | this.ctx = null; 6 | this.currentTransform = { 7 | flipHorizontal: false, 8 | flipVertical: false, 9 | rotation: 0, 10 | crop: 0, 11 | brightness: 100, 12 | contrast: 100, 13 | saturation: 100, 14 | noise: 0, 15 | blur: 0 16 | }; 17 | 18 | this.initializeElements(); 19 | this.setupEventListeners(); 20 | this.setupHelpModal(); 21 | this.loadSampleImages(); 22 | } 23 | 24 | initializeElements() { 25 | this.originalImageEl = document.getElementById('originalImage'); 26 | this.augmentedImageEl = document.getElementById('augmentedImage'); 27 | this.imageSelect = document.getElementById('imageSelect'); 28 | 29 | // Control elements 30 | this.flipHorizontalBtn = document.getElementById('flipHorizontal'); 31 | this.flipVerticalBtn = document.getElementById('flipVertical'); 32 | this.rotationSlider = document.getElementById('rotationSlider'); 33 | this.rotationValue = document.getElementById('rotationValue'); 34 | this.cropSlider = document.getElementById('cropSlider'); 35 | this.cropValue = document.getElementById('cropValue'); 36 | this.brightnessSlider = document.getElementById('brightnessSlider'); 37 | this.brightnessValue = document.getElementById('brightnessValue'); 38 | this.contrastSlider = document.getElementById('contrastSlider'); 39 | this.contrastValue = document.getElementById('contrastValue'); 40 | this.saturationSlider = document.getElementById('saturationSlider'); 41 | this.saturationValue = document.getElementById('saturationValue'); 42 | this.noiseSlider = document.getElementById('noiseSlider'); 43 | this.noiseValue = document.getElementById('noiseValue'); 44 | this.blurSlider = document.getElementById('blurSlider'); 45 | this.blurValue = document.getElementById('blurValue'); 46 | this.resetBtn = document.getElementById('resetBtn'); 47 | 48 | // Help modal elements 49 | this.helpBtn = document.getElementById('helpBtn'); 50 | this.helpModal = document.getElementById('helpModal'); 51 | this.closeBtn = document.querySelector('.close'); 52 | } 53 | 54 | setupEventListeners() { 55 | // Image selection 56 | this.imageSelect.addEventListener('change', (e) => { 57 | this.loadImage(e.target.value); 58 | }); 59 | 60 | // Button controls 61 | this.flipHorizontalBtn.addEventListener('click', () => { 62 | this.currentTransform.flipHorizontal = !this.currentTransform.flipHorizontal; 63 | this.updateButtonState(this.flipHorizontalBtn, this.currentTransform.flipHorizontal); 64 | this.applyAugmentation(); 65 | }); 66 | 67 | this.flipVerticalBtn.addEventListener('click', () => { 68 | this.currentTransform.flipVertical = !this.currentTransform.flipVertical; 69 | this.updateButtonState(this.flipVerticalBtn, this.currentTransform.flipVertical); 70 | this.applyAugmentation(); 71 | }); 72 | 73 | // Slider controls 74 | this.rotationSlider.addEventListener('input', (e) => { 75 | this.currentTransform.rotation = parseInt(e.target.value); 76 | this.rotationValue.textContent = `${this.currentTransform.rotation}°`; 77 | this.applyAugmentation(); 78 | }); 79 | 80 | this.cropSlider.addEventListener('input', (e) => { 81 | this.currentTransform.crop = parseInt(e.target.value); 82 | this.cropValue.textContent = `${this.currentTransform.crop}%`; 83 | this.applyAugmentation(); 84 | }); 85 | 86 | this.brightnessSlider.addEventListener('input', (e) => { 87 | this.currentTransform.brightness = parseInt(e.target.value); 88 | this.brightnessValue.textContent = `${this.currentTransform.brightness}%`; 89 | this.applyAugmentation(); 90 | }); 91 | 92 | this.contrastSlider.addEventListener('input', (e) => { 93 | this.currentTransform.contrast = parseInt(e.target.value); 94 | this.contrastValue.textContent = `${this.currentTransform.contrast}%`; 95 | this.applyAugmentation(); 96 | }); 97 | 98 | this.saturationSlider.addEventListener('input', (e) => { 99 | this.currentTransform.saturation = parseInt(e.target.value); 100 | this.saturationValue.textContent = `${this.currentTransform.saturation}%`; 101 | this.applyAugmentation(); 102 | }); 103 | 104 | this.noiseSlider.addEventListener('input', (e) => { 105 | this.currentTransform.noise = parseInt(e.target.value); 106 | this.noiseValue.textContent = `${this.currentTransform.noise}%`; 107 | this.applyAugmentation(); 108 | }); 109 | 110 | this.blurSlider.addEventListener('input', (e) => { 111 | this.currentTransform.blur = parseFloat(e.target.value); 112 | this.blurValue.textContent = `${this.currentTransform.blur}px`; 113 | this.applyAugmentation(); 114 | }); 115 | 116 | // Reset button 117 | this.resetBtn.addEventListener('click', () => { 118 | this.resetAll(); 119 | }); 120 | } 121 | 122 | setupHelpModal() { 123 | // Open help modal 124 | this.helpBtn.addEventListener('click', () => { 125 | this.helpModal.style.display = 'block'; 126 | document.body.style.overflow = 'hidden'; // Prevent background scrolling 127 | }); 128 | 129 | // Close help modal 130 | this.closeBtn.addEventListener('click', () => { 131 | this.helpModal.style.display = 'none'; 132 | document.body.style.overflow = 'auto'; // Restore scrolling 133 | }); 134 | 135 | // Close modal when clicking outside 136 | window.addEventListener('click', (event) => { 137 | if (event.target === this.helpModal) { 138 | this.helpModal.style.display = 'none'; 139 | document.body.style.overflow = 'auto'; 140 | } 141 | }); 142 | 143 | // Close modal with Escape key 144 | document.addEventListener('keydown', (event) => { 145 | if (event.key === 'Escape' && this.helpModal.style.display === 'block') { 146 | this.helpModal.style.display = 'none'; 147 | document.body.style.overflow = 'auto'; 148 | } 149 | }); 150 | } 151 | 152 | async loadSampleImages() { 153 | try { 154 | const response = await fetch('/api/sample-images'); 155 | const images = await response.json(); 156 | 157 | // Populate the dropdown 158 | this.imageSelect.innerHTML = ''; 159 | images.forEach((image, index) => { 160 | const option = document.createElement('option'); 161 | option.value = image.url; 162 | option.textContent = image.name; 163 | this.imageSelect.appendChild(option); 164 | }); 165 | 166 | // Load the first image 167 | if (images.length > 0) { 168 | this.loadImage(images[0].url); 169 | } 170 | } catch (error) { 171 | console.error('Error loading sample images:', error); 172 | // Fallback to default image 173 | this.loadImage('/simulation/sample-images/cat.jpg'); 174 | } 175 | } 176 | 177 | loadImage(src) { 178 | const img = new Image(); 179 | img.crossOrigin = 'anonymous'; 180 | img.onload = () => { 181 | this.originalImage = img; 182 | this.originalImageEl.src = src; 183 | this.setupCanvas(); 184 | this.applyAugmentation(); 185 | }; 186 | img.onerror = () => { 187 | console.error('Error loading image:', src); 188 | // Create a placeholder image 189 | this.createPlaceholderImage(); 190 | }; 191 | img.src = src; 192 | } 193 | 194 | createPlaceholderImage() { 195 | // Create a simple placeholder if images fail to load 196 | const canvas = document.createElement('canvas'); 197 | canvas.width = 400; 198 | canvas.height = 300; 199 | const ctx = canvas.getContext('2d'); 200 | 201 | // Draw a simple placeholder 202 | ctx.fillStyle = '#f0f0f0'; 203 | ctx.fillRect(0, 0, 400, 300); 204 | ctx.fillStyle = '#666'; 205 | ctx.font = '20px Arial'; 206 | ctx.textAlign = 'center'; 207 | ctx.fillText('Sample Image', 200, 150); 208 | 209 | const dataURL = canvas.toDataURL(); 210 | this.originalImageEl.src = dataURL; 211 | this.augmentedImageEl.src = dataURL; 212 | } 213 | 214 | setupCanvas() { 215 | if (this.canvas) { 216 | this.canvas.remove(); 217 | } 218 | 219 | this.canvas = document.createElement('canvas'); 220 | this.canvas.width = this.originalImage.width; 221 | this.canvas.height = this.originalImage.height; 222 | this.ctx = this.canvas.getContext('2d'); 223 | } 224 | 225 | applyAugmentation() { 226 | if (!this.originalImage || !this.ctx) return; 227 | 228 | // Clear canvas 229 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 230 | 231 | // Save context state 232 | this.ctx.save(); 233 | 234 | // Apply transformations 235 | this.applyGeometricTransformations(); 236 | this.applyColorAdjustments(); 237 | this.applyNoiseAndBlur(); 238 | 239 | // Draw the image with cropping 240 | this.drawImageWithCrop(); 241 | 242 | // Restore context state 243 | this.ctx.restore(); 244 | 245 | // Update the augmented image display 246 | this.augmentedImageEl.src = this.canvas.toDataURL(); 247 | } 248 | 249 | applyGeometricTransformations() { 250 | const { flipHorizontal, flipVertical, rotation, crop } = this.currentTransform; 251 | 252 | // Center the transformations 253 | this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2); 254 | 255 | // Apply rotation 256 | if (rotation !== 0) { 257 | this.ctx.rotate((rotation * Math.PI) / 180); 258 | } 259 | 260 | // Apply flipping 261 | if (flipHorizontal) { 262 | this.ctx.scale(-1, 1); 263 | } 264 | if (flipVertical) { 265 | this.ctx.scale(1, -1); 266 | } 267 | 268 | 269 | // Translate back to draw from top-left 270 | this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2); 271 | } 272 | 273 | drawImageWithCrop() { 274 | const { crop } = this.currentTransform; 275 | 276 | if (crop > 0) { 277 | // Calculate crop dimensions 278 | const cropAmount = crop / 100; 279 | const cropX = this.originalImage.width * cropAmount / 2; 280 | const cropY = this.originalImage.height * cropAmount / 2; 281 | const cropWidth = this.originalImage.width * (1 - cropAmount); 282 | const cropHeight = this.originalImage.height * (1 - cropAmount); 283 | 284 | // Draw only the cropped portion of the image 285 | this.ctx.drawImage( 286 | this.originalImage, 287 | cropX, cropY, cropWidth, cropHeight, // Source rectangle (what to crop from) 288 | 0, 0, this.canvas.width, this.canvas.height // Destination rectangle (where to draw) 289 | ); 290 | } else { 291 | // Draw the full image 292 | this.ctx.drawImage(this.originalImage, 0, 0); 293 | } 294 | } 295 | 296 | applyColorAdjustments() { 297 | const { brightness, contrast, saturation } = this.currentTransform; 298 | 299 | // Create a filter string 300 | let filters = []; 301 | 302 | if (brightness !== 100) { 303 | filters.push(`brightness(${brightness}%)`); 304 | } 305 | 306 | if (contrast !== 100) { 307 | filters.push(`contrast(${contrast}%)`); 308 | } 309 | 310 | if (saturation !== 100) { 311 | filters.push(`saturate(${saturation}%)`); 312 | } 313 | 314 | if (filters.length > 0) { 315 | this.ctx.filter = filters.join(' '); 316 | } 317 | } 318 | 319 | applyNoiseAndBlur() { 320 | const { noise, blur } = this.currentTransform; 321 | 322 | // Apply blur 323 | if (blur > 0) { 324 | this.ctx.filter = (this.ctx.filter || '') + ` blur(${blur}px)`; 325 | } 326 | 327 | // Note: Noise will be applied after drawing the image 328 | if (noise > 0) { 329 | this.ctx.globalCompositeOperation = 'multiply'; 330 | } 331 | } 332 | 333 | addNoise() { 334 | if (this.currentTransform.noise <= 0) return; 335 | 336 | const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); 337 | const data = imageData.data; 338 | const noiseIntensity = this.currentTransform.noise / 100; 339 | 340 | for (let i = 0; i < data.length; i += 4) { 341 | const noise = (Math.random() - 0.5) * noiseIntensity * 255; 342 | data[i] = Math.max(0, Math.min(255, data[i] + noise)); // Red 343 | data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise)); // Green 344 | data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise)); // Blue 345 | } 346 | 347 | this.ctx.putImageData(imageData, 0, 0); 348 | } 349 | 350 | updateButtonState(button, isActive) { 351 | if (isActive) { 352 | button.style.background = 'linear-gradient(135deg, #149474 0%, #1dcc92 100%)'; 353 | button.style.transform = 'scale(0.98)'; 354 | } else { 355 | button.style.background = 'linear-gradient(135deg, #1dcc92 0%, #149474 100%)'; 356 | button.style.transform = 'scale(1)'; 357 | } 358 | } 359 | 360 | resetAll() { 361 | // Reset transform values 362 | this.currentTransform = { 363 | flipHorizontal: false, 364 | flipVertical: false, 365 | rotation: 0, 366 | crop: 0, 367 | brightness: 100, 368 | contrast: 100, 369 | saturation: 100, 370 | noise: 0, 371 | blur: 0 372 | }; 373 | 374 | // Reset UI elements 375 | this.rotationSlider.value = 0; 376 | this.rotationValue.textContent = '0°'; 377 | this.cropSlider.value = 0; 378 | this.cropValue.textContent = '0%'; 379 | this.brightnessSlider.value = 100; 380 | this.brightnessValue.textContent = '100%'; 381 | this.contrastSlider.value = 100; 382 | this.contrastValue.textContent = '100%'; 383 | this.saturationSlider.value = 100; 384 | this.saturationValue.textContent = '100%'; 385 | this.noiseSlider.value = 0; 386 | this.noiseValue.textContent = '0%'; 387 | this.blurSlider.value = 0; 388 | this.blurValue.textContent = '0px'; 389 | 390 | // Reset button states 391 | this.updateButtonState(this.flipHorizontalBtn, false); 392 | this.updateButtonState(this.flipVerticalBtn, false); 393 | 394 | // Apply the reset 395 | this.applyAugmentation(); 396 | } 397 | } 398 | 399 | // Initialize the playground when the page loads 400 | document.addEventListener('DOMContentLoaded', () => { 401 | new ImageAugmentationPlayground(); 402 | }); 403 | -------------------------------------------------------------------------------- /simulation/script.js: -------------------------------------------------------------------------------- 1 | class ImageAugmentationPlayground { 2 | constructor() { 3 | this.originalImage = null; 4 | this.canvas = null; 5 | this.ctx = null; 6 | this.currentTransform = { 7 | flipHorizontal: false, 8 | flipVertical: false, 9 | rotation: 0, 10 | crop: 0, 11 | brightness: 100, 12 | contrast: 100, 13 | saturation: 100, 14 | noise: 0, 15 | blur: 0 16 | }; 17 | 18 | this.initializeElements(); 19 | this.setupEventListeners(); 20 | this.setupHelpModal(); 21 | this.loadSampleImages(); 22 | } 23 | 24 | initializeElements() { 25 | this.originalImageEl = document.getElementById('originalImage'); 26 | this.augmentedImageEl = document.getElementById('augmentedImage'); 27 | this.imageSelect = document.getElementById('imageSelect'); 28 | 29 | // Control elements 30 | this.flipHorizontalBtn = document.getElementById('flipHorizontal'); 31 | this.flipVerticalBtn = document.getElementById('flipVertical'); 32 | this.rotationSlider = document.getElementById('rotationSlider'); 33 | this.rotationValue = document.getElementById('rotationValue'); 34 | this.cropSlider = document.getElementById('cropSlider'); 35 | this.cropValue = document.getElementById('cropValue'); 36 | this.brightnessSlider = document.getElementById('brightnessSlider'); 37 | this.brightnessValue = document.getElementById('brightnessValue'); 38 | this.contrastSlider = document.getElementById('contrastSlider'); 39 | this.contrastValue = document.getElementById('contrastValue'); 40 | this.saturationSlider = document.getElementById('saturationSlider'); 41 | this.saturationValue = document.getElementById('saturationValue'); 42 | this.noiseSlider = document.getElementById('noiseSlider'); 43 | this.noiseValue = document.getElementById('noiseValue'); 44 | this.blurSlider = document.getElementById('blurSlider'); 45 | this.blurValue = document.getElementById('blurValue'); 46 | this.resetBtn = document.getElementById('resetBtn'); 47 | 48 | // Help modal elements 49 | this.helpBtn = document.getElementById('helpBtn'); 50 | this.helpModal = document.getElementById('helpModal'); 51 | this.closeBtn = document.querySelector('.close'); 52 | } 53 | 54 | setupEventListeners() { 55 | // Image selection 56 | this.imageSelect.addEventListener('change', (e) => { 57 | this.loadImage(e.target.value); 58 | }); 59 | 60 | // Button controls 61 | this.flipHorizontalBtn.addEventListener('click', () => { 62 | this.currentTransform.flipHorizontal = !this.currentTransform.flipHorizontal; 63 | this.updateButtonState(this.flipHorizontalBtn, this.currentTransform.flipHorizontal); 64 | this.applyAugmentation(); 65 | }); 66 | 67 | this.flipVerticalBtn.addEventListener('click', () => { 68 | this.currentTransform.flipVertical = !this.currentTransform.flipVertical; 69 | this.updateButtonState(this.flipVerticalBtn, this.currentTransform.flipVertical); 70 | this.applyAugmentation(); 71 | }); 72 | 73 | // Slider controls 74 | this.rotationSlider.addEventListener('input', (e) => { 75 | this.currentTransform.rotation = parseInt(e.target.value); 76 | this.rotationValue.textContent = `${this.currentTransform.rotation}°`; 77 | this.applyAugmentation(); 78 | }); 79 | 80 | this.cropSlider.addEventListener('input', (e) => { 81 | this.currentTransform.crop = parseInt(e.target.value); 82 | this.cropValue.textContent = `${this.currentTransform.crop}%`; 83 | this.applyAugmentation(); 84 | }); 85 | 86 | this.brightnessSlider.addEventListener('input', (e) => { 87 | this.currentTransform.brightness = parseInt(e.target.value); 88 | this.brightnessValue.textContent = `${this.currentTransform.brightness}%`; 89 | this.applyAugmentation(); 90 | }); 91 | 92 | this.contrastSlider.addEventListener('input', (e) => { 93 | this.currentTransform.contrast = parseInt(e.target.value); 94 | this.contrastValue.textContent = `${this.currentTransform.contrast}%`; 95 | this.applyAugmentation(); 96 | }); 97 | 98 | this.saturationSlider.addEventListener('input', (e) => { 99 | this.currentTransform.saturation = parseInt(e.target.value); 100 | this.saturationValue.textContent = `${this.currentTransform.saturation}%`; 101 | this.applyAugmentation(); 102 | }); 103 | 104 | this.noiseSlider.addEventListener('input', (e) => { 105 | this.currentTransform.noise = parseInt(e.target.value); 106 | this.noiseValue.textContent = `${this.currentTransform.noise}%`; 107 | this.applyAugmentation(); 108 | }); 109 | 110 | this.blurSlider.addEventListener('input', (e) => { 111 | this.currentTransform.blur = parseFloat(e.target.value); 112 | this.blurValue.textContent = `${this.currentTransform.blur}px`; 113 | this.applyAugmentation(); 114 | }); 115 | 116 | // Reset button 117 | this.resetBtn.addEventListener('click', () => { 118 | this.resetAll(); 119 | }); 120 | } 121 | 122 | setupHelpModal() { 123 | // Open help modal 124 | this.helpBtn.addEventListener('click', () => { 125 | this.helpModal.style.display = 'block'; 126 | document.body.style.overflow = 'hidden'; // Prevent background scrolling 127 | }); 128 | 129 | // Close help modal 130 | this.closeBtn.addEventListener('click', () => { 131 | this.helpModal.style.display = 'none'; 132 | document.body.style.overflow = 'auto'; // Restore scrolling 133 | }); 134 | 135 | // Close modal when clicking outside 136 | window.addEventListener('click', (event) => { 137 | if (event.target === this.helpModal) { 138 | this.helpModal.style.display = 'none'; 139 | document.body.style.overflow = 'auto'; 140 | } 141 | }); 142 | 143 | // Close modal with Escape key 144 | document.addEventListener('keydown', (event) => { 145 | if (event.key === 'Escape' && this.helpModal.style.display === 'block') { 146 | this.helpModal.style.display = 'none'; 147 | document.body.style.overflow = 'auto'; 148 | } 149 | }); 150 | } 151 | 152 | async loadSampleImages() { 153 | try { 154 | const response = await fetch('/api/sample-images'); 155 | const images = await response.json(); 156 | 157 | // Populate the dropdown 158 | this.imageSelect.innerHTML = ''; 159 | images.forEach((image, index) => { 160 | const option = document.createElement('option'); 161 | option.value = image.url; 162 | option.textContent = image.name; 163 | this.imageSelect.appendChild(option); 164 | }); 165 | 166 | // Load the first image 167 | if (images.length > 0) { 168 | this.loadImage(images[0].url); 169 | } 170 | } catch (error) { 171 | console.error('Error loading sample images:', error); 172 | // Fallback to default image 173 | this.loadImage('/simulation/sample-images/cat.jpg'); 174 | } 175 | } 176 | 177 | loadImage(src) { 178 | const img = new Image(); 179 | img.crossOrigin = 'anonymous'; 180 | img.onload = () => { 181 | this.originalImage = img; 182 | this.originalImageEl.src = src; 183 | this.setupCanvas(); 184 | this.applyAugmentation(); 185 | }; 186 | img.onerror = () => { 187 | console.error('Error loading image:', src); 188 | // Create a placeholder image 189 | this.createPlaceholderImage(); 190 | }; 191 | img.src = src; 192 | } 193 | 194 | createPlaceholderImage() { 195 | // Create a simple placeholder if images fail to load 196 | const canvas = document.createElement('canvas'); 197 | canvas.width = 400; 198 | canvas.height = 300; 199 | const ctx = canvas.getContext('2d'); 200 | 201 | // Draw a simple placeholder 202 | ctx.fillStyle = '#f0f0f0'; 203 | ctx.fillRect(0, 0, 400, 300); 204 | ctx.fillStyle = '#666'; 205 | ctx.font = '20px Arial'; 206 | ctx.textAlign = 'center'; 207 | ctx.fillText('Sample Image', 200, 150); 208 | 209 | const dataURL = canvas.toDataURL(); 210 | this.originalImageEl.src = dataURL; 211 | this.augmentedImageEl.src = dataURL; 212 | } 213 | 214 | setupCanvas() { 215 | if (this.canvas) { 216 | this.canvas.remove(); 217 | } 218 | 219 | this.canvas = document.createElement('canvas'); 220 | this.canvas.width = this.originalImage.width; 221 | this.canvas.height = this.originalImage.height; 222 | this.ctx = this.canvas.getContext('2d'); 223 | } 224 | 225 | applyAugmentation() { 226 | if (!this.originalImage || !this.ctx) return; 227 | 228 | // Clear canvas 229 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 230 | 231 | // Save context state 232 | this.ctx.save(); 233 | 234 | // Apply transformations 235 | this.applyGeometricTransformations(); 236 | this.applyColorAdjustments(); 237 | this.applyNoiseAndBlur(); 238 | 239 | // Draw the image with cropping 240 | this.drawImageWithCrop(); 241 | 242 | // Restore context state 243 | this.ctx.restore(); 244 | 245 | // Update the augmented image display 246 | this.augmentedImageEl.src = this.canvas.toDataURL(); 247 | } 248 | 249 | applyGeometricTransformations() { 250 | const { flipHorizontal, flipVertical, rotation, crop } = this.currentTransform; 251 | 252 | // Center the transformations 253 | this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2); 254 | 255 | // Apply rotation 256 | if (rotation !== 0) { 257 | this.ctx.rotate((rotation * Math.PI) / 180); 258 | } 259 | 260 | // Apply flipping 261 | if (flipHorizontal) { 262 | this.ctx.scale(-1, 1); 263 | } 264 | if (flipVertical) { 265 | this.ctx.scale(1, -1); 266 | } 267 | 268 | 269 | // Translate back to draw from top-left 270 | this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2); 271 | } 272 | 273 | drawImageWithCrop() { 274 | const { crop } = this.currentTransform; 275 | 276 | if (crop > 0) { 277 | // Calculate crop dimensions 278 | const cropAmount = crop / 100; 279 | const cropX = this.originalImage.width * cropAmount / 2; 280 | const cropY = this.originalImage.height * cropAmount / 2; 281 | const cropWidth = this.originalImage.width * (1 - cropAmount); 282 | const cropHeight = this.originalImage.height * (1 - cropAmount); 283 | 284 | // Draw only the cropped portion of the image 285 | this.ctx.drawImage( 286 | this.originalImage, 287 | cropX, cropY, cropWidth, cropHeight, // Source rectangle (what to crop from) 288 | 0, 0, this.canvas.width, this.canvas.height // Destination rectangle (where to draw) 289 | ); 290 | } else { 291 | // Draw the full image 292 | this.ctx.drawImage(this.originalImage, 0, 0); 293 | } 294 | } 295 | 296 | applyColorAdjustments() { 297 | const { brightness, contrast, saturation } = this.currentTransform; 298 | 299 | // Create a filter string 300 | let filters = []; 301 | 302 | if (brightness !== 100) { 303 | filters.push(`brightness(${brightness}%)`); 304 | } 305 | 306 | if (contrast !== 100) { 307 | filters.push(`contrast(${contrast}%)`); 308 | } 309 | 310 | if (saturation !== 100) { 311 | filters.push(`saturate(${saturation}%)`); 312 | } 313 | 314 | if (filters.length > 0) { 315 | this.ctx.filter = filters.join(' '); 316 | } 317 | } 318 | 319 | applyNoiseAndBlur() { 320 | const { noise, blur } = this.currentTransform; 321 | 322 | // Apply blur 323 | if (blur > 0) { 324 | this.ctx.filter = (this.ctx.filter || '') + ` blur(${blur}px)`; 325 | } 326 | 327 | // Note: Noise will be applied after drawing the image 328 | if (noise > 0) { 329 | this.ctx.globalCompositeOperation = 'multiply'; 330 | } 331 | } 332 | 333 | addNoise() { 334 | if (this.currentTransform.noise <= 0) return; 335 | 336 | const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); 337 | const data = imageData.data; 338 | const noiseIntensity = this.currentTransform.noise / 100; 339 | 340 | for (let i = 0; i < data.length; i += 4) { 341 | const noise = (Math.random() - 0.5) * noiseIntensity * 255; 342 | data[i] = Math.max(0, Math.min(255, data[i] + noise)); // Red 343 | data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise)); // Green 344 | data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise)); // Blue 345 | } 346 | 347 | this.ctx.putImageData(imageData, 0, 0); 348 | } 349 | 350 | updateButtonState(button, isActive) { 351 | if (isActive) { 352 | button.style.background = 'linear-gradient(135deg, #149474 0%, #1dcc92 100%)'; 353 | button.style.transform = 'scale(0.98)'; 354 | } else { 355 | button.style.background = 'linear-gradient(135deg, #1dcc92 0%, #149474 100%)'; 356 | button.style.transform = 'scale(1)'; 357 | } 358 | } 359 | 360 | resetAll() { 361 | // Reset transform values 362 | this.currentTransform = { 363 | flipHorizontal: false, 364 | flipVertical: false, 365 | rotation: 0, 366 | crop: 0, 367 | brightness: 100, 368 | contrast: 100, 369 | saturation: 100, 370 | noise: 0, 371 | blur: 0 372 | }; 373 | 374 | // Reset UI elements 375 | this.rotationSlider.value = 0; 376 | this.rotationValue.textContent = '0°'; 377 | this.cropSlider.value = 0; 378 | this.cropValue.textContent = '0%'; 379 | this.brightnessSlider.value = 100; 380 | this.brightnessValue.textContent = '100%'; 381 | this.contrastSlider.value = 100; 382 | this.contrastValue.textContent = '100%'; 383 | this.saturationSlider.value = 100; 384 | this.saturationValue.textContent = '100%'; 385 | this.noiseSlider.value = 0; 386 | this.noiseValue.textContent = '0%'; 387 | this.blurSlider.value = 0; 388 | this.blurValue.textContent = '0px'; 389 | 390 | // Reset button states 391 | this.updateButtonState(this.flipHorizontalBtn, false); 392 | this.updateButtonState(this.flipVerticalBtn, false); 393 | 394 | // Apply the reset 395 | this.applyAugmentation(); 396 | } 397 | } 398 | 399 | // Initialize the playground when the page loads 400 | document.addEventListener('DOMContentLoaded', () => { 401 | new ImageAugmentationPlayground(); 402 | }); 403 | -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | /* CodeSignal Design System Variables */ 2 | :root { 3 | /* Background colors */ 4 | --bg: #ffffff; 5 | --bg-elevated: #f8fafc; 6 | --bg-subtle: #f1f5f9; 7 | 8 | /* Surface colors */ 9 | --surface: #ffffff; 10 | --surface-elevated: #f8fafc; 11 | --surface-subtle: #f1f5f9; 12 | 13 | /* Text colors */ 14 | --text-primary: rgb(24, 33, 57); 15 | --text-secondary: rgb(73, 85, 115); 16 | --text-tertiary: rgba(73, 85, 115, 0.8); 17 | --text-muted: rgba(73, 85, 115, 0.6); 18 | 19 | /* Accent colors */ 20 | --accent-primary: #0ea5e9; 21 | --accent-secondary: #06b6d4; 22 | --accent-hover: #0284c7; 23 | --accent-active: #0369a1; 24 | 25 | /* Semantic colors */ 26 | --success: #059669; 27 | --success-bg: #ecfdf5; 28 | --success-border: #a7f3d0; 29 | 30 | --warning: #d97706; 31 | --warning-bg: #fffbeb; 32 | --warning-border: #fed7aa; 33 | 34 | --error: #dc2626; 35 | --error-bg: #fef2f2; 36 | --error-border: #fecaca; 37 | 38 | --info: #0ea5e9; 39 | --info-bg: #f0f9ff; 40 | --info-border: #bae6fd; 41 | 42 | /* Border colors */ 43 | --border-primary: #e2e8f0; 44 | --border-secondary: #cbd5e1; 45 | --border-subtle: #f1f5f9; 46 | --border-focus: #0ea5e9; 47 | 48 | /* Shadow colors */ 49 | --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 50 | --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 51 | --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 52 | --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 53 | 54 | /* Component specific */ 55 | --card-bg: #ffffff; 56 | --card-border: #e2e8f0; 57 | --card-shadow: var(--shadow-md); 58 | 59 | --button-primary-bg: #0ea5e9; 60 | --button-primary-text: #ffffff; 61 | --button-primary-hover: #0284c7; 62 | --button-primary-active: #0369a1; 63 | 64 | --input-bg: #ffffff; 65 | --input-border: #e2e8f0; 66 | --input-focus: #0ea5e9; 67 | --input-placeholder: #94a3b8; 68 | } 69 | 70 | /* Dark mode when preferred by system */ 71 | @media (prefers-color-scheme: dark) { 72 | :root { 73 | /* Background colors */ 74 | --bg: #1e2640; 75 | --bg-elevated: #26314c; 76 | --bg-subtle: #2a3550; 77 | 78 | /* Surface colors */ 79 | --surface: #26314c; 80 | --surface-elevated: #2a3550; 81 | --surface-subtle: #2e3954; 82 | 83 | /* Text colors */ 84 | --text-primary: #ffffff; 85 | --text-secondary: rgb(193, 199, 215); 86 | --text-tertiary: rgba(193, 199, 215, 0.8); 87 | --text-muted: rgba(193, 199, 215, 0.6); 88 | 89 | /* Accent colors */ 90 | --accent-primary: #38bdf8; 91 | --accent-secondary: #22d3ee; 92 | --accent-hover: #0ea5e9; 93 | --accent-active: #0284c7; 94 | 95 | /* Semantic colors */ 96 | --success: #10b981; 97 | --success-bg: rgba(16, 185, 129, 0.1); 98 | --success-border: rgba(16, 185, 129, 0.3); 99 | 100 | --warning: #f59e0b; 101 | --warning-bg: rgba(245, 158, 11, 0.1); 102 | --warning-border: rgba(245, 158, 11, 0.3); 103 | 104 | --error: #ef4444; 105 | --error-bg: rgba(239, 68, 68, 0.1); 106 | --error-border: rgba(239, 68, 68, 0.3); 107 | 108 | --info: #38bdf8; 109 | --info-bg: rgba(56, 189, 248, 0.1); 110 | --info-border: rgba(56, 189, 248, 0.3); 111 | 112 | /* Border colors */ 113 | --border-primary: rgba(193, 199, 215, 0.2); 114 | --border-secondary: rgba(193, 199, 215, 0.15); 115 | --border-subtle: rgba(193, 199, 215, 0.1); 116 | --border-focus: #38bdf8; 117 | 118 | /* Shadow colors */ 119 | --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.4); 120 | --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); 121 | --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); 122 | --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); 123 | 124 | /* Component specific */ 125 | --card-bg: #26314c; 126 | --card-border: rgba(193, 199, 215, 0.2); 127 | --card-shadow: var(--shadow-lg); 128 | 129 | --button-primary-bg: #38bdf8; 130 | --button-primary-text: #1e2640; 131 | --button-primary-hover: #0ea5e9; 132 | --button-primary-active: #0284c7; 133 | 134 | --input-bg: #26314c; 135 | --input-border: rgba(193, 199, 215, 0.2); 136 | --input-focus: #38bdf8; 137 | --input-placeholder: rgba(193, 199, 215, 0.5); 138 | } 139 | } 140 | 141 | /* Reset and base styles */ 142 | * { 143 | margin: 0; 144 | padding: 0; 145 | box-sizing: border-box; 146 | } 147 | 148 | body { 149 | font-family: "Work Sans", "Work Sans Fallback", ui-sans-serif, system-ui, sans-serif; 150 | background: var(--bg); 151 | min-height: 100vh; 152 | color: var(--text-primary); 153 | line-height: 1.5; 154 | -webkit-font-smoothing: antialiased; 155 | } 156 | 157 | .container { 158 | max-width: 1400px; 159 | margin: 0 auto; 160 | padding: 20px; 161 | } 162 | 163 | /* Header */ 164 | header { 165 | text-align: center; 166 | margin-bottom: 30px; 167 | padding: 1.5rem 1.25rem; 168 | border-bottom: 1px solid var(--border-primary); 169 | background: var(--surface-elevated); 170 | backdrop-filter: blur(8px); 171 | border-radius: 1rem; 172 | margin: 0 auto 30px auto; 173 | max-width: 1400px; 174 | } 175 | 176 | header h1 { 177 | font-size: 2.5rem; 178 | margin-bottom: 10px; 179 | color: var(--text-primary); 180 | font-weight: 600; 181 | letter-spacing: -0.025em; 182 | } 183 | 184 | header p { 185 | font-size: 1.2rem; 186 | color: var(--text-secondary); 187 | opacity: 0.9; 188 | } 189 | 190 | .help-btn { 191 | margin-top: 15px; 192 | padding: 10px 20px; 193 | background: var(--button-primary-bg); 194 | color: var(--button-primary-text); 195 | border: none; 196 | border-radius: 0.5rem; 197 | font-size: 1rem; 198 | font-weight: 600; 199 | cursor: pointer; 200 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 201 | box-shadow: var(--shadow-sm); 202 | } 203 | 204 | .help-btn:hover { 205 | background: var(--button-primary-hover); 206 | transform: translateY(-1px); 207 | box-shadow: var(--shadow-md); 208 | } 209 | 210 | .help-btn:active { 211 | background: var(--button-primary-active); 212 | transform: translateY(0); 213 | } 214 | 215 | /* Main content layout */ 216 | .main-content { 217 | display: grid; 218 | grid-template-columns: 2fr 1fr; 219 | gap: 30px; 220 | margin-bottom: 30px; 221 | } 222 | 223 | /* Image section */ 224 | .image-section { 225 | background: var(--card-bg); 226 | border-radius: 1rem; 227 | padding: 20px; 228 | box-shadow: var(--card-shadow); 229 | border: 1px solid var(--card-border); 230 | display: flex; 231 | flex-direction: column; 232 | gap: 20px; 233 | } 234 | 235 | .image-container { 236 | display: grid; 237 | grid-template-columns: 1fr 1fr; 238 | gap: 20px; 239 | } 240 | 241 | .image-panel h3 { 242 | text-align: center; 243 | margin-bottom: 15px; 244 | color: var(--text-secondary); 245 | font-size: 1.3rem; 246 | font-weight: 600; 247 | } 248 | 249 | .image-wrapper { 250 | border: 2px solid var(--border-primary); 251 | border-radius: 0.75rem; 252 | overflow: hidden; 253 | background: var(--surface-subtle); 254 | min-height: 300px; 255 | display: flex; 256 | align-items: center; 257 | justify-content: center; 258 | } 259 | 260 | .image-wrapper img { 261 | max-width: 100%; 262 | max-height: 100%; 263 | object-fit: contain; 264 | transition: all 0.3s ease; 265 | } 266 | 267 | /* Controls section */ 268 | .controls-section { 269 | background: var(--card-bg); 270 | border-radius: 1rem; 271 | padding: 25px; 272 | box-shadow: var(--card-shadow); 273 | border: 1px solid var(--card-border); 274 | height: fit-content; 275 | } 276 | 277 | .image-selector { 278 | margin-bottom: 25px; 279 | padding-bottom: 20px; 280 | border-bottom: 1px solid var(--border-primary); 281 | } 282 | 283 | .image-selector label { 284 | display: block; 285 | margin-bottom: 8px; 286 | font-weight: 600; 287 | color: var(--text-secondary); 288 | } 289 | 290 | .image-selector select { 291 | width: 100%; 292 | padding: 10px; 293 | border: 1px solid var(--input-border); 294 | border-radius: 0.5rem; 295 | font-size: 1rem; 296 | background: var(--input-bg); 297 | color: var(--text-primary); 298 | cursor: pointer; 299 | transition: border-color 0.2s ease; 300 | } 301 | 302 | .image-selector select:focus { 303 | outline: none; 304 | border-color: var(--input-focus); 305 | box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1); 306 | } 307 | 308 | .augmentation-controls h3 { 309 | margin-bottom: 20px; 310 | color: var(--text-primary); 311 | font-size: 1.4rem; 312 | text-align: center; 313 | font-weight: 600; 314 | } 315 | 316 | .control-group { 317 | margin-bottom: 25px; 318 | padding: 15px; 319 | background: var(--surface-subtle); 320 | border-radius: 0.75rem; 321 | border-left: 4px solid var(--accent-primary); 322 | } 323 | 324 | .control-group h4 { 325 | margin-bottom: 15px; 326 | color: var(--text-secondary); 327 | font-size: 1.1rem; 328 | font-weight: 600; 329 | } 330 | 331 | .control-item { 332 | margin-bottom: 15px; 333 | } 334 | 335 | .control-item:last-child { 336 | margin-bottom: 0; 337 | } 338 | 339 | .control-item label { 340 | display: block; 341 | margin-bottom: 8px; 342 | font-weight: 500; 343 | color: var(--text-tertiary); 344 | font-size: 0.95rem; 345 | } 346 | 347 | .control-item input[type="range"] { 348 | width: 100%; 349 | height: 6px; 350 | border-radius: 3px; 351 | background: var(--border-secondary); 352 | outline: none; 353 | -webkit-appearance: none; 354 | cursor: pointer; 355 | } 356 | 357 | .control-item input[type="range"]::-webkit-slider-thumb { 358 | -webkit-appearance: none; 359 | appearance: none; 360 | width: 20px; 361 | height: 20px; 362 | border-radius: 50%; 363 | background: var(--accent-primary); 364 | cursor: pointer; 365 | box-shadow: var(--shadow-sm); 366 | } 367 | 368 | .control-item input[type="range"]::-moz-range-thumb { 369 | width: 20px; 370 | height: 20px; 371 | border-radius: 50%; 372 | background: var(--accent-primary); 373 | cursor: pointer; 374 | border: none; 375 | box-shadow: var(--shadow-sm); 376 | } 377 | 378 | /* Buttons */ 379 | .aug-btn { 380 | width: 100%; 381 | padding: 12px 16px; 382 | background: var(--button-primary-bg); 383 | color: var(--button-primary-text); 384 | border: none; 385 | border-radius: 0.5rem; 386 | font-size: 1rem; 387 | font-weight: 600; 388 | cursor: pointer; 389 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 390 | box-shadow: var(--shadow-sm); 391 | } 392 | 393 | .aug-btn:hover { 394 | background: var(--button-primary-hover); 395 | transform: translateY(-1px); 396 | box-shadow: var(--shadow-md); 397 | } 398 | 399 | .aug-btn:active { 400 | background: var(--button-primary-active); 401 | transform: translateY(0); 402 | } 403 | 404 | .aug-btn.active { 405 | background: var(--success); 406 | box-shadow: var(--shadow-md); 407 | } 408 | 409 | .reset-btn { 410 | width: 100%; 411 | padding: 12px 16px; 412 | background: var(--text-muted); 413 | color: white; 414 | border: none; 415 | border-radius: 0.5rem; 416 | font-size: 1rem; 417 | font-weight: 600; 418 | cursor: pointer; 419 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 420 | box-shadow: var(--shadow-sm); 421 | } 422 | 423 | .reset-btn:hover { 424 | background: var(--text-tertiary); 425 | transform: translateY(-1px); 426 | box-shadow: var(--shadow-md); 427 | } 428 | 429 | /* Info panel */ 430 | .info-panel { 431 | background: var(--surface-subtle); 432 | border-radius: 0.75rem; 433 | padding: 20px; 434 | border: 1px solid var(--border-primary); 435 | } 436 | 437 | .info-panel h3 { 438 | margin-bottom: 15px; 439 | color: var(--text-primary); 440 | font-size: 1.3rem; 441 | font-weight: 600; 442 | } 443 | 444 | .info-panel ul { 445 | list-style: none; 446 | padding-left: 0; 447 | } 448 | 449 | .info-panel li { 450 | margin-bottom: 12px; 451 | padding-left: 20px; 452 | position: relative; 453 | line-height: 1.6; 454 | color: var(--text-tertiary); 455 | } 456 | 457 | .info-panel li:before { 458 | content: "•"; 459 | position: absolute; 460 | left: 0; 461 | top: 0; 462 | color: var(--accent-primary); 463 | font-weight: bold; 464 | } 465 | 466 | .info-panel strong { 467 | color: var(--text-primary); 468 | } 469 | 470 | /* Modal Styles */ 471 | .modal { 472 | display: none; 473 | position: fixed; 474 | z-index: 1000; 475 | left: 0; 476 | top: 0; 477 | width: 100%; 478 | height: 100%; 479 | background-color: rgba(0, 0, 0, 0.5); 480 | backdrop-filter: blur(5px); 481 | } 482 | 483 | .modal-content { 484 | background-color: var(--card-bg); 485 | margin: 2% auto; 486 | padding: 0; 487 | border-radius: 1rem; 488 | width: 90%; 489 | max-width: 800px; 490 | max-height: 90vh; 491 | overflow-y: auto; 492 | box-shadow: var(--shadow-xl); 493 | border: 1px solid var(--card-border); 494 | animation: modalSlideIn 0.3s ease-out; 495 | } 496 | 497 | @keyframes modalSlideIn { 498 | from { 499 | opacity: 0; 500 | transform: translateY(-50px); 501 | } 502 | to { 503 | opacity: 1; 504 | transform: translateY(0); 505 | } 506 | } 507 | 508 | .modal-header { 509 | background: var(--button-primary-bg); 510 | color: var(--button-primary-text); 511 | padding: 20px 30px; 512 | border-radius: 1rem 1rem 0 0; 513 | display: flex; 514 | justify-content: space-between; 515 | align-items: center; 516 | } 517 | 518 | .modal-header h2 { 519 | margin: 0; 520 | font-size: 1.5rem; 521 | font-weight: 600; 522 | } 523 | 524 | .close { 525 | color: var(--button-primary-text); 526 | font-size: 28px; 527 | font-weight: bold; 528 | cursor: pointer; 529 | transition: opacity 0.3s ease; 530 | } 531 | 532 | .close:hover { 533 | opacity: 0.7; 534 | } 535 | 536 | .modal-body { 537 | padding: 30px; 538 | } 539 | 540 | .help-section { 541 | margin-bottom: 25px; 542 | padding-bottom: 20px; 543 | border-bottom: 1px solid var(--border-primary); 544 | } 545 | 546 | .help-section:last-child { 547 | border-bottom: none; 548 | margin-bottom: 0; 549 | } 550 | 551 | .help-section h3 { 552 | color: var(--accent-primary); 553 | margin-bottom: 15px; 554 | font-size: 1.2rem; 555 | font-weight: 600; 556 | } 557 | 558 | .help-section p { 559 | line-height: 1.6; 560 | color: var(--text-tertiary); 561 | margin-bottom: 15px; 562 | } 563 | 564 | .help-section ul, .help-section ol { 565 | margin-left: 20px; 566 | line-height: 1.6; 567 | } 568 | 569 | .help-section li { 570 | margin-bottom: 8px; 571 | color: var(--text-tertiary); 572 | } 573 | 574 | .help-section strong { 575 | color: var(--text-primary); 576 | font-weight: 600; 577 | } 578 | 579 | /* Responsive modal */ 580 | @media (max-width: 768px) { 581 | .modal-content { 582 | width: 95%; 583 | margin: 5% auto; 584 | } 585 | 586 | .modal-header { 587 | padding: 15px 20px; 588 | } 589 | 590 | .modal-header h2 { 591 | font-size: 1.3rem; 592 | } 593 | 594 | .modal-body { 595 | padding: 20px; 596 | } 597 | } 598 | 599 | /* Responsive design */ 600 | @media (max-width: 1024px) { 601 | .main-content { 602 | grid-template-columns: 1fr; 603 | gap: 20px; 604 | } 605 | 606 | .image-container { 607 | grid-template-columns: 1fr; 608 | } 609 | 610 | header h1 { 611 | font-size: 2rem; 612 | } 613 | } 614 | 615 | @media (max-width: 768px) { 616 | .container { 617 | padding: 15px; 618 | } 619 | 620 | header { 621 | margin: 0 15px 30px 15px; 622 | padding: 1rem; 623 | } 624 | 625 | header h1 { 626 | font-size: 1.8rem; 627 | } 628 | 629 | header p { 630 | font-size: 1rem; 631 | } 632 | 633 | .controls-section { 634 | padding: 20px; 635 | } 636 | 637 | .control-group { 638 | padding: 12px; 639 | } 640 | } 641 | 642 | /* Focus-visible styles */ 643 | :focus-visible { 644 | outline: 2px solid var(--border-focus); 645 | outline-offset: 2px; 646 | } 647 | 648 | /* Smooth transitions */ 649 | *, 650 | *::before, 651 | *::after { 652 | transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), 653 | color 0.2s cubic-bezier(0.4, 0, 0.2, 1), 654 | border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), 655 | box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1), 656 | transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); 657 | } -------------------------------------------------------------------------------- /simulation/style.css: -------------------------------------------------------------------------------- 1 | /* CodeSignal Design System Variables */ 2 | :root { 3 | /* Background colors */ 4 | --bg: #ffffff; 5 | --bg-elevated: #f8fafc; 6 | --bg-subtle: #f1f5f9; 7 | 8 | /* Surface colors */ 9 | --surface: #ffffff; 10 | --surface-elevated: #f8fafc; 11 | --surface-subtle: #f1f5f9; 12 | 13 | /* Text colors */ 14 | --text-primary: rgb(24, 33, 57); 15 | --text-secondary: rgb(73, 85, 115); 16 | --text-tertiary: rgba(73, 85, 115, 0.8); 17 | --text-muted: rgba(73, 85, 115, 0.6); 18 | 19 | /* Accent colors */ 20 | --accent-primary: #0ea5e9; 21 | --accent-secondary: #06b6d4; 22 | --accent-hover: #0284c7; 23 | --accent-active: #0369a1; 24 | 25 | /* Semantic colors */ 26 | --success: #059669; 27 | --success-bg: #ecfdf5; 28 | --success-border: #a7f3d0; 29 | 30 | --warning: #d97706; 31 | --warning-bg: #fffbeb; 32 | --warning-border: #fed7aa; 33 | 34 | --error: #dc2626; 35 | --error-bg: #fef2f2; 36 | --error-border: #fecaca; 37 | 38 | --info: #0ea5e9; 39 | --info-bg: #f0f9ff; 40 | --info-border: #bae6fd; 41 | 42 | /* Border colors */ 43 | --border-primary: #e2e8f0; 44 | --border-secondary: #cbd5e1; 45 | --border-subtle: #f1f5f9; 46 | --border-focus: #0ea5e9; 47 | 48 | /* Shadow colors */ 49 | --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 50 | --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 51 | --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 52 | --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 53 | 54 | /* Component specific */ 55 | --card-bg: #ffffff; 56 | --card-border: #e2e8f0; 57 | --card-shadow: var(--shadow-md); 58 | 59 | --button-primary-bg: #0ea5e9; 60 | --button-primary-text: #ffffff; 61 | --button-primary-hover: #0284c7; 62 | --button-primary-active: #0369a1; 63 | 64 | --input-bg: #ffffff; 65 | --input-border: #e2e8f0; 66 | --input-focus: #0ea5e9; 67 | --input-placeholder: #94a3b8; 68 | } 69 | 70 | /* Dark mode when preferred by system */ 71 | @media (prefers-color-scheme: dark) { 72 | :root { 73 | /* Background colors */ 74 | --bg: #1e2640; 75 | --bg-elevated: #26314c; 76 | --bg-subtle: #2a3550; 77 | 78 | /* Surface colors */ 79 | --surface: #26314c; 80 | --surface-elevated: #2a3550; 81 | --surface-subtle: #2e3954; 82 | 83 | /* Text colors */ 84 | --text-primary: #ffffff; 85 | --text-secondary: rgb(193, 199, 215); 86 | --text-tertiary: rgba(193, 199, 215, 0.8); 87 | --text-muted: rgba(193, 199, 215, 0.6); 88 | 89 | /* Accent colors */ 90 | --accent-primary: #38bdf8; 91 | --accent-secondary: #22d3ee; 92 | --accent-hover: #0ea5e9; 93 | --accent-active: #0284c7; 94 | 95 | /* Semantic colors */ 96 | --success: #10b981; 97 | --success-bg: rgba(16, 185, 129, 0.1); 98 | --success-border: rgba(16, 185, 129, 0.3); 99 | 100 | --warning: #f59e0b; 101 | --warning-bg: rgba(245, 158, 11, 0.1); 102 | --warning-border: rgba(245, 158, 11, 0.3); 103 | 104 | --error: #ef4444; 105 | --error-bg: rgba(239, 68, 68, 0.1); 106 | --error-border: rgba(239, 68, 68, 0.3); 107 | 108 | --info: #38bdf8; 109 | --info-bg: rgba(56, 189, 248, 0.1); 110 | --info-border: rgba(56, 189, 248, 0.3); 111 | 112 | /* Border colors */ 113 | --border-primary: rgba(193, 199, 215, 0.2); 114 | --border-secondary: rgba(193, 199, 215, 0.15); 115 | --border-subtle: rgba(193, 199, 215, 0.1); 116 | --border-focus: #38bdf8; 117 | 118 | /* Shadow colors */ 119 | --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.4); 120 | --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); 121 | --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); 122 | --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); 123 | 124 | /* Component specific */ 125 | --card-bg: #26314c; 126 | --card-border: rgba(193, 199, 215, 0.2); 127 | --card-shadow: var(--shadow-lg); 128 | 129 | --button-primary-bg: #38bdf8; 130 | --button-primary-text: #1e2640; 131 | --button-primary-hover: #0ea5e9; 132 | --button-primary-active: #0284c7; 133 | 134 | --input-bg: #26314c; 135 | --input-border: rgba(193, 199, 215, 0.2); 136 | --input-focus: #38bdf8; 137 | --input-placeholder: rgba(193, 199, 215, 0.5); 138 | } 139 | } 140 | 141 | /* Reset and base styles */ 142 | * { 143 | margin: 0; 144 | padding: 0; 145 | box-sizing: border-box; 146 | } 147 | 148 | body { 149 | font-family: "Work Sans", "Work Sans Fallback", ui-sans-serif, system-ui, sans-serif; 150 | background: var(--bg); 151 | min-height: 100vh; 152 | color: var(--text-primary); 153 | line-height: 1.5; 154 | -webkit-font-smoothing: antialiased; 155 | } 156 | 157 | .container { 158 | max-width: 1400px; 159 | margin: 0 auto; 160 | padding: 20px; 161 | } 162 | 163 | /* Header */ 164 | header { 165 | text-align: center; 166 | margin-bottom: 30px; 167 | padding: 1.5rem 1.25rem; 168 | border-bottom: 1px solid var(--border-primary); 169 | background: var(--surface-elevated); 170 | backdrop-filter: blur(8px); 171 | border-radius: 1rem; 172 | margin: 0 auto 30px auto; 173 | max-width: 1400px; 174 | } 175 | 176 | header h1 { 177 | font-size: 2.5rem; 178 | margin-bottom: 10px; 179 | color: var(--text-primary); 180 | font-weight: 600; 181 | letter-spacing: -0.025em; 182 | } 183 | 184 | header p { 185 | font-size: 1.2rem; 186 | color: var(--text-secondary); 187 | opacity: 0.9; 188 | } 189 | 190 | .help-btn { 191 | margin-top: 15px; 192 | padding: 10px 20px; 193 | background: var(--button-primary-bg); 194 | color: var(--button-primary-text); 195 | border: none; 196 | border-radius: 0.5rem; 197 | font-size: 1rem; 198 | font-weight: 600; 199 | cursor: pointer; 200 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 201 | box-shadow: var(--shadow-sm); 202 | } 203 | 204 | .help-btn:hover { 205 | background: var(--button-primary-hover); 206 | transform: translateY(-1px); 207 | box-shadow: var(--shadow-md); 208 | } 209 | 210 | .help-btn:active { 211 | background: var(--button-primary-active); 212 | transform: translateY(0); 213 | } 214 | 215 | /* Main content layout */ 216 | .main-content { 217 | display: grid; 218 | grid-template-columns: 2fr 1fr; 219 | gap: 30px; 220 | margin-bottom: 30px; 221 | } 222 | 223 | /* Image section */ 224 | .image-section { 225 | background: var(--card-bg); 226 | border-radius: 1rem; 227 | padding: 20px; 228 | box-shadow: var(--card-shadow); 229 | border: 1px solid var(--card-border); 230 | display: flex; 231 | flex-direction: column; 232 | gap: 20px; 233 | } 234 | 235 | .image-container { 236 | display: grid; 237 | grid-template-columns: 1fr 1fr; 238 | gap: 20px; 239 | } 240 | 241 | .image-panel h3 { 242 | text-align: center; 243 | margin-bottom: 15px; 244 | color: var(--text-secondary); 245 | font-size: 1.3rem; 246 | font-weight: 600; 247 | } 248 | 249 | .image-wrapper { 250 | border: 2px solid var(--border-primary); 251 | border-radius: 0.75rem; 252 | overflow: hidden; 253 | background: var(--surface-subtle); 254 | min-height: 300px; 255 | display: flex; 256 | align-items: center; 257 | justify-content: center; 258 | } 259 | 260 | .image-wrapper img { 261 | max-width: 100%; 262 | max-height: 100%; 263 | object-fit: contain; 264 | transition: all 0.3s ease; 265 | } 266 | 267 | /* Controls section */ 268 | .controls-section { 269 | background: var(--card-bg); 270 | border-radius: 1rem; 271 | padding: 25px; 272 | box-shadow: var(--card-shadow); 273 | border: 1px solid var(--card-border); 274 | height: fit-content; 275 | } 276 | 277 | .image-selector { 278 | margin-bottom: 25px; 279 | padding-bottom: 20px; 280 | border-bottom: 1px solid var(--border-primary); 281 | } 282 | 283 | .image-selector label { 284 | display: block; 285 | margin-bottom: 8px; 286 | font-weight: 600; 287 | color: var(--text-secondary); 288 | } 289 | 290 | .image-selector select { 291 | width: 100%; 292 | padding: 10px; 293 | border: 1px solid var(--input-border); 294 | border-radius: 0.5rem; 295 | font-size: 1rem; 296 | background: var(--input-bg); 297 | color: var(--text-primary); 298 | cursor: pointer; 299 | transition: border-color 0.2s ease; 300 | } 301 | 302 | .image-selector select:focus { 303 | outline: none; 304 | border-color: var(--input-focus); 305 | box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1); 306 | } 307 | 308 | .augmentation-controls h3 { 309 | margin-bottom: 20px; 310 | color: var(--text-primary); 311 | font-size: 1.4rem; 312 | text-align: center; 313 | font-weight: 600; 314 | } 315 | 316 | .control-group { 317 | margin-bottom: 25px; 318 | padding: 15px; 319 | background: var(--surface-subtle); 320 | border-radius: 0.75rem; 321 | border-left: 4px solid var(--accent-primary); 322 | } 323 | 324 | .control-group h4 { 325 | margin-bottom: 15px; 326 | color: var(--text-secondary); 327 | font-size: 1.1rem; 328 | font-weight: 600; 329 | } 330 | 331 | .control-item { 332 | margin-bottom: 15px; 333 | } 334 | 335 | .control-item:last-child { 336 | margin-bottom: 0; 337 | } 338 | 339 | .control-item label { 340 | display: block; 341 | margin-bottom: 8px; 342 | font-weight: 500; 343 | color: var(--text-tertiary); 344 | font-size: 0.95rem; 345 | } 346 | 347 | .control-item input[type="range"] { 348 | width: 100%; 349 | height: 6px; 350 | border-radius: 3px; 351 | background: var(--border-secondary); 352 | outline: none; 353 | -webkit-appearance: none; 354 | cursor: pointer; 355 | } 356 | 357 | .control-item input[type="range"]::-webkit-slider-thumb { 358 | -webkit-appearance: none; 359 | appearance: none; 360 | width: 20px; 361 | height: 20px; 362 | border-radius: 50%; 363 | background: var(--accent-primary); 364 | cursor: pointer; 365 | box-shadow: var(--shadow-sm); 366 | } 367 | 368 | .control-item input[type="range"]::-moz-range-thumb { 369 | width: 20px; 370 | height: 20px; 371 | border-radius: 50%; 372 | background: var(--accent-primary); 373 | cursor: pointer; 374 | border: none; 375 | box-shadow: var(--shadow-sm); 376 | } 377 | 378 | /* Buttons */ 379 | .aug-btn { 380 | width: 100%; 381 | padding: 12px 16px; 382 | background: var(--button-primary-bg); 383 | color: var(--button-primary-text); 384 | border: none; 385 | border-radius: 0.5rem; 386 | font-size: 1rem; 387 | font-weight: 600; 388 | cursor: pointer; 389 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 390 | box-shadow: var(--shadow-sm); 391 | } 392 | 393 | .aug-btn:hover { 394 | background: var(--button-primary-hover); 395 | transform: translateY(-1px); 396 | box-shadow: var(--shadow-md); 397 | } 398 | 399 | .aug-btn:active { 400 | background: var(--button-primary-active); 401 | transform: translateY(0); 402 | } 403 | 404 | .aug-btn.active { 405 | background: var(--success); 406 | box-shadow: var(--shadow-md); 407 | } 408 | 409 | .reset-btn { 410 | width: 100%; 411 | padding: 12px 16px; 412 | background: var(--text-muted); 413 | color: white; 414 | border: none; 415 | border-radius: 0.5rem; 416 | font-size: 1rem; 417 | font-weight: 600; 418 | cursor: pointer; 419 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 420 | box-shadow: var(--shadow-sm); 421 | } 422 | 423 | .reset-btn:hover { 424 | background: var(--text-tertiary); 425 | transform: translateY(-1px); 426 | box-shadow: var(--shadow-md); 427 | } 428 | 429 | /* Info panel */ 430 | .info-panel { 431 | background: var(--surface-subtle); 432 | border-radius: 0.75rem; 433 | padding: 20px; 434 | border: 1px solid var(--border-primary); 435 | } 436 | 437 | .info-panel h3 { 438 | margin-bottom: 15px; 439 | color: var(--text-primary); 440 | font-size: 1.3rem; 441 | font-weight: 600; 442 | } 443 | 444 | .info-panel ul { 445 | list-style: none; 446 | padding-left: 0; 447 | } 448 | 449 | .info-panel li { 450 | margin-bottom: 12px; 451 | padding-left: 20px; 452 | position: relative; 453 | line-height: 1.6; 454 | color: var(--text-tertiary); 455 | } 456 | 457 | .info-panel li:before { 458 | content: "•"; 459 | position: absolute; 460 | left: 0; 461 | top: 0; 462 | color: var(--accent-primary); 463 | font-weight: bold; 464 | } 465 | 466 | .info-panel strong { 467 | color: var(--text-primary); 468 | } 469 | 470 | /* Modal Styles */ 471 | .modal { 472 | display: none; 473 | position: fixed; 474 | z-index: 1000; 475 | left: 0; 476 | top: 0; 477 | width: 100%; 478 | height: 100%; 479 | background-color: rgba(0, 0, 0, 0.5); 480 | backdrop-filter: blur(5px); 481 | } 482 | 483 | .modal-content { 484 | background-color: var(--card-bg); 485 | margin: 2% auto; 486 | padding: 0; 487 | border-radius: 1rem; 488 | width: 90%; 489 | max-width: 800px; 490 | max-height: 90vh; 491 | overflow-y: auto; 492 | box-shadow: var(--shadow-xl); 493 | border: 1px solid var(--card-border); 494 | animation: modalSlideIn 0.3s ease-out; 495 | } 496 | 497 | @keyframes modalSlideIn { 498 | from { 499 | opacity: 0; 500 | transform: translateY(-50px); 501 | } 502 | to { 503 | opacity: 1; 504 | transform: translateY(0); 505 | } 506 | } 507 | 508 | .modal-header { 509 | background: var(--button-primary-bg); 510 | color: var(--button-primary-text); 511 | padding: 20px 30px; 512 | border-radius: 1rem 1rem 0 0; 513 | display: flex; 514 | justify-content: space-between; 515 | align-items: center; 516 | } 517 | 518 | .modal-header h2 { 519 | margin: 0; 520 | font-size: 1.5rem; 521 | font-weight: 600; 522 | } 523 | 524 | .close { 525 | color: var(--button-primary-text); 526 | font-size: 28px; 527 | font-weight: bold; 528 | cursor: pointer; 529 | transition: opacity 0.3s ease; 530 | } 531 | 532 | .close:hover { 533 | opacity: 0.7; 534 | } 535 | 536 | .modal-body { 537 | padding: 30px; 538 | } 539 | 540 | .help-section { 541 | margin-bottom: 25px; 542 | padding-bottom: 20px; 543 | border-bottom: 1px solid var(--border-primary); 544 | } 545 | 546 | .help-section:last-child { 547 | border-bottom: none; 548 | margin-bottom: 0; 549 | } 550 | 551 | .help-section h3 { 552 | color: var(--accent-primary); 553 | margin-bottom: 15px; 554 | font-size: 1.2rem; 555 | font-weight: 600; 556 | } 557 | 558 | .help-section p { 559 | line-height: 1.6; 560 | color: var(--text-tertiary); 561 | margin-bottom: 15px; 562 | } 563 | 564 | .help-section ul, .help-section ol { 565 | margin-left: 20px; 566 | line-height: 1.6; 567 | } 568 | 569 | .help-section li { 570 | margin-bottom: 8px; 571 | color: var(--text-tertiary); 572 | } 573 | 574 | .help-section strong { 575 | color: var(--text-primary); 576 | font-weight: 600; 577 | } 578 | 579 | /* Responsive modal */ 580 | @media (max-width: 768px) { 581 | .modal-content { 582 | width: 95%; 583 | margin: 5% auto; 584 | } 585 | 586 | .modal-header { 587 | padding: 15px 20px; 588 | } 589 | 590 | .modal-header h2 { 591 | font-size: 1.3rem; 592 | } 593 | 594 | .modal-body { 595 | padding: 20px; 596 | } 597 | } 598 | 599 | /* Responsive design */ 600 | @media (max-width: 1024px) { 601 | .main-content { 602 | grid-template-columns: 1fr; 603 | gap: 20px; 604 | } 605 | 606 | .image-container { 607 | grid-template-columns: 1fr; 608 | } 609 | 610 | header h1 { 611 | font-size: 2rem; 612 | } 613 | } 614 | 615 | @media (max-width: 768px) { 616 | .container { 617 | padding: 15px; 618 | } 619 | 620 | header { 621 | margin: 0 15px 30px 15px; 622 | padding: 1rem; 623 | } 624 | 625 | header h1 { 626 | font-size: 1.8rem; 627 | } 628 | 629 | header p { 630 | font-size: 1rem; 631 | } 632 | 633 | .controls-section { 634 | padding: 20px; 635 | } 636 | 637 | .control-group { 638 | padding: 12px; 639 | } 640 | } 641 | 642 | /* Focus-visible styles */ 643 | :focus-visible { 644 | outline: 2px solid var(--border-focus); 645 | outline-offset: 2px; 646 | } 647 | 648 | /* Smooth transitions */ 649 | *, 650 | *::before, 651 | *::after { 652 | transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), 653 | color 0.2s cubic-bezier(0.4, 0, 0.2, 1), 654 | border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), 655 | box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1), 656 | transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); 657 | } --------------------------------------------------------------------------------