import { BLOCK_SIZE } from '../constants.js'; /** * Renders Tetris blocks using sprite sheet with color palettes from backdrops */ export default class SpriteBlockRenderer { /** * Create a block texture from sprite sheet with palette colors * @param {Phaser.Scene} scene - The Phaser scene * @param {number[]} colorPalette - Array of 7 colors extracted from backdrop * @param {number} level - Current level (1-10) determines which sprite to use * @param {string} key - Texture key to create * @param {number} colorIndex - Which color from palette to use (0-6) */ static createBlockTexture(scene, colorPalette, level, key, colorIndex) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = false; canvas.width = BLOCK_SIZE; canvas.height = BLOCK_SIZE; // Get the color to use const color = colorPalette[colorIndex % colorPalette.length]; const r = (color >> 16) & 0xFF; const g = (color >> 8) & 0xFF; const b = color & 0xFF; // Get the sprite sheet and extract pattern const spriteSheet = scene.textures.get('blocks-spritesheet').getSourceImage(); const spriteX = (level - 1) * BLOCK_SIZE; const tempCanvas = document.createElement('canvas'); const tempCtx = tempCanvas.getContext('2d'); tempCtx.imageSmoothingEnabled = false; tempCanvas.width = spriteSheet.width; tempCanvas.height = spriteSheet.height; tempCtx.drawImage(spriteSheet, 0, 0); const spriteData = tempCtx.getImageData(spriteX, 0, BLOCK_SIZE, BLOCK_SIZE); const pixels = spriteData.data; // Create output image data const outputData = ctx.createImageData(BLOCK_SIZE, BLOCK_SIZE); const output = outputData.data; // Colorize: use grayscale brightness to modulate the base color // Grayscale values create depth (lighter/darker variations) for (let i = 0; i < pixels.length; i += 4) { const alpha = pixels[i + 3]; if (alpha > 0) { // Get grayscale brightness (0-255) const brightness = pixels[i]; // R channel (grayscale, so R=G=B) // Normalize brightness to a multiplier (0.5 to 1.5) // 128 (50% gray) = 1.0x (base color) // 0 (black) = 0.5x (darkest) // 255 (white) = 1.5x (lightest) const multiplier = 0.5 + (brightness / 255) * 1.0; // Apply brightness multiplier to base color output[i] = Math.min(255, Math.floor(r * multiplier)); output[i + 1] = Math.min(255, Math.floor(g * multiplier)); output[i + 2] = Math.min(255, Math.floor(b * multiplier)); output[i + 3] = 255; } else { // Transparent pixel output[i] = 0; output[i + 1] = 0; output[i + 2] = 0; output[i + 3] = 0; } } ctx.putImageData(outputData, 0, 0); // Create texture and set nearest neighbor const texture = scene.textures.addCanvas(key, canvas); texture.setFilter(Phaser.Textures.FilterMode.NEAREST); } /** * Create a crush animation frame texture * @param {Phaser.Scene} scene - The Phaser scene * @param {number} color - The color to apply * @param {number} frameIndex - Which frame (0-4) * @param {string} key - Texture key to create */ static createCrushTexture(scene, color, frameIndex, key) { // Check if texture already exists if (scene.textures.exists(key)) { return; } const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = false; canvas.width = BLOCK_SIZE; canvas.height = BLOCK_SIZE; const r = (color >> 16) & 0xFF; const g = (color >> 8) & 0xFF; const b = color & 0xFF; // Get the crush sprite sheet const spriteSheet = scene.textures.get('crush-spritesheet').getSourceImage(); const spriteX = frameIndex * BLOCK_SIZE; const tempCanvas = document.createElement('canvas'); const tempCtx = tempCanvas.getContext('2d'); tempCtx.imageSmoothingEnabled = false; tempCanvas.width = spriteSheet.width; tempCanvas.height = spriteSheet.height; tempCtx.drawImage(spriteSheet, 0, 0); const spriteData = tempCtx.getImageData(spriteX, 0, BLOCK_SIZE, BLOCK_SIZE); const pixels = spriteData.data; const outputData = ctx.createImageData(BLOCK_SIZE, BLOCK_SIZE); const output = outputData.data; // Apply grayscale brightness to color (darker = more visible, lighter/white = transparent) for (let i = 0; i < pixels.length; i += 4) { const brightness = pixels[i]; // Grayscale R channel const alpha = pixels[i + 3]; // Light pixels or transparent become fully transparent if (brightness >= 200 || alpha === 0) { output[i] = 0; output[i + 1] = 0; output[i + 2] = 0; output[i + 3] = 0; } else { // Darker pixels get colored - use brightness to modulate color intensity // Darker sprite pixels = darker colored blocks const multiplier = 0.3 + (brightness / 255) * 0.9; output[i] = Math.min(255, Math.floor(r * multiplier)); output[i + 1] = Math.min(255, Math.floor(g * multiplier)); output[i + 2] = Math.min(255, Math.floor(b * multiplier)); output[i + 3] = 255; } } ctx.putImageData(outputData, 0, 0); const texture = scene.textures.addCanvas(key, canvas); if (texture) { texture.setFilter(Phaser.Textures.FilterMode.NEAREST); } } /** * Subtly enhance colors with 20% extra contrast * @param {number[]} palette - Original palette from backdrop * @returns {number[]} Enhanced palette with subtle contrast boost */ static enhancePalette(palette) { const enhanced = []; for (let i = 0; i < palette.length; i++) { let color = palette[i]; let r = (color >> 16) & 0xFF; let g = (color >> 8) & 0xFF; let b = color & 0xFF; // Add 20% contrast: push values away from middle gray (128) const contrastFactor = 0.2; r = Math.min(255, Math.max(0, Math.floor(128 + (r - 128) * (1 + contrastFactor)))); g = Math.min(255, Math.max(0, Math.floor(128 + (g - 128) * (1 + contrastFactor)))); b = Math.min(255, Math.max(0, Math.floor(128 + (b - 128) * (1 + contrastFactor)))); enhanced.push((r << 16) | (g << 8) | b); } return enhanced; } /** * Ensure colors in palette are distinct from each other * @param {number[]} palette - Color palette * @returns {number[]} Palette with distinct colors */ static ensureDistinctColors(palette) { const result = [palette[0]]; for (let i = 1; i < palette.length; i++) { let color = palette[i]; let attempts = 0; // Check if too similar to existing colors while (attempts < 10) { let tooSimilar = false; for (let j = 0; j < result.length; j++) { if (this.colorDistance(color, result[j]) < 100) { tooSimilar = true; break; } } if (!tooSimilar) break; // Adjust color let r = (color >> 16) & 0xFF; let g = (color >> 8) & 0xFF; let b = color & 0xFF; r = (r + 60) % 256; g = (g + 40) % 256; b = (b + 80) % 256; color = (r << 16) | (g << 8) | b; attempts++; } result.push(color); } return result; } /** * Calculate color distance */ static colorDistance(c1, c2) { const r1 = (c1 >> 16) & 0xFF; const g1 = (c1 >> 8) & 0xFF; const b1 = c1 & 0xFF; const r2 = (c2 >> 16) & 0xFF; const g2 = (c2 >> 8) & 0xFF; const b2 = c2 & 0xFF; return Math.sqrt((r1-r2)**2 + (g1-g2)**2 + (b1-b2)**2); } }