broadfield-dev/stack-overflow-game
0
1import { BLOCK_SIZE } from '../constants.js';2 3/**4 * Renders Tetris blocks using sprite sheet with color palettes from backdrops5 */6export default class SpriteBlockRenderer {7 /**8 * Create a block texture from sprite sheet with palette colors9 * @param {Phaser.Scene} scene - The Phaser scene10 * @param {number[]} colorPalette - Array of 7 colors extracted from backdrop11 * @param {number} level - Current level (1-10) determines which sprite to use12 * @param {string} key - Texture key to create13 * @param {number} colorIndex - Which color from palette to use (0-6)14 */15 static createBlockTexture(scene, colorPalette, level, key, colorIndex) {16 const canvas = document.createElement('canvas');17 const ctx = canvas.getContext('2d');18 ctx.imageSmoothingEnabled = false;19 canvas.width = BLOCK_SIZE;20 canvas.height = BLOCK_SIZE;21 22 // Get the color to use23 const color = colorPalette[colorIndex % colorPalette.length];24 const r = (color >> 16) & 0xFF;25 const g = (color >> 8) & 0xFF;26 const b = color & 0xFF;27 28 // Get the sprite sheet and extract pattern29 const spriteSheet = scene.textures.get('blocks-spritesheet').getSourceImage();30 const spriteX = (level - 1) * BLOCK_SIZE;31 32 const tempCanvas = document.createElement('canvas');33 const tempCtx = tempCanvas.getContext('2d');34 tempCtx.imageSmoothingEnabled = false;35 tempCanvas.width = spriteSheet.width;36 tempCanvas.height = spriteSheet.height;37 tempCtx.drawImage(spriteSheet, 0, 0);38 39 const spriteData = tempCtx.getImageData(spriteX, 0, BLOCK_SIZE, BLOCK_SIZE);40 const pixels = spriteData.data;41 42 // Create output image data43 const outputData = ctx.createImageData(BLOCK_SIZE, BLOCK_SIZE);44 const output = outputData.data;45 46 // Colorize: use grayscale brightness to modulate the base color47 // Grayscale values create depth (lighter/darker variations)48 for (let i = 0; i < pixels.length; i += 4) {49 const alpha = pixels[i + 3];50 51 if (alpha > 0) {52 // Get grayscale brightness (0-255)53 const brightness = pixels[i]; // R channel (grayscale, so R=G=B)54 55 // Normalize brightness to a multiplier (0.5 to 1.5)56 // 128 (50% gray) = 1.0x (base color)57 // 0 (black) = 0.5x (darkest)58 // 255 (white) = 1.5x (lightest)59 const multiplier = 0.5 + (brightness / 255) * 1.0;60 61 // Apply brightness multiplier to base color62 output[i] = Math.min(255, Math.floor(r * multiplier));63 output[i + 1] = Math.min(255, Math.floor(g * multiplier));64 output[i + 2] = Math.min(255, Math.floor(b * multiplier));65 output[i + 3] = 255;66 } else {67 // Transparent pixel68 output[i] = 0;69 output[i + 1] = 0;70 output[i + 2] = 0;71 output[i + 3] = 0;72 }73 }74 75 ctx.putImageData(outputData, 0, 0);76 77 // Create texture and set nearest neighbor78 const texture = scene.textures.addCanvas(key, canvas);79 texture.setFilter(Phaser.Textures.FilterMode.NEAREST);80 }81 82 /**83 * Create a crush animation frame texture84 * @param {Phaser.Scene} scene - The Phaser scene85 * @param {number} color - The color to apply86 * @param {number} frameIndex - Which frame (0-4)87 * @param {string} key - Texture key to create88 */89 static createCrushTexture(scene, color, frameIndex, key) {90 // Check if texture already exists91 if (scene.textures.exists(key)) {92 return;93 }94 95 const canvas = document.createElement('canvas');96 const ctx = canvas.getContext('2d');97 ctx.imageSmoothingEnabled = false;98 canvas.width = BLOCK_SIZE;99 canvas.height = BLOCK_SIZE;100 101 const r = (color >> 16) & 0xFF;102 const g = (color >> 8) & 0xFF;103 const b = color & 0xFF;104 105 // Get the crush sprite sheet106 const spriteSheet = scene.textures.get('crush-spritesheet').getSourceImage();107 const spriteX = frameIndex * BLOCK_SIZE;108 109 const tempCanvas = document.createElement('canvas');110 const tempCtx = tempCanvas.getContext('2d');111 tempCtx.imageSmoothingEnabled = false;112 tempCanvas.width = spriteSheet.width;113 tempCanvas.height = spriteSheet.height;114 tempCtx.drawImage(spriteSheet, 0, 0);115 116 const spriteData = tempCtx.getImageData(spriteX, 0, BLOCK_SIZE, BLOCK_SIZE);117 const pixels = spriteData.data;118 119 const outputData = ctx.createImageData(BLOCK_SIZE, BLOCK_SIZE);120 const output = outputData.data;121 122 // Apply grayscale brightness to color (darker = more visible, lighter/white = transparent)123 for (let i = 0; i < pixels.length; i += 4) {124 const brightness = pixels[i]; // Grayscale R channel125 const alpha = pixels[i + 3];126 127 // Light pixels or transparent become fully transparent128 if (brightness >= 200 || alpha === 0) {129 output[i] = 0;130 output[i + 1] = 0;131 output[i + 2] = 0;132 output[i + 3] = 0;133 } else {134 // Darker pixels get colored - use brightness to modulate color intensity135 // Darker sprite pixels = darker colored blocks136 const multiplier = 0.3 + (brightness / 255) * 0.9;137 output[i] = Math.min(255, Math.floor(r * multiplier));138 output[i + 1] = Math.min(255, Math.floor(g * multiplier));139 output[i + 2] = Math.min(255, Math.floor(b * multiplier));140 output[i + 3] = 255;141 }142 }143 144 ctx.putImageData(outputData, 0, 0);145 146 const texture = scene.textures.addCanvas(key, canvas);147 if (texture) {148 texture.setFilter(Phaser.Textures.FilterMode.NEAREST);149 }150 }151 152 /**153 * Subtly enhance colors with 20% extra contrast154 * @param {number[]} palette - Original palette from backdrop155 * @returns {number[]} Enhanced palette with subtle contrast boost156 */157 static enhancePalette(palette) {158 const enhanced = [];159 160 for (let i = 0; i < palette.length; i++) {161 let color = palette[i];162 let r = (color >> 16) & 0xFF;163 let g = (color >> 8) & 0xFF;164 let b = color & 0xFF;165 166 // Add 20% contrast: push values away from middle gray (128)167 const contrastFactor = 0.2;168 r = Math.min(255, Math.max(0, Math.floor(128 + (r - 128) * (1 + contrastFactor))));169 g = Math.min(255, Math.max(0, Math.floor(128 + (g - 128) * (1 + contrastFactor))));170 b = Math.min(255, Math.max(0, Math.floor(128 + (b - 128) * (1 + contrastFactor))));171 172 enhanced.push((r << 16) | (g << 8) | b);173 }174 175 return enhanced;176 }177 178 /**179 * Ensure colors in palette are distinct from each other180 * @param {number[]} palette - Color palette181 * @returns {number[]} Palette with distinct colors182 */183 static ensureDistinctColors(palette) {184 const result = [palette[0]];185 186 for (let i = 1; i < palette.length; i++) {187 let color = palette[i];188 let attempts = 0;189 190 // Check if too similar to existing colors191 while (attempts < 10) {192 let tooSimilar = false;193 194 for (let j = 0; j < result.length; j++) {195 if (this.colorDistance(color, result[j]) < 100) {196 tooSimilar = true;197 break;198 }199 }200 201 if (!tooSimilar) break;202 203 // Adjust color204 let r = (color >> 16) & 0xFF;205 let g = (color >> 8) & 0xFF;206 let b = color & 0xFF;207 208 r = (r + 60) % 256;209 g = (g + 40) % 256;210 b = (b + 80) % 256;211 212 color = (r << 16) | (g << 8) | b;213 attempts++;214 }215 216 result.push(color);217 }218 219 return result;220 }221 222 /**223 * Calculate color distance224 */225 static colorDistance(c1, c2) {226 const r1 = (c1 >> 16) & 0xFF;227 const g1 = (c1 >> 8) & 0xFF;228 const b1 = c1 & 0xFF;229 const r2 = (c2 >> 16) & 0xFF;230 const g2 = (c2 >> 8) & 0xFF;231 const b2 = c2 & 0xFF;232 233 return Math.sqrt((r1-r2)**2 + (g1-g2)**2 + (b1-b2)**2);234 }235}236 237 