summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRishi-k-s <rishikrishna.sr@gmail.com>2025-07-25 12:17:54 +0530
committerRishi-k-s <rishikrishna.sr@gmail.com>2025-07-25 12:17:54 +0530
commit9c1a4b4b9f76ba38109e4528daa1f5fa324f830b (patch)
tree62f170a3a037d24fb48161620a225ea819b87930
first commit
-rw-r--r--.vscode/settings.json3
-rw-r--r--README.md81
-rw-r--r--index.html95
-rw-r--r--script.js420
-rw-r--r--style.css287
5 files changed, 886 insertions, 0 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..6f3a291
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "liveServer.settings.port": 5501
+} \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8b53a01
--- /dev/null
+++ b/README.md
@@ -0,0 +1,81 @@
+# Image to Halftone Converter
+
+A web-based tool that converts images into beautiful halftone patterns using vanilla JavaScript and HTML5 Canvas. This project is inspired by traditional halftone printing techniques used in newspapers and comic books.
+
+## Features
+
+- **Image Upload**: Drag and drop or click to upload images
+- **Real-time Controls**: Adjust halftone parameters with interactive sliders
+- **Multiple Effect Types**:
+ - Basic Halftone
+ - Duotone (two-color effect)
+ - Tritone (three-color effect)
+- **Customizable Parameters**:
+ - Dot size (1-20 pixels)
+ - Dot spacing (1-15 pixels)
+ - Screen angle (0-89 degrees)
+ - Dot color
+ - Background color
+- **Download Results**: Save your halftone images as PNG files
+- **Responsive Design**: Works on desktop and mobile devices
+
+## How to Use
+
+1. **Upload an Image**: Click on the upload area or drag and drop an image file
+2. **Adjust Settings**: Use the control panel to customize:
+ - **Dot Size**: Controls how large the halftone dots can get
+ - **Dot Spacing**: Controls the distance between dot centers
+ - **Screen Angle**: Rotates the halftone pattern
+ - **Colors**: Choose dot and background colors
+ - **Effect Type**: Select from basic, duotone, or tritone effects
+3. **Generate**: Click "Generate Halftone" to create your effect
+4. **Download**: Save the result to your device
+
+## Technical Details
+
+This implementation is based on the halftone printing process where:
+- Images are converted to grayscale
+- Dot sizes vary based on the brightness of the original image
+- Darker areas create larger dots, lighter areas create smaller dots
+- Multiple angles can be used to create complex color effects
+
+The halftone algorithm:
+1. Samples the source image at regular intervals
+2. Converts pixel values to grayscale using luminance formula (0.299*R + 0.587*G + 0.114*B)
+3. Maps brightness values to dot sizes
+4. Draws circles using HTML5 Canvas arc method
+5. Supports rotation for different screen angles
+
+## File Structure
+
+```
+├── index.html # Main HTML structure
+├── style.css # Styling and responsive design
+├── script.js # Halftone conversion logic
+└── README.md # This documentation
+```
+
+## Browser Compatibility
+
+- Modern browsers supporting HTML5 Canvas
+- File API for image upload
+- ES6+ JavaScript features
+
+## Inspiration
+
+This project is inspired by:
+- Traditional halftone printing techniques
+- [anderoonies.github.io/projects/halftone/](https://anderoonies.github.io/projects/halftone/)
+- Comic book and newspaper printing methods
+
+## Future Enhancements
+
+- CMYK color separation
+- Batch processing
+- More effect presets
+- Print-ready output formats
+- Advanced moiré pattern control
+
+## License
+
+This project is open source and available under the MIT License.
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..dcfa4ca
--- /dev/null
+++ b/index.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Image to Halftone Converter</title>
+ <link rel="stylesheet" href="style.css">
+</head>
+<body>
+ <div class="container">
+ <header>
+ <h1>Image to Halftone Converter</h1>
+ <p>Convert your images into beautiful halftone patterns</p>
+ </header>
+
+ <main>
+ <section class="upload-section">
+ <div class="upload-area" id="uploadArea">
+ <div class="upload-content">
+ <div class="upload-icon">📷</div>
+ <p>Click here or drag and drop an image</p>
+ <input type="file" id="imageInput" accept="image/*" style="display: none;">
+ </div>
+ </div>
+ </section>
+
+ <section class="controls-section" id="controlsSection" style="display: none;">
+ <div class="controls-grid">
+ <div class="control-group">
+ <label for="dotSize">Dot Size:</label>
+ <input type="range" id="dotSize" min="1" max="20" value="5">
+ <span id="dotSizeValue">5</span>px
+ </div>
+
+ <div class="control-group">
+ <label for="dotResolution">Dot Spacing:</label>
+ <input type="range" id="dotResolution" min="1" max="15" value="4">
+ <span id="dotResolutionValue">4</span>px
+ </div>
+
+ <div class="control-group">
+ <label for="angle">Screen Angle:</label>
+ <input type="range" id="angle" min="0" max="89" value="0">
+ <span id="angleValue">0</span>°
+ </div>
+
+ <div class="control-group">
+ <label for="halftoneColor">Dot Color:</label>
+ <input type="color" id="halftoneColor" value="#000000">
+ </div>
+
+ <div class="control-group">
+ <label for="backgroundColor">Background:</label>
+ <input type="color" id="backgroundColor" value="#ffffff">
+ </div>
+
+ <div class="control-group">
+ <label for="effectType">Effect Type:</label>
+ <select id="effectType">
+ <option value="basic">Basic Halftone</option>
+ <option value="duotone">Duotone</option>
+ <option value="tritone">Tritone</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="button-group">
+ <button id="generateBtn">Generate Halftone</button>
+ <button id="downloadBtn" style="display: none;">Download Result</button>
+ <button id="resetBtn">Reset</button>
+ </div>
+ </section>
+
+ <section class="preview-section">
+ <div class="canvas-container">
+ <div class="canvas-wrapper">
+ <h3>Original</h3>
+ <canvas id="sourceCanvas"></canvas>
+ </div>
+ <div class="canvas-wrapper">
+ <h3>Halftone Result</h3>
+ <canvas id="targetCanvas"></canvas>
+ </div>
+ </div>
+ </section>
+ </main>
+
+ <footer>
+ <p>Inspired by traditional halftone printing techniques</p>
+ </footer>
+ </div>
+
+ <script src="script.js"></script>
+</body>
+</html>
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..eca6ff8
--- /dev/null
+++ b/script.js
@@ -0,0 +1,420 @@
+class HalftoneConverter {
+ constructor() {
+ this.sourceCanvas = document.getElementById('sourceCanvas');
+ this.targetCanvas = document.getElementById('targetCanvas');
+ this.sourceCtx = this.sourceCanvas.getContext('2d');
+ this.targetCtx = this.targetCanvas.getContext('2d');
+ this.currentImage = null;
+
+ // Enable anti-aliasing and smooth rendering
+ this.sourceCtx.imageSmoothingEnabled = true;
+ this.sourceCtx.imageSmoothingQuality = 'high';
+ this.targetCtx.imageSmoothingEnabled = true;
+ this.targetCtx.imageSmoothingQuality = 'high';
+
+ this.initializeEventListeners();
+ this.initializeControls();
+ }
+
+ initializeEventListeners() {
+ const uploadArea = document.getElementById('uploadArea');
+ const imageInput = document.getElementById('imageInput');
+ const generateBtn = document.getElementById('generateBtn');
+ const downloadBtn = document.getElementById('downloadBtn');
+ const resetBtn = document.getElementById('resetBtn');
+
+ // File upload handling
+ uploadArea.addEventListener('click', () => imageInput.click());
+ uploadArea.addEventListener('dragover', this.handleDragOver.bind(this));
+ uploadArea.addEventListener('dragleave', this.handleDragLeave.bind(this));
+ uploadArea.addEventListener('drop', this.handleDrop.bind(this));
+ imageInput.addEventListener('change', this.handleFileSelect.bind(this));
+
+ // Button handlers
+ generateBtn.addEventListener('click', this.generateHalftone.bind(this));
+ downloadBtn.addEventListener('click', this.downloadResult.bind(this));
+ resetBtn.addEventListener('click', this.reset.bind(this));
+
+ // Real-time control updates
+ const controls = ['dotSize', 'dotResolution', 'angle', 'halftoneColor', 'backgroundColor', 'effectType'];
+ controls.forEach(id => {
+ const element = document.getElementById(id);
+ element.addEventListener('input', this.updateControlValues.bind(this));
+ });
+ }
+
+ initializeControls() {
+ this.updateControlValues();
+ }
+
+ updateControlValues() {
+ const dotSize = document.getElementById('dotSize');
+ const dotResolution = document.getElementById('dotResolution');
+ const angle = document.getElementById('angle');
+
+ document.getElementById('dotSizeValue').textContent = dotSize.value;
+ document.getElementById('dotResolutionValue').textContent = dotResolution.value;
+ document.getElementById('angleValue').textContent = angle.value;
+ }
+
+ handleDragOver(e) {
+ e.preventDefault();
+ document.getElementById('uploadArea').classList.add('dragover');
+ }
+
+ handleDragLeave(e) {
+ e.preventDefault();
+ document.getElementById('uploadArea').classList.remove('dragover');
+ }
+
+ handleDrop(e) {
+ e.preventDefault();
+ document.getElementById('uploadArea').classList.remove('dragover');
+ const files = e.dataTransfer.files;
+ if (files.length > 0) {
+ this.loadImage(files[0]);
+ }
+ }
+
+ handleFileSelect(e) {
+ const file = e.target.files[0];
+ if (file) {
+ this.loadImage(file);
+ }
+ }
+
+ loadImage(file) {
+ if (!file.type.startsWith('image/')) {
+ alert('Please select a valid image file.');
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const img = new Image();
+ img.onload = () => {
+ this.currentImage = img;
+ this.displayOriginalImage();
+ document.getElementById('controlsSection').style.display = 'block';
+ document.getElementById('controlsSection').classList.add('fade-in');
+ };
+ img.src = e.target.result;
+ };
+ reader.readAsDataURL(file);
+ }
+
+ displayOriginalImage() {
+ const maxWidth = 400;
+ const maxHeight = 400;
+
+ let { width, height } = this.currentImage;
+
+ // Calculate aspect ratio and resize if needed
+ if (width > maxWidth || height > maxHeight) {
+ const aspectRatio = width / height;
+ if (width > height) {
+ width = maxWidth;
+ height = maxWidth / aspectRatio;
+ } else {
+ height = maxHeight;
+ width = maxHeight * aspectRatio;
+ }
+ }
+
+ this.sourceCanvas.width = width;
+ this.sourceCanvas.height = height;
+
+ // Enable high-quality image rendering
+ this.sourceCtx.imageSmoothingEnabled = true;
+ this.sourceCtx.imageSmoothingQuality = 'high';
+
+ this.sourceCtx.drawImage(this.currentImage, 0, 0, width, height);
+ }
+
+ // Utility functions from the reference
+ positionToDataIndex(x, y, width) {
+ return (y * width + x) * 4;
+ }
+
+ map(value, minA, maxA, minB, maxB) {
+ return ((value - minA) / (maxA - minA)) * (maxB - minB) + minB;
+ }
+
+ rotatePointAboutPosition([x, y], [rotX, rotY], angle) {
+ return [
+ (x - rotX) * Math.cos(angle) - (y - rotY) * Math.sin(angle) + rotX,
+ (x - rotX) * Math.sin(angle) + (y - rotY) * Math.cos(angle) + rotY,
+ ];
+ }
+
+ halftone({ angle, dotSize, dotResolution, targetCtx, sourceCtx, width, height, color, layer = false }) {
+ const sourceImageData = sourceCtx.getImageData(0, 0, width, height);
+ angle = (angle * Math.PI) / 180;
+
+ // Enable anti-aliasing for smoother circles
+ targetCtx.imageSmoothingEnabled = true;
+ targetCtx.imageSmoothingQuality = 'high';
+
+ if (!layer) {
+ targetCtx.fillStyle = document.getElementById('backgroundColor').value;
+ targetCtx.fillRect(0, 0, width, height);
+ }
+
+ targetCtx.fillStyle = color;
+
+ // Get the four corners of the screen
+ const tl = [0, 0];
+ const tr = [width, 0];
+ const br = [width, height];
+ const bl = [0, height];
+
+ // Rotate the screen, then find the minimum and maximum of the values
+ const boundaries = [tl, br, tr, bl].map(([x, y]) => {
+ return this.rotatePointAboutPosition([x, y], [width / 2, height / 2], angle);
+ });
+
+ const minX = Math.min(...boundaries.map((point) => point[0])) | 0;
+ const minY = Math.min(...boundaries.map((point) => point[1])) | 0;
+ const maxY = Math.max(...boundaries.map((point) => point[1])) | 0;
+ const maxX = Math.max(...boundaries.map((point) => point[0])) | 0;
+
+ for (let y = minY; y < maxY; y += dotResolution) {
+ for (let x = minX; x < maxX; x += dotResolution) {
+ let [rotatedX, rotatedY] = this.rotatePointAboutPosition(
+ [x, y],
+ [width / 2, height / 2],
+ -angle
+ );
+
+ if (rotatedX < 0 || rotatedY < 0 || rotatedX > width || rotatedY > height) {
+ continue;
+ }
+
+ const index = this.positionToDataIndex(
+ Math.floor(rotatedX),
+ Math.floor(rotatedY),
+ width
+ );
+
+ // Convert to grayscale using luminance formula
+ const r = sourceImageData.data[index];
+ const g = sourceImageData.data[index + 1];
+ const b = sourceImageData.data[index + 2];
+ const alpha = sourceImageData.data[index + 3];
+ const value = (r * 0.299 + g * 0.587 + b * 0.114);
+
+ if (alpha > 0) {
+ const circleRadius = this.map(value, 0, 255, dotSize / 2, 0);
+
+ if (circleRadius > 0.1) { // Only draw circles with meaningful size
+ targetCtx.beginPath();
+ targetCtx.arc(rotatedX, rotatedY, circleRadius, 0, Math.PI * 2);
+ targetCtx.closePath();
+ targetCtx.fill();
+ }
+ }
+ }
+ }
+ }
+
+ generateBasicHalftone() {
+ const dotSize = parseInt(document.getElementById('dotSize').value);
+ const dotResolution = parseInt(document.getElementById('dotResolution').value);
+ const angle = parseInt(document.getElementById('angle').value);
+ const color = document.getElementById('halftoneColor').value;
+
+ this.halftone({
+ angle: angle,
+ dotSize: dotSize,
+ dotResolution: dotResolution,
+ targetCtx: this.targetCtx,
+ sourceCtx: this.sourceCtx,
+ width: this.sourceCanvas.width,
+ height: this.sourceCanvas.height,
+ color: color
+ });
+ }
+
+ generateDuotone() {
+ const dotSize = parseInt(document.getElementById('dotSize').value);
+ const dotResolution = parseInt(document.getElementById('dotResolution').value);
+ const angle = parseInt(document.getElementById('angle').value);
+
+ // Create two layers - one for highlights, one for shadows
+ const tempCanvas = document.createElement('canvas');
+ const tempCtx = tempCanvas.getContext('2d');
+ tempCanvas.width = this.sourceCanvas.width;
+ tempCanvas.height = this.sourceCanvas.height;
+
+ // Layer 1: All values (light color)
+ this.halftone({
+ angle: angle + 15,
+ dotSize: dotSize,
+ dotResolution: dotResolution,
+ targetCtx: this.targetCtx,
+ sourceCtx: this.sourceCtx,
+ width: this.sourceCanvas.width,
+ height: this.sourceCanvas.height,
+ color: '#8B4513' // Brown
+ });
+
+ // Create shadow layer
+ const sourceImageData = this.sourceCtx.getImageData(0, 0, this.sourceCanvas.width, this.sourceCanvas.height);
+ const shadowImageData = tempCtx.createImageData(this.sourceCanvas.width, this.sourceCanvas.height);
+
+ for (let i = 0; i < sourceImageData.data.length; i += 4) {
+ const r = sourceImageData.data[i];
+ const g = sourceImageData.data[i + 1];
+ const b = sourceImageData.data[i + 2];
+ const value = (r * 0.299 + g * 0.587 + b * 0.114);
+
+ if (value < 127) {
+ const adjustedValue = this.map(value, 0, 127, 0, 255);
+ shadowImageData.data[i] = adjustedValue;
+ shadowImageData.data[i + 1] = adjustedValue;
+ shadowImageData.data[i + 2] = adjustedValue;
+ shadowImageData.data[i + 3] = 255;
+ } else {
+ shadowImageData.data[i] = 255;
+ shadowImageData.data[i + 1] = 255;
+ shadowImageData.data[i + 2] = 255;
+ shadowImageData.data[i + 3] = 255;
+ }
+ }
+
+ tempCtx.putImageData(shadowImageData, 0, 0);
+
+ // Layer 2: Shadows (black)
+ this.halftone({
+ angle: angle,
+ dotSize: dotSize,
+ dotResolution: dotResolution,
+ targetCtx: this.targetCtx,
+ sourceCtx: tempCtx,
+ width: this.sourceCanvas.width,
+ height: this.sourceCanvas.height,
+ color: '#000000',
+ layer: true
+ });
+ }
+
+ generateTritone() {
+ const dotSize = parseInt(document.getElementById('dotSize').value);
+ const dotResolution = parseInt(document.getElementById('dotResolution').value);
+ const angle = parseInt(document.getElementById('angle').value);
+
+ const colors = ['#FFD700', '#FF6347', '#000000']; // Gold, Tomato, Black
+ const angles = [angle, angle + 30, angle + 60];
+
+ colors.forEach((color, index) => {
+ this.halftone({
+ angle: angles[index],
+ dotSize: dotSize,
+ dotResolution: dotResolution,
+ targetCtx: this.targetCtx,
+ sourceCtx: this.sourceCtx,
+ width: this.sourceCanvas.width,
+ height: this.sourceCanvas.height,
+ color: color,
+ layer: index > 0
+ });
+ });
+ }
+
+ generateHalftone() {
+ if (!this.currentImage) {
+ alert('Please upload an image first.');
+ return;
+ }
+
+ // Set up target canvas with high-resolution rendering
+ const scaleFactor = 2; // Render at 2x resolution for smoother output
+ const displayWidth = this.sourceCanvas.width;
+ const displayHeight = this.sourceCanvas.height;
+
+ // Set actual canvas size to higher resolution
+ this.targetCanvas.width = displayWidth * scaleFactor;
+ this.targetCanvas.height = displayHeight * scaleFactor;
+
+ // Scale the context to match
+ this.targetCtx.scale(scaleFactor, scaleFactor);
+
+ // Set display size to normal
+ this.targetCanvas.style.width = displayWidth + 'px';
+ this.targetCanvas.style.height = displayHeight + 'px';
+
+ const effectType = document.getElementById('effectType').value;
+
+ // Add loading state
+ document.getElementById('generateBtn').classList.add('loading');
+ document.getElementById('generateBtn').textContent = 'Generating...';
+
+ // Use setTimeout to allow UI to update
+ setTimeout(() => {
+ try {
+ switch (effectType) {
+ case 'basic':
+ this.generateBasicHalftone();
+ break;
+ case 'duotone':
+ this.generateDuotone();
+ break;
+ case 'tritone':
+ this.generateTritone();
+ break;
+ }
+
+ document.getElementById('downloadBtn').style.display = 'inline-block';
+ document.getElementById('downloadBtn').classList.add('fade-in');
+ } catch (error) {
+ console.error('Error generating halftone:', error);
+ alert('An error occurred while generating the halftone. Please try again.');
+ } finally {
+ document.getElementById('generateBtn').classList.remove('loading');
+ document.getElementById('generateBtn').textContent = 'Generate Halftone';
+ }
+ }, 100);
+ }
+
+ downloadResult() {
+ if (!this.targetCanvas.width) {
+ alert('Please generate a halftone first.');
+ return;
+ }
+
+ const link = document.createElement('a');
+ link.download = 'halftone-result.png';
+ link.href = this.targetCanvas.toDataURL();
+ link.click();
+ }
+
+ reset() {
+ this.currentImage = null;
+ this.sourceCtx.clearRect(0, 0, this.sourceCanvas.width, this.sourceCanvas.height);
+ this.targetCtx.clearRect(0, 0, this.targetCanvas.width, this.targetCanvas.height);
+
+ // Reset canvas transformations
+ this.targetCtx.setTransform(1, 0, 0, 1, 0, 0);
+ this.targetCanvas.style.width = '';
+ this.targetCanvas.style.height = '';
+
+ document.getElementById('controlsSection').style.display = 'none';
+ document.getElementById('downloadBtn').style.display = 'none';
+ document.getElementById('imageInput').value = '';
+
+ // Reset controls to default values
+ document.getElementById('dotSize').value = 5;
+ document.getElementById('dotResolution').value = 4;
+ document.getElementById('angle').value = 0;
+ document.getElementById('halftoneColor').value = '#000000';
+ document.getElementById('backgroundColor').value = '#ffffff';
+ document.getElementById('effectType').value = 'basic';
+
+ this.updateControlValues();
+ }
+}
+
+// Initialize the application when the DOM is loaded
+document.addEventListener('DOMContentLoaded', () => {
+ new HalftoneConverter();
+});
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..f36fd4c
--- /dev/null
+++ b/style.css
@@ -0,0 +1,287 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ line-height: 1.6;
+ color: #333;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+header {
+ text-align: center;
+ margin-bottom: 40px;
+ color: white;
+}
+
+header h1 {
+ font-size: 2.5rem;
+ margin-bottom: 10px;
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
+}
+
+header p {
+ font-size: 1.1rem;
+ opacity: 0.9;
+}
+
+main {
+ background: white;
+ border-radius: 15px;
+ padding: 30px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
+}
+
+.upload-section {
+ margin-bottom: 30px;
+}
+
+.upload-area {
+ border: 3px dashed #ccc;
+ border-radius: 10px;
+ padding: 40px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background: #f9f9f9;
+}
+
+.upload-area:hover {
+ border-color: #667eea;
+ background: #f0f4ff;
+}
+
+.upload-area.dragover {
+ border-color: #667eea;
+ background: #e8f0fe;
+}
+
+.upload-content {
+ pointer-events: none;
+}
+
+.upload-icon {
+ font-size: 3rem;
+ margin-bottom: 15px;
+}
+
+.upload-area p {
+ font-size: 1.1rem;
+ color: #666;
+}
+
+.controls-section {
+ margin-bottom: 30px;
+ padding: 25px;
+ background: #f8f9fa;
+ border-radius: 10px;
+}
+
+.controls-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 20px;
+ margin-bottom: 25px;
+}
+
+.control-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.control-group label {
+ font-weight: 600;
+ color: #444;
+}
+
+.control-group input[type="range"] {
+ width: 100%;
+ height: 6px;
+ background: #ddd;
+ outline: none;
+ border-radius: 3px;
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.control-group input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ background: #667eea;
+ cursor: pointer;
+ border-radius: 50%;
+}
+
+.control-group input[type="range"]::-moz-range-thumb {
+ width: 20px;
+ height: 20px;
+ background: #667eea;
+ cursor: pointer;
+ border-radius: 50%;
+ border: none;
+}
+
+.control-group input[type="color"] {
+ width: 60px;
+ height: 40px;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+}
+
+.control-group select {
+ padding: 8px 12px;
+ border: 2px solid #ddd;
+ border-radius: 5px;
+ background: white;
+ font-size: 14px;
+}
+
+.button-group {
+ display: flex;
+ gap: 15px;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+button {
+ padding: 12px 24px;
+ border: none;
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+#generateBtn {
+ background: #28a745;
+ color: white;
+}
+
+#generateBtn:hover {
+ background: #218838;
+ transform: translateY(-2px);
+}
+
+#downloadBtn {
+ background: #007bff;
+ color: white;
+}
+
+#downloadBtn:hover {
+ background: #0056b3;
+ transform: translateY(-2px);
+}
+
+#resetBtn {
+ background: #6c757d;
+ color: white;
+}
+
+#resetBtn:hover {
+ background: #545b62;
+ transform: translateY(-2px);
+}
+
+.preview-section {
+ margin-top: 30px;
+}
+
+.canvas-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 30px;
+ margin-top: 20px;
+}
+
+.canvas-wrapper {
+ text-align: center;
+}
+
+.canvas-wrapper h3 {
+ margin-bottom: 15px;
+ color: #444;
+ font-size: 1.2rem;
+}
+
+canvas {
+ max-width: 100%;
+ border: 2px solid #ddd;
+ border-radius: 8px;
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
+}
+
+footer {
+ text-align: center;
+ margin-top: 40px;
+ color: white;
+ opacity: 0.8;
+}
+
+.loading {
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+ .container {
+ padding: 15px;
+ }
+
+ header h1 {
+ font-size: 2rem;
+ }
+
+ main {
+ padding: 20px;
+ }
+
+ .controls-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .canvas-container {
+ grid-template-columns: 1fr;
+ gap: 20px;
+ }
+
+ .button-group {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ button {
+ width: 200px;
+ }
+}
+
+/* Animation for smooth transitions */
+.fade-in {
+ animation: fadeIn 0.5s ease-in;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}