// Grid UI Logic document.addEventListener('DOMContentLoaded', () => { // Determine context const tabBtns = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); tabBtns.forEach(btn => { btn.addEventListener('click', () => { // Remove active class from all tabBtns.forEach(b => b.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); // Add active class to clicked button and target content btn.classList.add('active'); const targetId = `tab-${btn.dataset.tab}`; const targetContent = document.getElementById(targetId); if (targetContent) { targetContent.classList.add('active'); } }); }); initializeGridUI(); }); let gridImages = []; // Stores Image objects let currentTemplateId = 1; // Offsets for panning: map of slotIndex -> { x: float, y: float } (range -1.0 to 1.0) let gridOffsets = {}; function initializeGridUI() { const gridUploadInput = document.getElementById('gridImageInput'); const gridDropZone = document.getElementById('gridDropZone'); if (!gridUploadInput) return; // Not in tab mode or elements missing // File Upload Handlers gridDropZone.addEventListener('click', () => gridUploadInput.click()); gridUploadInput.addEventListener('change', (e) => handleGridFiles(e.target.files)); gridDropZone.addEventListener('dragover', (e) => { e.preventDefault(); gridDropZone.classList.add('drag-over'); }); gridDropZone.addEventListener('dragleave', () => gridDropZone.classList.remove('drag-over')); gridDropZone.addEventListener('drop', (e) => { e.preventDefault(); gridDropZone.classList.remove('drag-over'); handleGridFiles(e.dataTransfer.files); }); // Template Selection renderTemplateOptions(); // Generate Button document.getElementById('gridGenerateBtn').addEventListener('click', generateGrid); // Download Button document.getElementById('gridDownloadBtn').addEventListener('click', downloadGrid); // Fake Number Controls const fakeNumberToggle = document.getElementById('fakeNumberToggle'); const fakeNumberInput = document.getElementById('fakeNumberInput'); const fakeNumberContainer = document.getElementById('fakeNumberInputContainer'); fakeNumberToggle.addEventListener('change', (e) => { fakeNumberContainer.style.display = e.target.checked ? 'block' : 'none'; updateGridPreview(); }); fakeNumberInput.addEventListener('input', () => { updateGridPreview(); }); // Initialize Canvas Interaction initCanvasInteraction(); } function renderTemplateOptions() { const container = document.getElementById('gridTemplateList'); const templates = TemplateManager.getTemplates(); container.innerHTML = ''; templates.forEach(t => { const btn = document.createElement('button'); btn.className = 'template-btn'; if (t.id === currentTemplateId) btn.classList.add('active'); // Create Dynamic Preview const canvasW = t.canvasSize[0]; const canvasH = t.canvasSize[1]; let slotsHtml = ''; t.slots.forEach(slot => { const left = (slot.x / canvasW) * 100; const top = (slot.y / canvasH) * 100; const width = (slot.w / canvasW) * 100; const height = (slot.h / canvasH) * 100; slotsHtml += `
`; }); btn.innerHTML = `
${slotsHtml}
${t.expectedCount} ภาพ `; btn.onclick = () => { document.querySelectorAll('.template-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentTemplateId = t.id; gridOffsets = {}; updateGridPreview(); }; container.appendChild(btn); }); } function handleGridFiles(fileList) { if (!fileList.length) return; // Load images Array.from(fileList).forEach(file => { if (!file.type.startsWith('image/')) return; const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { gridImages.push(img); renderGridImagesList(); updateGridPreview(); }; img.src = e.target.result; }; reader.readAsDataURL(file); }); } // Drag & Drop Reordering State let draggedItemIndex = null; function renderGridImagesList() { const list = document.getElementById('gridImagesList'); list.innerHTML = ''; gridImages.forEach((img, index) => { const div = document.createElement('div'); div.className = 'grid-thumb-item'; div.draggable = true; // Enable drag // Drag Events div.addEventListener('dragstart', (e) => { draggedItemIndex = index; e.dataTransfer.effectAllowed = 'move'; div.style.opacity = '0.5'; }); div.addEventListener('dragend', () => { div.style.opacity = '1'; draggedItemIndex = null; document.querySelectorAll('.grid-thumb-item').forEach(item => item.classList.remove('drag-over-target')); }); div.addEventListener('dragover', (e) => { e.preventDefault(); // Allow drop e.dataTransfer.dropEffect = 'move'; if (draggedItemIndex !== index) { div.classList.add('drag-over-target'); } }); div.addEventListener('dragleave', () => { div.classList.remove('drag-over-target'); }); div.addEventListener('drop', (e) => { e.preventDefault(); if (draggedItemIndex !== null && draggedItemIndex !== index) { // Reorder array const movedItem = gridImages[draggedItemIndex]; gridImages.splice(draggedItemIndex, 1); gridImages.splice(index, 0, movedItem); // Also reorder offsets if we want to be fancy, but resetting is safer/simpler for now // or we could map them. Let's just reset offsets for simplicity as image moved slots gridOffsets = {}; renderGridImagesList(); updateGridPreview(); } }); const thumb = img.cloneNode(); div.appendChild(thumb); const removeBtn = document.createElement('button'); removeBtn.className = 'remove-btn'; removeBtn.innerHTML = '×'; removeBtn.onclick = (e) => { e.stopPropagation(); gridImages.splice(index, 1); gridOffsets = {}; renderGridImagesList(); updateGridPreview(); }; div.appendChild(removeBtn); list.appendChild(div); }); // Update count hint const template = TemplateManager.getTemplateById(currentTemplateId); const countSpan = document.getElementById('gridImageCount'); if (countSpan) countSpan.textContent = `${gridImages.length} / ${template.expectedCount}`; } function updateGridPreview() { const previewContainer = document.getElementById('gridCanvasPreview'); // If no images, clear the preview if (gridImages.length === 0) { previewContainer.innerHTML = ''; return; } const template = TemplateManager.getTemplateById(currentTemplateId); // Get fake number if enabled let fakeNumber = null; const fakeNumberToggle = document.getElementById('fakeNumberToggle'); if (fakeNumberToggle && fakeNumberToggle.checked) { fakeNumber = document.getElementById('fakeNumberInput').value || ""; } // Pass the offsets and fakeNumber const canvas = GridGenerator.generate(gridImages, template, gridOffsets, fakeNumber); previewContainer.innerHTML = ''; // Scale for display canvas.style.width = '100%'; canvas.style.height = 'auto'; // Add class for cursor interaction canvas.classList.add('interactive-canvas'); previewContainer.appendChild(canvas); } // Canvas Interaction (Panning) let isDragging = false; let startX, startY; let activeSlotIndex = -1; let initialOffset = { x: 0, y: 0 }; function initCanvasInteraction() { const container = document.getElementById('gridCanvasPreview'); container.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); // Document level for smooth drag outside document.addEventListener('mouseup', onMouseUp); } function onMouseDown(e) { if (e.target.tagName !== 'CANVAS') return; e.preventDefault(); const canvas = e.target; // Get mouse pos relative to canvas const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const mouseX = (e.clientX - rect.left) * scaleX; const mouseY = (e.clientY - rect.top) * scaleY; // Find clicked slot const template = TemplateManager.getTemplateById(currentTemplateId); const count = Math.min(gridImages.length, template.slots.length); activeSlotIndex = -1; for (let i = 0; i < count; i++) { const slot = template.slots[i]; // Simple hit test (ignoring borders for simplicity, or include them) if (mouseX >= slot.x && mouseX <= slot.x + slot.w && mouseY >= slot.y && mouseY <= slot.y + slot.h) { activeSlotIndex = i; break; } } if (activeSlotIndex !== -1) { isDragging = true; startX = e.clientX; startY = e.clientY; initialOffset = { ... (gridOffsets[activeSlotIndex] || { x: 0, y: 0 }) }; canvas.style.cursor = 'grabbing'; } } function onMouseMove(e) { if (!isDragging || activeSlotIndex === -1) return; e.preventDefault(); const canvas = document.querySelector('#gridCanvasPreview canvas'); if (!canvas) return; // Calculate Delta const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; // Convert delta to offset range (-1 to 1) manually? // This is tricky because the sensitivity depends on how much "extra" image there is. // If the image fits perfectly, maxOffset is 0, so panning does nothing (correct). // If we move 100px, how much logic offset is that? // We need to know the max pan capabilities of the current image in the current slot. // Ideally, we move pixel-for-pixel. // Let's retrieve image and slot info const template = TemplateManager.getTemplateById(currentTemplateId); const slot = template.slots[activeSlotIndex]; const img = gridImages[activeSlotIndex]; if (!img || !slot) return; const border = 5; // Hardcoded border width from generator const actualW = slot.w - (border * 2); const actualH = slot.h - (border * 2); const slotAspect = actualW / actualH; const imgAspect = img.naturalWidth / img.naturalHeight; let maxOffsetX_px = 0; let maxOffsetY_px = 0; // Calculate how the image is scaled in the slot // Logic duplicated from GridGenerator... ideally we refactor shared math but let's recompute. if (imgAspect > slotAspect) { // Image is wider - fit height // rendered height = actualH // rendered width = actualH * imgAspect const renderedW = actualH * imgAspect; maxOffsetX_px = (renderedW - actualW) / 2; } else { // Image is taller - fit width // rendered width = actualW // rendered height = actualW / imgAspect const renderedH = actualW / imgAspect; maxOffsetY_px = (renderedH - actualH) / 2; } // Map pixels to -1..1 range // delta 100px means we want to shift offset. // current_pixel_offset = initial_pixel_offset + delta // new_normalized = current_pixel_offset / max_offset_px // Since offset 1.0 = max_px, offset 0.0 = 0px // initial_pixel_val = initialOffset.x * maxOffsetX_px; // new_pixel_val = initial_pixel_val - deltaX (Mouse move right -> pan image right -> seeing left part? // Wait, usually panning: move mouse right = image moves right. // In our generator: // sourceX = center + (maxOffset * offset.x) // If offset.x is positive, sourceX increases -> we crop from the right -> image appears to move LEFT? // Let's verify: // Center = 500. MaxOffset = 100. // Offset 0 -> sourceX = 500. // Offset 1 -> sourceX = 600. Viewport shows 600+. Image shifted LEFT relative to frame. // So to move image RIGHT (mouse right), we need to DECREASE sourceX. // So we need to DECREASE offset. // X Axis let newOffsetX = initialOffset.x; if (maxOffsetX_px > 0) { // Sensitivity factor? Pixel to pixel mapping // To move image by deltaX pixels visually: // shift sourceX by -deltaX * (sourceScale / destScale) ? // Actually we are mapping screen pixels to canvas pixels. // Canvas is scaled via CSS. const rect = canvas.getBoundingClientRect(); const screenToCanvasRatio = canvas.width / rect.width; // We want the image to move with the mouse pointer exactly. // The image is drawn at 'actualW' size (destination). // If we move mouse 10px right, we want image to shift 10px right. // Drawing: ctx.drawImage(img, srcX, ...) // If we change srcX by -10, the image drawn shifts right by 10 (if scale is 1). // Calculate scale between Source Image and Destination Canvas Slot let scale = 1; if (imgAspect > slotAspect) { scale = img.naturalHeight / actualH; // Source / Dest } else { scale = img.naturalWidth / actualW; } const moveX_src = -deltaX * screenToCanvasRatio * scale; const moveY_src = -deltaY * screenToCanvasRatio * scale; // Convert src movement to offset change // offset = src_shift / max_offset if (maxOffsetX_px > 0) { // maxOffsetX_px is in Destination pixels... wait. // maxOffsetX defined above was: (renderedW - actualW) / 2 // renderedW IS destination size. // So maxOffsetX_px is in Desintation pixels (Canvas coords). const moveX_dest = deltaX * screenToCanvasRatio; // Move right = positive delta. // We want image to move right = shift left side in view = sourceX decreases. // offset factor = move_dest / max_offset_dest // BUT, verify direction again. // offset 1 => shift LEFT. // we want shift RIGHT => decrease offset. // So minus sign. newOffsetX = initialOffset.x - (moveX_dest / maxOffsetX_px); } } // Y Axis let newOffsetY = initialOffset.y; if (maxOffsetY_px > 0) { const moveY_dest = deltaY * (canvas.height / canvas.getBoundingClientRect().height); newOffsetY = initialOffset.y - (moveY_dest / maxOffsetY_px); } // Clamp newOffsetX = Math.max(-1, Math.min(1, newOffsetX)); newOffsetY = Math.max(-1, Math.min(1, newOffsetY)); // Update State gridOffsets[activeSlotIndex] = { x: newOffsetX, y: newOffsetY }; // Redraw (Throttle?) // For now direct redraw updateGridPreview(); } function onMouseUp() { isDragging = false; activeSlotIndex = -1; const canvas = document.querySelector('#gridCanvasPreview canvas'); if (canvas) canvas.style.cursor = 'default'; } function generateGrid() { updateGridPreview(); // Force refresh } function downloadGrid() { const previewContainer = document.getElementById('gridCanvasPreview'); const canvas = previewContainer.querySelector('canvas'); if (!canvas) return; const link = document.createElement('a'); link.download = `autogrid-${Date.now()}.png`; link.href = canvas.toDataURL('image/png'); link.click(); }