/* global buildPiecePath */ class PieceRenderer { /** * Render all puzzle pieces as Phaser textures via off-screen canvas clipping. * In Phaser 3.9, TextureManager emits 'addtexture' with the key as argument * (not 'addtexture-KEY' per-texture events that arrived in later versions). * * @param {Phaser.Scene} scene * @param {HTMLImageElement} sourceImage * @param {PieceData[]} pieceDataArray * @param {number} cols * @param {number} rows * @param {number} imageW * @param {number} imageH * @param {function(number, number)} onProgress * @returns {Promise<{ pieceW, pieceH, tabSize, canvasW, canvasH }>} */ static renderAll(scene, sourceImage, pieceDataArray, cols, rows, imageW, imageH, onProgress) { const pieceW = imageW / cols; const pieceH = imageH / rows; const tabSize = Math.max(pieceW, pieceH) * 0.32; const canvasW = Math.ceil(pieceW + 2 * tabSize); const canvasH = Math.ceil(pieceH + 2 * tabSize); const total = pieceDataArray.length; const dims = { pieceW, pieceH, tabSize, canvasW, canvasH }; // Build a set of expected keys so the listener ignores unrelated textures // Track both outlined (piece_N) and clean (piece_clean_N) textures const pending = new Set(); pieceDataArray.forEach(p => { pending.add(`piece_${p.id}`); pending.add(`piece_clean_${p.id}`); }); const totalTextures = pending.size; let texDone = 0; let done = 0; return new Promise(resolve => { const onAdded = (key) => { if (!pending.has(key)) return; pending.delete(key); texDone++; // Only count progress for main piece textures (not clean variants) if (key.startsWith('piece_') && !key.startsWith('piece_clean_')) { done++; if (onProgress) onProgress(done, total); } if (texDone === totalTextures) { scene.textures.off('addtexture', onAdded); resolve(dims); } }; scene.textures.on('addtexture', onAdded); // Kick off all canvas renders + addBase64 calls pieceDataArray.forEach(piece => { const canvas = document.createElement('canvas'); canvas.width = canvasW; canvas.height = canvasH; const ctx = canvas.getContext('2d'); // Clip path ctx.save(); buildPiecePath(ctx, piece.gridCol, piece.gridRow, cols, rows, pieceW, pieceH, piece.edges, tabSize); ctx.clip(); // Draw the source region that corresponds to the full canvas extent, // including the tab zones that overlap neighbouring cells. // srcX/srcY may be slightly negative for pieces on the top/left border; // the canvas spec clips out-of-bounds source regions automatically. ctx.drawImage( sourceImage, piece.gridCol * pieceW - tabSize, piece.gridRow * pieceH - tabSize, canvasW, canvasH, 0, 0, canvasW, canvasH ); ctx.restore(); // Subtle edge shading ctx.save(); buildPiecePath(ctx, piece.gridCol, piece.gridRow, cols, rows, pieceW, pieceH, piece.edges, tabSize); ctx.clip(); ctx.strokeStyle = 'rgba(0,0,0,0.35)'; ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); // Save clean version (used after pieces merge) const cleanKey = `piece_clean_${piece.id}`; if (scene.textures.exists(cleanKey)) scene.textures.remove(cleanKey); scene.textures.addBase64(cleanKey, canvas.toDataURL('image/png')); // Dark outer glow (drawn behind the stroke, outside the clip) ctx.save(); buildPiecePath(ctx, piece.gridCol, piece.gridRow, cols, rows, pieceW, pieceH, piece.edges, tabSize); ctx.shadowColor = 'rgba(0, 0, 0, 0.7)'; ctx.shadowBlur = 8; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; ctx.lineWidth = 2; ctx.stroke(); ctx.restore(); // Bright off-white stroke along the piece outline ctx.save(); buildPiecePath(ctx, piece.gridCol, piece.gridRow, cols, rows, pieceW, pieceH, piece.edges, tabSize); ctx.strokeStyle = 'rgba(240, 240, 255, 0.6)'; ctx.lineWidth = 1.5; ctx.stroke(); ctx.restore(); const key = `piece_${piece.id}`; if (scene.textures.exists(key)) scene.textures.remove(key); scene.textures.addBase64(key, canvas.toDataURL('image/png')); }); }); } }