Files
gemini-extension/scripts/grid_generator.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

113 lines
4.1 KiB
JavaScript

// Grid Generator logic
// Ported from python engine/grid_generator.py
class GridGenerator {
static BORDER_WIDTH = 5;
static BORDER_COLOR = '#FFFFFF';
/**
* Generates a grid image
* @param {Array<HTMLImageElement>} images - Array of loaded image elements
* @param {GridTemplate} template - The template to use
* @param {Object} offsets - Optional offsets map { slotIndex: {x, y} }
* @param {string} fakeNumber - Optional text to display on the last slot (e.g. "+5")
* @returns {HTMLCanvasElement} The canvas with the generated grid
*/
static generate(images, template, offsets = {}, fakeNumber = null) {
const canvas = document.createElement('canvas');
canvas.width = template.canvasSize[0];
canvas.height = template.canvasSize[1];
const ctx = canvas.getContext('2d');
// Fill background with border color
ctx.fillStyle = this.BORDER_COLOR;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const count = Math.min(images.length, template.slots.length);
for (let i = 0; i < count; i++) {
const slot = template.slots[i];
const img = images[i];
// Calculate the actual image size (subtract borders)
const border = this.BORDER_WIDTH;
const actualW = slot.w - (border * 2);
const actualH = slot.h - (border * 2);
const actualX = slot.x + border;
const actualY = slot.y + border;
// Get offset for this slot (default to center 0,0)
// Range [-1, 1]
const offset = offsets[i] || { x: 0, y: 0 };
// Calculate crop
const imgAspect = img.naturalWidth / img.naturalHeight;
const slotAspect = actualW / actualH;
let sourceX, sourceY, sourceW, sourceH;
if (imgAspect > slotAspect) {
// Image is wider - fit to height
sourceH = img.naturalHeight;
sourceW = sourceH * slotAspect;
const maxOffset = (img.naturalWidth - sourceW) / 2;
const panX = maxOffset * offset.x;
sourceX = (img.naturalWidth - sourceW) / 2 + panX;
sourceY = 0;
} else {
// Image is taller - fit to width
sourceW = img.naturalWidth;
sourceH = sourceW / slotAspect;
const maxOffset = (img.naturalHeight - sourceH) / 2;
const panY = maxOffset * offset.y;
sourceX = 0;
sourceY = (img.naturalHeight - sourceH) / 2 + panY;
}
// Draw image to canvas
ctx.drawImage(
img,
sourceX, sourceY, sourceW, sourceH, // Source
actualX, actualY, actualW, actualH // Destination
);
}
// Draw fake number in last slot if provided
if (template.slots.length > 0 && typeof fakeNumber === 'string' && fakeNumber.length > 0) {
const lastSlotIndex = template.slots.length - 1;
const lastSlot = template.slots[lastSlotIndex];
// Calculate slot dimensions (including border logic)
const border = this.BORDER_WIDTH;
const actualW = lastSlot.w - (border * 2);
const actualH = lastSlot.h - (border * 2);
const actualX = lastSlot.x + border;
const actualY = lastSlot.y + border;
// 1. Draw Semi-Transparent Overlay
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; // 50% opacity black
ctx.fillRect(actualX, actualY, actualW, actualH);
// 2. Draw Text centered
// Responsive font size: 20% of slot height
const fontSize = Math.floor(actualH * 0.2);
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Calculate center of slot
const centerX = actualX + (actualW / 2);
const centerY = actualY + (actualH / 2);
ctx.fillText(fakeNumber, centerX, centerY);
}
return canvas;
}
}