Files
gemini-extension/sidepanel/grid_ui.js
Kunthawat Greethong f490c63632 feat: add extension implementation and docs
Add manifest.json, sidepanel components, and scripts.
Include project assets and documentation files.
Remove placeholder blank file.
2026-01-06 08:49:28 +07:00

465 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 += `<div class="preview-slot" style="left: ${left}%; top: ${top}%; width: ${width}%; height: ${height}%;"></div>`;
});
btn.innerHTML = `
<div class="template-preview">
${slotsHtml}
</div>
<span>${t.expectedCount} ภาพ</span>
`;
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();
}