├── .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 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
Original Image
23 |
24 |
25 |
26 |
27 |
28 |
29 |
Augmented Image
30 |
31 |
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 | Choose an image:
53 |
54 | Loading images...
55 |
56 |
57 |
58 |
59 |
Augmentation Techniques
60 |
61 |
62 |
63 |
Geometric Transformations
64 |
65 |
66 | Flip Horizontal
67 |
68 |
69 |
70 | Flip Vertical
71 |
72 |
73 |
74 | Rotation: 0°
75 |
76 |
77 |
78 |
79 | Crop: 0%
80 |
81 |
82 |
83 |
84 |
85 |
103 |
104 |
105 |
106 |
Noise & Blur
107 |
108 |
109 | Noise: 0%
110 |
111 |
112 |
113 |
114 | Blur: 0px
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | Reset All
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
138 |
139 |
140 |
What is Data Augmentation?
141 |
Data augmentation is a technique used in machine learning to artificially increase the size of your training dataset by creating modified versions of existing images. This helps models learn better and become more robust to real-world variations.
142 |
143 |
144 |
145 |
How to Use This Simulation
146 |
147 | Choose an Image: Select from the dropdown to load a sample image
148 | Apply Techniques: Use sliders and buttons to transform the image
149 | Compare Results: See original vs augmented images side by side
150 | Experiment: Try different combinations of techniques
151 | Reset: Use the reset button to start over
152 |
153 |
154 |
155 |
156 |
Geometric Transformations
157 |
158 | Flip Horizontal/Vertical: Mirror images - useful when object orientation doesn't matter
159 | Rotation: Rotate images by any angle - helps models recognize objects from different viewpoints
160 | Crop: Remove parts of the image - simulates different camera distances and focuses
161 |
162 |
163 |
164 |
165 |
Color Adjustments
166 |
167 | Brightness: Adjust lighting - handles different lighting conditions
168 | Contrast: Modify color intensity - improves feature visibility
169 | Saturation: Control color vibrancy - handles different color conditions
170 |
171 |
172 |
173 |
174 |
Noise & Blur Effects
175 |
176 | Noise: Add random pixels - makes models robust to sensor noise
177 | Blur: Soften image details - handles motion blur and focus issues
178 |
179 |
180 |
181 |
182 |
Advanced Techniques
183 |
184 | Zoom: Scale images - simulates different camera distances
185 | Shear: Skew images - handles perspective changes
186 | Mixup: Blend two images - creates smooth transitions between classes
187 | Cutout: Remove random patches - forces focus on different image parts
188 |
189 |
190 |
191 |
192 |
Pro Tips
193 |
194 | Start Simple: Begin with one technique at a time
195 | Combine Techniques: Use multiple augmentations for maximum diversity
196 | Consider Your Domain: Different techniques work better for different image types
197 | Monitor Performance: Always validate that augmentations improve your model
198 |
199 |
200 |
201 |
202 |
Real-World Applications
203 |
204 | Medical Imaging: Rotation and brightness help with different scan orientations
205 | Autonomous Driving: Weather conditions, lighting, and camera angles
206 | Satellite Imagery: Different times of day, seasons, and weather
207 | Face Recognition: Different expressions, angles, and lighting
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
--------------------------------------------------------------------------------
/simulation/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Image Augmentation Playground
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
Original Image
23 |
24 |
25 |
26 |
27 |
28 |
29 |
Augmented Image
30 |
31 |
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 | Choose an image:
53 |
54 | Loading images...
55 |
56 |
57 |
58 |
59 |
Augmentation Techniques
60 |
61 |
62 |
63 |
Geometric Transformations
64 |
65 |
66 | Flip Horizontal
67 |
68 |
69 |
70 | Flip Vertical
71 |
72 |
73 |
74 | Rotation: 0°
75 |
76 |
77 |
78 |
79 | Crop: 0%
80 |
81 |
82 |
83 |
84 |
85 |
103 |
104 |
105 |
106 |
Noise & Blur
107 |
108 |
109 | Noise: 0%
110 |
111 |
112 |
113 |
114 | Blur: 0px
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | Reset All
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
138 |
139 |
140 |
What is Data Augmentation?
141 |
Data augmentation is a technique used in machine learning to artificially increase the size of your training dataset by creating modified versions of existing images. This helps models learn better and become more robust to real-world variations.
142 |
143 |
144 |
145 |
How to Use This Simulation
146 |
147 | Choose an Image: Select from the dropdown to load a sample image
148 | Apply Techniques: Use sliders and buttons to transform the image
149 | Compare Results: See original vs augmented images side by side
150 | Experiment: Try different combinations of techniques
151 | Reset: Use the reset button to start over
152 |
153 |
154 |
155 |
156 |
Geometric Transformations
157 |
158 | Flip Horizontal/Vertical: Mirror images - useful when object orientation doesn't matter
159 | Rotation: Rotate images by any angle - helps models recognize objects from different viewpoints
160 | Crop: Remove parts of the image - simulates different camera distances and focuses
161 |
162 |
163 |
164 |
165 |
Color Adjustments
166 |
167 | Brightness: Adjust lighting - handles different lighting conditions
168 | Contrast: Modify color intensity - improves feature visibility
169 | Saturation: Control color vibrancy - handles different color conditions
170 |
171 |
172 |
173 |
174 |
Noise & Blur Effects
175 |
176 | Noise: Add random pixels - makes models robust to sensor noise
177 | Blur: Soften image details - handles motion blur and focus issues
178 |
179 |
180 |
181 |
182 |
Advanced Techniques
183 |
184 | Zoom: Scale images - simulates different camera distances
185 | Shear: Skew images - handles perspective changes
186 | Mixup: Blend two images - creates smooth transitions between classes
187 | Cutout: Remove random patches - forces focus on different image parts
188 |
189 |
190 |
191 |
192 |
Pro Tips
193 |
194 | Start Simple: Begin with one technique at a time
195 | Combine Techniques: Use multiple augmentations for maximum diversity
196 | Consider Your Domain: Different techniques work better for different image types
197 | Monitor Performance: Always validate that augmentations improve your model
198 |
199 |
200 |
201 |
202 |
Real-World Applications
203 |
204 | Medical Imaging: Rotation and brightness help with different scan orientations
205 | Autonomous Driving: Weather conditions, lighting, and camera angles
206 | Satellite Imagery: Different times of day, seasons, and weather
207 | Face Recognition: Different expressions, angles, and lighting
208 |
209 |
210 |
211 |
212 |
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 | }
--------------------------------------------------------------------------------