ppprew/dotmatrix-g-code-wizard
0
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>DotMatrix G-Code Wizard</title>7 <link rel="icon" type="image/x-icon" href="/static/favicon.ico">8 <script src="https://cdn.tailwindcss.com"></script>9 <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>10 <script src="https://unpkg.com/feather-icons"></script>11 <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>12 <script>13 tailwind.config = {14 theme: {15 extend: {16 colors: {17 undefined: {18 500: '#6366f1',19 600: '#4f46e5',20 }21 }22 }23 }24 }25 </script>26 <style>27 .dropzone {28 border: 2px dashed #6366f1;29 transition: all 0.3s ease;30 }31 .dropzone.active {32 border-color: #4f46e5;33 background-color: rgba(99, 102, 241, 0.1);34 }35 .slider-thumb::-webkit-slider-thumb {36 -webkit-appearance: none;37 appearance: none;38 width: 20px;39 height: 20px;40 background: #6366f1;41 cursor: pointer;42 border-radius: 50%;43 }44 .slider-thumb::-moz-range-thumb {45 width: 20px;46 height: 20px;47 background: #6366f1;48 cursor: pointer;49 border-radius: 50%;50 }51 #canvas-container {52 position: relative;53 }54 canvas {55 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);56 }57 </style>58</head>59<body class="bg-gray-50 min-h-screen">60 <div class="max-w-6xl mx-auto px-4 py-8">61 <!-- Header -->62 <header class="mb-12 text-center">63 <h1 class="text-4xl font-bold text-gray-800 mb-2">DotMatrix G-Code Wizard 🌀</h1>64 <p class="text-gray-600">Transform your images into halftone patterns and generate G-code for fabrication</p>65 </header>66 67 <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">68 <!-- Left Column - Controls -->69 <div class="lg:col-span-1 bg-white rounded-xl shadow-md p-6">70 <h2 class="text-xl font-semibold text-gray-800 mb-6">Image Upload</h2>71 72 <!-- Dropzone -->73 <div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer mb-6">74 <div class="flex flex-col items-center justify-center">75 <i data-feather="upload" class="w-12 h-12 text-undefined-500 mb-3"></i>76 <p class="text-gray-600">Drag & drop your image here</p>77 <p class="text-sm text-gray-500 mt-1">or click to browse</p>78 </div>79 <input type="file" id="fileInput" class="hidden" accept="image/*">80 </div>81 82 <h2 class="text-xl font-semibold text-gray-800 mb-6 mt-8">Halftone Settings</h2>83 84 <!-- Controls -->85 <div class="space-y-6">86 <div>87 <div class="flex justify-between items-center mb-2">88 <label class="text-gray-700">Spot Radius</label>89 <span id="radiusValue" class="text-gray-600">5px</span>90 </div>91 <input type="range" id="radiusSlider" min="1" max="20" value="5" class="w-full slider-thumb">92 <p class="text-xs text-gray-500 mt-1">Adjust the size of halftone dots</p>93 </div>94 95 <div>96 <div class="flex justify-between items-center mb-2">97 <label class="text-gray-700">Contrast</label>98 <span id="contrastValue" class="text-gray-600">50%</span>99 </div>100 <input type="range" id="contrastSlider" min="0" max="100" value="50" class="w-full slider-thumb">101 <p class="text-xs text-gray-500 mt-1">Control the contrast of the effect</p>102 </div>103 104 <div>105 <div class="flex justify-between items-center mb-2">106 <label class="text-gray-700">Angle</label>107 <span id="angleValue" class="text-gray-600">45°</span>108 </div>109 <input type="range" id="angleSlider" min="0" max="180" value="45" class="w-full slider-thumb">110 <p class="text-xs text-gray-500 mt-1">Set the angle of the pattern</p>111 </div>112 113 <div>114 <div class="flex justify-between items-center mb-2">115 <label class="text-gray-700">Density</label>116 <span id="densityValue" class="text-gray-600">50%</span>117 </div>118 <input type="range" id="densitySlider" min="10" max="90" value="50" class="w-full slider-thumb">119 <p class="text-xs text-gray-500 mt-1">Adjust dot distribution density</p>120 </div>121 </div>122 123 <!-- Action Buttons -->124 <div class="flex space-x-3 mt-8">125 <button id="resetBtn" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 py-2 px-4 rounded-lg transition flex items-center justify-center">126 <i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i> Reset127 </button>128 <button id="generateBtn" class="flex-1 bg-undefined-600 hover:bg-undefined-700 text-white py-2 px-4 rounded-lg transition flex items-center justify-center">129 <i data-feather="code" class="w-4 h-4 mr-2"></i> Generate G-Code130 </button>131 </div>132 </div>133 134 <!-- Right Column - Canvas and Output -->135 <div class="lg:col-span-2 space-y-6">136 <!-- Canvas Preview -->137 <div class="bg-white rounded-xl shadow-md p-6">138 <h2 class="text-xl font-semibold text-gray-800 mb-4">Preview</h2>139 <div id="canvas-container" class="relative border border-gray-200 rounded-lg overflow-hidden">140 <canvas id="previewCanvas" class="w-full"></canvas>141 <div id="noImagePlaceholder" class="absolute inset-0 flex items-center justify-center bg-gray-100">142 <div class="text-center p-6">143 <i data-feather="image" class="w-12 h-12 text-gray-400 mx-auto mb-3"></i>144 <p class="text-gray-500">Upload an image to see the preview</p>145 </div>146 </div>147 </div>148 </div>149 150 <!-- G-Code Output -->151 <div class="bg-white rounded-xl shadow-md p-6">152 <div class="flex justify-between items-center mb-4">153 <h2 class="text-xl font-semibold text-gray-800">G-Code Output</h2>154 <button id="downloadBtn" class="bg-undefined-600 hover:bg-undefined-700 text-white py-1 px-3 rounded-lg text-sm transition flex items-center disabled:opacity-50" disabled>155 <i data-feather="download" class="w-3 h-3 mr-2"></i> Download156 </button>157 </div>158 <textarea id="gcodeOutput" class="w-full h-64 p-3 border border-gray-200 rounded-lg font-mono text-sm bg-gray-50" placeholder="G-code will appear here..." readonly></textarea>159 </div>160 </div>161 </div>162 </div>163 164 <script>165 document.addEventListener('DOMContentLoaded', function() {166 feather.replace();167 168 // Canvas setup169 const canvas = new fabric.Canvas('previewCanvas', {170 backgroundColor: '#ffffff',171 preserveObjectStacking: true172 });173 174 // UI Elements175 const dropzone = document.getElementById('dropzone');176 const fileInput = document.getElementById('fileInput');177 const noImagePlaceholder = document.getElementById('noImagePlaceholder');178 const radiusSlider = document.getElementById('radiusSlider');179 const contrastSlider = document.getElementById('contrastSlider');180 const angleSlider = document.getElementById('angleSlider');181 const densitySlider = document.getElementById('densitySlider');182 const radiusValue = document.getElementById('radiusValue');183 const contrastValue = document.getElementById('contrastValue');184 const angleValue = document.getElementById('angleValue');185 const densityValue = document.getElementById('densityValue');186 const resetBtn = document.getElementById('resetBtn');187 const generateBtn = document.getElementById('generateBtn');188 const downloadBtn = document.getElementById('downloadBtn');189 const gcodeOutput = document.getElementById('gcodeOutput');190 191 // Default values192 let currentImage = null;193 let settings = {194 radius: 5,195 contrast: 50,196 angle: 45,197 density: 50198 };199 200 // Event Listeners201 dropzone.addEventListener('click', () => fileInput.click());202 dropzone.addEventListener('dragover', (e) => {203 e.preventDefault();204 dropzone.classList.add('active');205 });206 dropzone.addEventListener('dragleave', () => dropzone.classList.remove('active'));207 dropzone.addEventListener('drop', (e) => {208 e.preventDefault();209 dropzone.classList.remove('active');210 if (e.dataTransfer.files.length) {211 handleImageUpload(e.dataTransfer.files[0]);212 }213 });214 215 fileInput.addEventListener('change', (e) => {216 if (e.target.files.length) {217 handleImageUpload(e.target.files[0]);218 }219 });220 221 radiusSlider.addEventListener('input', updateSettings);222 contrastSlider.addEventListener('input', updateSettings);223 angleSlider.addEventListener('input', updateSettings);224 densitySlider.addEventListener('input', updateSettings);225 226 resetBtn.addEventListener('click', resetSettings);227 generateBtn.addEventListener('click', generateGCode);228 downloadBtn.addEventListener('click', downloadGCode);229 230 // Functions231 function handleImageUpload(file) {232 if (!file.type.match('image.*')) {233 alert('Please select an image file (JPEG, PNG, etc.)');234 return;235 }236 237 const reader = new FileReader();238 reader.onload = function(e) {239 fabric.Image.fromURL(e.target.result, function(img) {240 // Clear canvas241 canvas.clear();242 243 // Scale image to fit canvas244 const scale = Math.min(1, canvas.width / img.width, canvas.height / img.height);245 img.scale(scale);246 247 // Center image248 canvas.setWidth(img.width * scale);249 canvas.setHeight(img.height * scale);250 canvas.centerObject(img);251 canvas.add(img);252 canvas.renderAll();253 254 currentImage = img;255 noImagePlaceholder.style.display = 'none';256 257 // Apply initial halftone effect258 applyHalftoneEffect();259 });260 };261 reader.readAsDataURL(file);262 }263 264 function updateSettings() {265 settings.radius = parseInt(radiusSlider.value);266 settings.contrast = parseInt(contrastSlider.value);267 settings.angle = parseInt(angleSlider.value);268 settings.density = parseInt(densitySlider.value);269 270 // Update display values271 radiusValue.textContent = `${settings.radius}px`;272 contrastValue.textContent = `${settings.contrast}%`;273 angleValue.textContent = `${settings.angle}°`;274 densityValue.textContent = `${settings.density}%`;275 276 if (currentImage) {277 applyHalftoneEffect();278 }279 }280 function applyHalftoneEffect() {281 if (!currentImage) return;282 283 // Create offscreen canvas for processing284 const offscreenCanvas = document.createElement('canvas');285 const offscreenCtx = offscreenCanvas.getContext('2d');286 offscreenCanvas.width = currentImage.width * currentImage.scaleX;287 offscreenCanvas.height = currentImage.height * currentImage.scaleY;288 289 // Draw current image to offscreen canvas290 offscreenCtx.drawImage(currentImage._element, 0, 0, offscreenCanvas.width, offscreenCanvas.height);291 292 // Get image data293 const imageData = offscreenCtx.getImageData(0, 0, offscreenCanvas.width, offscreenCanvas.height);294 const data = imageData.data;295 296 // Halftone effect parameters297 const radius = settings.radius;298 const contrast = settings.contrast / 100;299 const angle = (settings.angle * Math.PI) / 180;300 const density = settings.density / 100;301 302 // Clear and prepare canvas303 canvas.clear();304 const ctx = canvas.getContext('2d');305 ctx.fillStyle = 'white';306 ctx.fillRect(0, 0, canvas.width, canvas.height);307 308 // Halftone algorithm309 const dotSpacing = radius * 2;310 const cosAngle = Math.cos(angle);311 const sinAngle = Math.sin(angle);312 313 // Convert to grayscale first for better performance314 const grayscaleData = new Array(offscreenCanvas.width * offscreenCanvas.height);315 for (let i = 0; i < data.length; i += 4) {316 const idx = i / 4;317 const r = data[i];318 const g = data[i + 1];319 const b = data[i + 2];320 grayscaleData[idx] = 0.299 * r + 0.587 * g + 0.114 * b;321 }322 323 // Draw halftone pattern324 for (let y = 0; y < offscreenCanvas.height; y += dotSpacing) {325 for (let x = 0; x < offscreenCanvas.width; x += dotSpacing) {326 // Rotated grid position327 const rx = Math.round(x * cosAngle - y * sinAngle);328 const ry = Math.round(x * sinAngle + y * cosAngle);329 330 if (rx >= 0 && rx < offscreenCanvas.width && ry >= 0 && ry < offscreenCanvas.height) {331 // Get brightness at this position (inverted for dark dots)332 const idx = ry * offscreenCanvas.width + rx;333 const brightness = 1 - grayscaleData[idx] / 255;334 335 // Apply contrast336 let contrastBrightness = ((brightness - 0.5) * contrast) + 0.5;337 contrastBrightness = Math.max(0, Math.min(1, contrastBrightness));338 339 // Scale by density340 const scaledBrightness = contrastBrightness * density;341 342 // Draw dot (only if visible enough)343 if (scaledBrightness > 0.05) {344 const dotRadius = radius * scaledBrightness;345 ctx.beginPath();346 ctx.arc(x, y, dotRadius, 0, Math.PI * 2);347 ctx.fillStyle = 'black';348 ctx.fill();349 }350 }351 }352 }353 354 canvas.renderAll();355 }356function resetSettings() {357 radiusSlider.value = 5;358 contrastSlider.value = 50;359 angleSlider.value = 45;360 densitySlider.value = 50;361 updateSettings();362 }363 function generateGCode() {364 if (!currentImage) {365 alert('Please upload an image first');366 return;367 }368 369 // First apply the current halftone effect370 applyHalftoneEffect();371// In a real implementation, this would generate the actual G-code372 // For this demo, we'll create a sample G-code373 const sampleGCode = `; DotMatrix G-Code374; Generated from image with halftone settings:375; Radius: ${settings.radius}px376; Contrast: ${settings.contrast}%377; Angle: ${settings.angle}°378; Density: ${settings.density}%379 380G21 ; Set units to millimeters381G90 ; Absolute positioning382G28 ; Home all axes383 384; Start drawing halftone pattern385G1 F3000 ; Set feedrate386G1 Z5 ; Raise Z-axis387G1 X0 Y0 ; Move to start position388G1 Z0 ; Lower Z-axis389 390; Halftone pattern would be here391M2 ; End program`;392 393 gcodeOutput.value = sampleGCode;394 downloadBtn.disabled = false;395 }396 397 function downloadGCode() {398 const gcode = gcodeOutput.value;399 if (!gcode) return;400 401 const blob = new Blob([gcode], { type: 'text/plain' });402 const url = URL.createObjectURL(blob);403 const a = document.createElement('a');404 a.href = url;405 a.download = 'halftone.gcode';406 document.body.appendChild(a);407 a.click();408 document.body.removeChild(a);409 URL.revokeObjectURL(url);410 }411 412 // Initialize canvas size413 function resizeCanvas() {414 const container = document.getElementById('canvas-container');415 canvas.setWidth(container.clientWidth);416 canvas.setHeight(container.clientWidth * 0.66); // 3:2 aspect ratio417 if (currentImage) {418 canvas.renderAll();419 }420 }421 422 window.addEventListener('resize', resizeCanvas);423 resizeCanvas();424 });425 </script>426 <script>feather.replace();</script>427</body>428</html>429 