Add manifest.json, sidepanel components, and scripts. Include project assets and documentation files. Remove placeholder blank file.
113 lines
4.1 KiB
JavaScript
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;
|
|
}
|
|
}
|