Created
December 29, 2025 01:18
-
-
Save joshfedo/8e1d63026f3128a98546352ccc4050a4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Button Badge Maker - 58mm</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 600px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 20px; | |
| padding: 25px; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.3); | |
| } | |
| h1 { | |
| color: #333; | |
| margin-bottom: 10px; | |
| font-size: 24px; | |
| } | |
| .subtitle { | |
| color: #666; | |
| margin-bottom: 20px; | |
| font-size: 14px; | |
| } | |
| .upload-area { | |
| border: 3px dashed #667eea; | |
| border-radius: 15px; | |
| padding: 40px 20px; | |
| text-align: center; | |
| background: #f8f9ff; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| margin-bottom: 20px; | |
| } | |
| .upload-area:hover { | |
| background: #eef1ff; | |
| border-color: #764ba2; | |
| } | |
| .upload-area.dragover { | |
| background: #e0e7ff; | |
| border-color: #764ba2; | |
| } | |
| .upload-icon { | |
| font-size: 48px; | |
| margin-bottom: 10px; | |
| } | |
| input[type="file"] { | |
| display: none; | |
| } | |
| .upload-text { | |
| color: #667eea; | |
| font-weight: 600; | |
| margin-bottom: 5px; | |
| } | |
| .upload-hint { | |
| color: #999; | |
| font-size: 13px; | |
| } | |
| .thumbnail-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); | |
| gap: 15px; | |
| margin-bottom: 20px; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| padding: 15px; | |
| background: #f5f5f5; | |
| border-radius: 10px; | |
| } | |
| .thumbnail { | |
| position: relative; | |
| aspect-ratio: 1; | |
| cursor: pointer; | |
| } | |
| .thumbnail-image-wrapper { | |
| width: 100%; | |
| height: 100%; | |
| border-radius: 50%; | |
| overflow: hidden; | |
| border: 2px solid #ddd; | |
| } | |
| .thumbnail.selected .thumbnail-image-wrapper { | |
| border: 3px solid #667eea; | |
| box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.3); | |
| } | |
| .thumbnail img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .thumbnail .remove { | |
| position: absolute; | |
| top: -8px; | |
| right: -8px; | |
| background: #ff4757; | |
| color: white; | |
| border: none; | |
| border-radius: 50%; | |
| width: 28px; | |
| height: 28px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 10; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
| } | |
| .thumbnail .edit-badge { | |
| position: absolute; | |
| bottom: -8px; | |
| right: -8px; | |
| background: #667eea; | |
| color: white; | |
| border: none; | |
| border-radius: 50%; | |
| width: 28px; | |
| height: 28px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 10; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
| } | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 10; | |
| } | |
| .button-group { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| } | |
| button { | |
| flex: 1; | |
| padding: 15px; | |
| border: none; | |
| border-radius: 10px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| font-size: 16px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .btn-secondary { | |
| background: #f1f3f5; | |
| color: #333; | |
| } | |
| .btn-secondary:hover { | |
| background: #e9ecef; | |
| } | |
| .info-box { | |
| background: #fff3cd; | |
| border-left: 4px solid #ffc107; | |
| padding: 12px; | |
| border-radius: 5px; | |
| margin-bottom: 20px; | |
| font-size: 13px; | |
| color: #856404; | |
| } | |
| .print-canvas { | |
| display: none; | |
| } | |
| /* Modal Styles */ | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.9); | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .modal.active { | |
| display: flex; | |
| } | |
| .modal-content { | |
| background: white; | |
| border-radius: 20px; | |
| padding: 20px; | |
| width: 90%; | |
| max-width: 500px; | |
| max-height: 95vh; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 10px; | |
| } | |
| .modal-header h2 { | |
| font-size: 18px; | |
| color: #333; | |
| margin: 0; | |
| } | |
| .close-modal { | |
| background: #f1f3f5; | |
| border: none; | |
| border-radius: 50%; | |
| width: 36px; | |
| height: 36px; | |
| min-width: 36px; | |
| font-size: 24px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .edit-preview { | |
| position: relative; | |
| width: 280px; | |
| height: 280px; | |
| max-width: min(280px, 80vw); | |
| max-height: min(280px, 80vw); | |
| margin: 0 auto; | |
| border: 2px solid #ddd; | |
| border-radius: 50%; | |
| overflow: hidden; | |
| background: #f5f5f5; | |
| touch-action: none; | |
| flex-shrink: 0; | |
| } | |
| .edit-preview canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| cursor: move; | |
| } | |
| .edit-preview canvas.picker-mode { | |
| cursor: crosshair; | |
| } | |
| .controls { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .control-label { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: #333; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .control-buttons { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .control-btn { | |
| flex: 1; | |
| padding: 10px; | |
| border: 2px solid #667eea; | |
| background: white; | |
| color: #667eea; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .control-btn:active { | |
| background: #667eea; | |
| color: white; | |
| } | |
| .control-btn.active { | |
| background: #667eea; | |
| color: white; | |
| } | |
| input[type="range"] { | |
| width: 100%; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: #e9ecef; | |
| outline: none; | |
| -webkit-appearance: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #667eea; | |
| cursor: pointer; | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #667eea; | |
| cursor: pointer; | |
| border: none; | |
| } | |
| .modal-actions { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .btn-save { | |
| flex: 1; | |
| padding: 15px; | |
| border: none; | |
| border-radius: 10px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| font-weight: 600; | |
| cursor: pointer; | |
| font-size: 16px; | |
| } | |
| @media print { | |
| body { | |
| background: white; | |
| padding: 0; | |
| margin: 0; | |
| } | |
| .container { | |
| display: none !important; | |
| } | |
| .print-canvas { | |
| display: block !important; | |
| page-break-after: always; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🎯 58mm Button Badge Maker</h1> | |
| <p class="subtitle">Upload images and create a printable sheet</p> | |
| <div class="info-box"> | |
| 📏 <span id="sizeInfo">Each circle is 80mm total (58mm button + 11mm bleed). Standard letter paper fits 6 buttons.</span> | |
| </div> | |
| <div style="margin-bottom: 20px;"> | |
| <label style="display: block; margin-bottom: 8px; font-weight: 600; color: #333; font-size: 14px;">Button Size:</label> | |
| <select id="buttonSizeSelect" onchange="changeButtonSize()" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 10px; font-size: 14px; background: white; cursor: pointer;"> | |
| <option value="58">58mm Button (80mm total with bleed) - 6 per page</option> | |
| <option value="25">25mm Button (35mm total with bleed) - 35 per page</option> | |
| </select> | |
| </div> | |
| <div class="upload-area" id="uploadArea"> | |
| <div class="upload-icon">📸</div> | |
| <div class="upload-text">Tap to upload images</div> | |
| <div class="upload-hint">or drag and drop here</div> | |
| <input type="file" id="fileInput" accept="image/*" multiple> | |
| </div> | |
| <div id="thumbnailContainer" style="display: none;"> | |
| <h3 style="margin-bottom: 10px; color: #333; font-size: 16px;">Uploaded Images (<span id="imageCount">0</span>)</h3> | |
| <div class="thumbnail-grid" id="thumbnailGrid"></div> | |
| </div> | |
| <div class="button-group"> | |
| <button class="btn-secondary" id="clearBtn" onclick="clearAll()">Clear All</button> | |
| <button class="btn-primary" id="printBtn" onclick="generateAndPrint()">Generate & Print</button> | |
| </div> | |
| </div> | |
| <canvas id="printCanvas" class="print-canvas"></canvas> | |
| <!-- Edit Modal --> | |
| <div id="editModal" class="modal"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h2>✂️ Edit Image</h2> | |
| <button class="close-modal" onclick="closeEditModal()">×</button> | |
| </div> | |
| <div class="edit-preview" id="editPreview"> | |
| <canvas id="editCanvas"></canvas> | |
| </div> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <div class="control-label"> | |
| <span>🔍 Zoom</span> | |
| <span id="zoomValue">100%</span> | |
| </div> | |
| <input type="range" id="zoomSlider" min="25" max="300" value="100" step="1"> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label"> | |
| <span>🎨 Background</span> | |
| </div> | |
| <div class="control-buttons"> | |
| <button class="control-btn" onclick="setBackgroundMode('none')" id="bgNone">None</button> | |
| <button class="control-btn" onclick="setBackgroundMode('dominant')" id="bgDominant">Auto</button> | |
| <button class="control-btn" onclick="setBackgroundMode('picker')" id="bgPicker">Pick</button> | |
| <button class="control-btn" onclick="setBackgroundMode('blur')" id="bgBlur">Blur</button> | |
| </div> | |
| <div id="pickerHint" style="display: none; font-size: 12px; color: #667eea; margin-top: 5px; text-align: center;"> | |
| 👆 Tap on the image to pick a color | |
| </div> | |
| <div id="colorSwatch" style="display: none; margin-top: 8px; text-align: center;"> | |
| <div style="display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; background: #f5f5f5; border-radius: 8px;"> | |
| <div id="swatchColor" style="width: 24px; height: 24px; border-radius: 4px; border: 2px solid #ddd;"></div> | |
| <span id="swatchText" style="font-size: 12px; color: #666;">No color picked</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label"> | |
| <span>↕️ Position</span> | |
| </div> | |
| <div class="control-buttons"> | |
| <button class="control-btn" onclick="nudge(0, -10)">↑</button> | |
| <button class="control-btn" onclick="nudge(-10, 0)">←</button> | |
| <button class="control-btn" onclick="resetPosition()">⊙</button> | |
| <button class="control-btn" onclick="nudge(10, 0)">→</button> | |
| <button class="control-btn" onclick="nudge(0, 10)">↓</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn-save" onclick="saveEdit()">Save Changes</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let uploadedImages = []; | |
| let imageTransforms = []; // Store zoom/pan for each image | |
| let currentEditIndex = -1; | |
| let isDragging = false; | |
| let dragStart = { x: 0, y: 0 }; | |
| // Button size configuration | |
| let currentButtonSize = 58; // 58mm or 25mm | |
| let BUTTON_SIZE_MM = 58; | |
| let BLEED_MM = 11; | |
| let TOTAL_SIZE_MM = 80; // 58mm button + 11mm bleed on each side | |
| const MM_TO_PX = 3.7795275591; // 96 DPI | |
| let BUTTON_SIZE_PX = TOTAL_SIZE_MM * MM_TO_PX; | |
| // Letter size: 8.5 x 11 inches = 215.9 x 279.4 mm | |
| const PAGE_WIDTH_MM = 215.9; | |
| const PAGE_HEIGHT_MM = 279.4; | |
| const PAGE_WIDTH_PX = PAGE_WIDTH_MM * MM_TO_PX; | |
| const PAGE_HEIGHT_PX = PAGE_HEIGHT_MM * MM_TO_PX; | |
| function changeButtonSize() { | |
| const select = document.getElementById('buttonSizeSelect'); | |
| currentButtonSize = parseInt(select.value); | |
| if (currentButtonSize === 58) { | |
| BUTTON_SIZE_MM = 58; | |
| BLEED_MM = 11; | |
| TOTAL_SIZE_MM = 80; | |
| document.getElementById('sizeInfo').textContent = 'Each circle is 80mm total (58mm button + 11mm bleed). Standard letter paper fits 6 buttons.'; | |
| } else if (currentButtonSize === 25) { | |
| BUTTON_SIZE_MM = 25; | |
| BLEED_MM = 5; | |
| TOTAL_SIZE_MM = 35; | |
| document.getElementById('sizeInfo').textContent = 'Each circle is 35mm total (25mm button + 5mm bleed). Standard letter paper fits 35 buttons.'; | |
| } | |
| BUTTON_SIZE_PX = TOTAL_SIZE_MM * MM_TO_PX; | |
| } | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const thumbnailGrid = document.getElementById('thumbnailGrid'); | |
| const thumbnailContainer = document.getElementById('thumbnailContainer'); | |
| const imageCount = document.getElementById('imageCount'); | |
| const editModal = document.getElementById('editModal'); | |
| const editCanvas = document.getElementById('editCanvas'); | |
| const editPreview = document.getElementById('editPreview'); | |
| const zoomSlider = document.getElementById('zoomSlider'); | |
| const zoomValue = document.getElementById('zoomValue'); | |
| // Upload area click | |
| uploadArea.addEventListener('click', () => fileInput.click()); | |
| // File input change | |
| fileInput.addEventListener('change', handleFiles); | |
| // Drag and drop | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('dragover'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('dragover'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('dragover'); | |
| handleFiles({ target: { files: e.dataTransfer.files } }); | |
| }); | |
| // Zoom slider | |
| zoomSlider.addEventListener('input', (e) => { | |
| if (currentEditIndex >= 0) { | |
| imageTransforms[currentEditIndex].scale = parseInt(e.target.value) / 100; | |
| zoomValue.textContent = e.target.value + '%'; | |
| renderEditPreview(); | |
| } | |
| }); | |
| // Canvas interaction for panning and color picking | |
| editCanvas.addEventListener('mousedown', handleCanvasMouseDown); | |
| editCanvas.addEventListener('mousemove', drag); | |
| editCanvas.addEventListener('mouseup', endDrag); | |
| editCanvas.addEventListener('mouseleave', endDrag); | |
| // Touch events for mobile | |
| editCanvas.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| handleCanvasMouseDown({ clientX: touch.clientX, clientY: touch.clientY }); | |
| }); | |
| editCanvas.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| drag({ clientX: touch.clientX, clientY: touch.clientY }); | |
| }); | |
| editCanvas.addEventListener('touchend', endDrag); | |
| function handleCanvasMouseDown(e) { | |
| if (currentEditIndex < 0) return; | |
| // If in picker mode, pick color instead of dragging | |
| if (imageTransforms[currentEditIndex].bgMode === 'picker') { | |
| pickColorFromCanvas(e); | |
| return; | |
| } | |
| // Otherwise, start dragging | |
| startDrag(e); | |
| } | |
| function pickColorFromCanvas(e) { | |
| const rect = editCanvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| // Get pixel color from canvas | |
| const ctx = editCanvas.getContext('2d'); | |
| const pixel = ctx.getImageData(x, y, 1, 1).data; | |
| const color = `rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`; | |
| // Save picked color | |
| imageTransforms[currentEditIndex].pickedColor = color; | |
| // Update color swatch display | |
| updateColorSwatch(color); | |
| // Re-render with new background color | |
| renderEditPreview(); | |
| } | |
| function updateColorSwatch(color) { | |
| const colorSwatch = document.getElementById('colorSwatch'); | |
| const swatchColor = document.getElementById('swatchColor'); | |
| const swatchText = document.getElementById('swatchText'); | |
| colorSwatch.style.display = 'block'; | |
| if (color) { | |
| swatchColor.style.backgroundColor = color; | |
| swatchText.textContent = 'Color picked ✓'; | |
| } else { | |
| swatchColor.style.backgroundColor = '#f5f5f5'; | |
| swatchText.textContent = 'Tap to pick'; | |
| } | |
| } | |
| function startDrag(e) { | |
| if (currentEditIndex < 0) return; | |
| isDragging = true; | |
| dragStart.x = e.clientX - imageTransforms[currentEditIndex].offsetX; | |
| dragStart.y = e.clientY - imageTransforms[currentEditIndex].offsetY; | |
| } | |
| function drag(e) { | |
| if (!isDragging || currentEditIndex < 0) return; | |
| imageTransforms[currentEditIndex].offsetX = e.clientX - dragStart.x; | |
| imageTransforms[currentEditIndex].offsetY = e.clientY - dragStart.y; | |
| renderEditPreview(); | |
| } | |
| function endDrag() { | |
| isDragging = false; | |
| } | |
| function handleFiles(e) { | |
| const files = Array.from(e.target.files); | |
| files.forEach(file => { | |
| if (file.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| uploadedImages.push(img); | |
| imageTransforms.push({ | |
| scale: 1, | |
| offsetX: 0, | |
| offsetY: 0, | |
| bgMode: 'none', | |
| bgColor: null, | |
| pickedColor: null | |
| }); | |
| addThumbnail(img, uploadedImages.length - 1); | |
| updateUI(); | |
| }; | |
| img.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| } | |
| function addThumbnail(img, index) { | |
| const div = document.createElement('div'); | |
| div.className = 'thumbnail'; | |
| div.onclick = () => openEditModal(index); | |
| const imageWrapper = document.createElement('div'); | |
| imageWrapper.className = 'thumbnail-image-wrapper'; | |
| const imgEl = document.createElement('img'); | |
| imgEl.src = img.src; | |
| imageWrapper.appendChild(imgEl); | |
| const editBtn = document.createElement('button'); | |
| editBtn.className = 'edit-badge'; | |
| editBtn.innerHTML = '✏️'; | |
| editBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| openEditModal(index); | |
| }; | |
| const removeBtn = document.createElement('button'); | |
| removeBtn.className = 'remove'; | |
| removeBtn.innerHTML = '×'; | |
| removeBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| removeImage(index); | |
| }; | |
| div.appendChild(imageWrapper); | |
| div.appendChild(editBtn); | |
| div.appendChild(removeBtn); | |
| thumbnailGrid.appendChild(div); | |
| } | |
| function removeImage(index) { | |
| uploadedImages.splice(index, 1); | |
| imageTransforms.splice(index, 1); | |
| thumbnailGrid.innerHTML = ''; | |
| uploadedImages.forEach((img, i) => addThumbnail(img, i)); | |
| updateUI(); | |
| } | |
| function clearAll() { | |
| uploadedImages = []; | |
| imageTransforms = []; | |
| thumbnailGrid.innerHTML = ''; | |
| updateUI(); | |
| fileInput.value = ''; | |
| } | |
| function updateUI() { | |
| imageCount.textContent = uploadedImages.length; | |
| thumbnailContainer.style.display = uploadedImages.length > 0 ? 'block' : 'none'; | |
| } | |
| function openEditModal(index) { | |
| currentEditIndex = index; | |
| editModal.classList.add('active'); | |
| // Wait for modal to fully render with double RAF | |
| requestAnimationFrame(() => { | |
| requestAnimationFrame(() => { | |
| // Get actual rendered size | |
| const rect = editPreview.getBoundingClientRect(); | |
| const size = Math.min(rect.width, rect.height); | |
| // Set canvas to exact size | |
| editCanvas.width = size; | |
| editCanvas.height = size; | |
| // Update slider | |
| zoomSlider.value = imageTransforms[index].scale * 100; | |
| zoomValue.textContent = Math.round(imageTransforms[index].scale * 100) + '%'; | |
| // Extract dominant color if not already done | |
| if (!imageTransforms[index].bgColor) { | |
| imageTransforms[index].bgColor = extractDominantColor(uploadedImages[index]); | |
| } | |
| // Update background mode buttons | |
| updateBackgroundButtons(); | |
| // Show color swatch if color was picked | |
| if (imageTransforms[index].pickedColor) { | |
| updateColorSwatch(imageTransforms[index].pickedColor); | |
| } else { | |
| updateColorSwatch(null); | |
| } | |
| renderEditPreview(); | |
| }); | |
| }); | |
| } | |
| function extractDominantColor(img) { | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = 50; | |
| canvas.height = 50; | |
| ctx.drawImage(img, 0, 0, 50, 50); | |
| const imageData = ctx.getImageData(0, 0, 50, 50).data; | |
| let r = 0, g = 0, b = 0; | |
| for (let i = 0; i < imageData.length; i += 4) { | |
| r += imageData[i]; | |
| g += imageData[i + 1]; | |
| b += imageData[i + 2]; | |
| } | |
| const pixelCount = imageData.length / 4; | |
| r = Math.round(r / pixelCount); | |
| g = Math.round(g / pixelCount); | |
| b = Math.round(b / pixelCount); | |
| return `rgb(${r}, ${g}, ${b})`; | |
| } | |
| function setBackgroundMode(mode) { | |
| if (currentEditIndex < 0) return; | |
| imageTransforms[currentEditIndex].bgMode = mode; | |
| updateBackgroundButtons(); | |
| // Show/hide picker hint | |
| const pickerHint = document.getElementById('pickerHint'); | |
| pickerHint.style.display = mode === 'picker' ? 'block' : 'none'; | |
| // Show/hide color swatch | |
| if (mode === 'picker') { | |
| updateColorSwatch(imageTransforms[currentEditIndex].pickedColor); | |
| } else { | |
| document.getElementById('colorSwatch').style.display = 'none'; | |
| } | |
| // Change cursor style | |
| editCanvas.style.cursor = mode === 'picker' ? 'crosshair' : 'move'; | |
| renderEditPreview(); | |
| } | |
| function updateBackgroundButtons() { | |
| if (currentEditIndex < 0) return; | |
| const mode = imageTransforms[currentEditIndex].bgMode; | |
| document.getElementById('bgNone').classList.toggle('active', mode === 'none'); | |
| document.getElementById('bgDominant').classList.toggle('active', mode === 'dominant'); | |
| document.getElementById('bgPicker').classList.toggle('active', mode === 'picker'); | |
| document.getElementById('bgBlur').classList.toggle('active', mode === 'blur'); | |
| // Update cursor and hint | |
| const pickerHint = document.getElementById('pickerHint'); | |
| if (pickerHint) { | |
| pickerHint.style.display = mode === 'picker' ? 'block' : 'none'; | |
| } | |
| editCanvas.style.cursor = mode === 'picker' ? 'crosshair' : 'move'; | |
| } | |
| function closeEditModal() { | |
| editModal.classList.remove('active'); | |
| currentEditIndex = -1; | |
| // Update thumbnail | |
| thumbnailGrid.innerHTML = ''; | |
| uploadedImages.forEach((img, i) => addThumbnail(img, i)); | |
| } | |
| function renderEditPreview() { | |
| if (currentEditIndex < 0) return; | |
| const ctx = editCanvas.getContext('2d'); | |
| const img = uploadedImages[currentEditIndex]; | |
| const transform = imageTransforms[currentEditIndex]; | |
| const size = editCanvas.width; | |
| // Clear | |
| ctx.clearRect(0, 0, size, size); | |
| // Draw circular mask | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.arc(size/2, size/2, size/2, 0, Math.PI * 2); | |
| ctx.closePath(); | |
| ctx.clip(); | |
| // Draw background based on mode | |
| if (transform.bgMode === 'dominant') { | |
| ctx.fillStyle = transform.bgColor; | |
| ctx.fillRect(0, 0, size, size); | |
| } else if (transform.bgMode === 'picker') { | |
| // Use picked color or fallback to dominant color | |
| ctx.fillStyle = transform.pickedColor || transform.bgColor || '#f5f5f5'; | |
| ctx.fillRect(0, 0, size, size); | |
| } else if (transform.bgMode === 'blur') { | |
| // Draw blurred background | |
| ctx.filter = 'blur(20px)'; | |
| const bgScale = Math.max(size / img.width, size / img.height) * 1.2; | |
| const bgWidth = img.width * bgScale; | |
| const bgHeight = img.height * bgScale; | |
| const bgX = (size - bgWidth) / 2; | |
| const bgY = (size - bgHeight) / 2; | |
| ctx.drawImage(img, bgX, bgY, bgWidth, bgHeight); | |
| ctx.filter = 'none'; | |
| } else { | |
| // Fill with white or transparent | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.fillRect(0, 0, size, size); | |
| } | |
| // Calculate scaled dimensions | |
| const scale = transform.scale * Math.max(size / img.width, size / img.height); | |
| const scaledWidth = img.width * scale; | |
| const scaledHeight = img.height * scale; | |
| // Draw image with offset | |
| const x = (size - scaledWidth) / 2 + transform.offsetX; | |
| const y = (size - scaledHeight) / 2 + transform.offsetY; | |
| ctx.drawImage(img, x, y, scaledWidth, scaledHeight); | |
| ctx.restore(); | |
| // Draw border | |
| ctx.strokeStyle = '#667eea'; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); | |
| ctx.arc(size/2, size/2, size/2, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| // Draw safe zone | |
| const safeZoneRatio = BUTTON_SIZE_MM / TOTAL_SIZE_MM; | |
| ctx.strokeStyle = '#ff9999'; | |
| ctx.lineWidth = 1; | |
| ctx.setLineDash([5, 5]); | |
| ctx.beginPath(); | |
| ctx.arc(size/2, size/2, size/2 * safeZoneRatio, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| } | |
| function nudge(dx, dy) { | |
| if (currentEditIndex < 0) return; | |
| imageTransforms[currentEditIndex].offsetX += dx; | |
| imageTransforms[currentEditIndex].offsetY += dy; | |
| renderEditPreview(); | |
| } | |
| function resetPosition() { | |
| if (currentEditIndex < 0) return; | |
| imageTransforms[currentEditIndex].offsetX = 0; | |
| imageTransforms[currentEditIndex].offsetY = 0; | |
| renderEditPreview(); | |
| } | |
| function saveEdit() { | |
| closeEditModal(); | |
| } | |
| function generateAndPrint() { | |
| if (uploadedImages.length === 0) { | |
| alert('Please upload at least one image first!'); | |
| return; | |
| } | |
| const canvas = document.getElementById('printCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Set canvas to letter size | |
| canvas.width = PAGE_WIDTH_PX; | |
| canvas.height = PAGE_HEIGHT_PX; | |
| // White background | |
| ctx.fillStyle = 'white'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Calculate grid layout based on button size | |
| let cols, rows, maxButtons; | |
| if (currentButtonSize === 58) { | |
| // 80mm buttons: 2 columns x 3 rows = 6 buttons | |
| cols = 2; | |
| rows = 3; | |
| maxButtons = 6; | |
| } else { | |
| // 35mm buttons: 5 columns x 7 rows = 35 buttons | |
| cols = 5; | |
| rows = 7; | |
| maxButtons = 35; | |
| } | |
| const marginX = (PAGE_WIDTH_PX - (BUTTON_SIZE_PX * cols)) / (cols + 1); | |
| const marginY = (PAGE_HEIGHT_PX - (BUTTON_SIZE_PX * rows)) / (rows + 1); | |
| uploadedImages.forEach((img, index) => { | |
| if (index >= maxButtons) return; // Max buttons per page based on size | |
| const col = index % cols; | |
| const row = Math.floor(index / cols); | |
| const x = marginX + (col * (BUTTON_SIZE_PX + marginX)); | |
| const y = marginY + (row * (BUTTON_SIZE_PX + marginY)); | |
| // Draw circle with image | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.arc(x + BUTTON_SIZE_PX/2, y + BUTTON_SIZE_PX/2, BUTTON_SIZE_PX/2, 0, Math.PI * 2); | |
| ctx.closePath(); | |
| ctx.clip(); | |
| // Get transform for this image | |
| const transform = imageTransforms[index]; | |
| // Draw background based on mode | |
| if (transform.bgMode === 'dominant') { | |
| ctx.fillStyle = transform.bgColor; | |
| ctx.fillRect(x, y, BUTTON_SIZE_PX, BUTTON_SIZE_PX); | |
| } else if (transform.bgMode === 'picker') { | |
| // Use picked color or fallback | |
| ctx.fillStyle = transform.pickedColor || transform.bgColor || '#ffffff'; | |
| ctx.fillRect(x, y, BUTTON_SIZE_PX, BUTTON_SIZE_PX); | |
| } else if (transform.bgMode === 'blur') { | |
| // Draw blurred background | |
| ctx.filter = 'blur(40px)'; | |
| const bgScale = Math.max(BUTTON_SIZE_PX / img.width, BUTTON_SIZE_PX / img.height) * 1.2; | |
| const bgWidth = img.width * bgScale; | |
| const bgHeight = img.height * bgScale; | |
| const bgX = x + (BUTTON_SIZE_PX - bgWidth) / 2; | |
| const bgY = y + (BUTTON_SIZE_PX - bgHeight) / 2; | |
| ctx.drawImage(img, bgX, bgY, bgWidth, bgHeight); | |
| ctx.filter = 'none'; | |
| } else { | |
| // Fill with white | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.fillRect(x, y, BUTTON_SIZE_PX, BUTTON_SIZE_PX); | |
| } | |
| // Calculate dimensions with zoom | |
| const baseScale = Math.max(BUTTON_SIZE_PX / img.width, BUTTON_SIZE_PX / img.height); | |
| const scale = baseScale * transform.scale; | |
| const scaledWidth = img.width * scale; | |
| const scaledHeight = img.height * scale; | |
| // Apply offset (scaled to print size) | |
| const scaleFactor = BUTTON_SIZE_PX / editCanvas.width; | |
| const offsetX = x + (BUTTON_SIZE_PX - scaledWidth) / 2 + (transform.offsetX * scaleFactor); | |
| const offsetY = y + (BUTTON_SIZE_PX - scaledHeight) / 2 + (transform.offsetY * scaleFactor); | |
| ctx.drawImage(img, offsetX, offsetY, scaledWidth, scaledHeight); | |
| ctx.restore(); | |
| // Draw light outline for cutting guide (especially helpful for white buttons) | |
| ctx.strokeStyle = '#cccccc'; | |
| ctx.lineWidth = 0.5; | |
| ctx.beginPath(); | |
| ctx.arc(x + BUTTON_SIZE_PX/2, y + BUTTON_SIZE_PX/2, BUTTON_SIZE_PX/2, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| }); | |
| // Trigger print | |
| window.print(); | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment