// 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} 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; } }