Skip to content

Instantly share code, notes, and snippets.

@joshfedo
Created December 29, 2025 01:18
Show Gist options
  • Select an option

  • Save joshfedo/8e1d63026f3128a98546352ccc4050a4 to your computer and use it in GitHub Desktop.

Select an option

Save joshfedo/8e1d63026f3128a98546352ccc4050a4 to your computer and use it in GitHub Desktop.
<!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