iPuzzle/js/puzzle/PieceRenderer.js

125 lines
4.5 KiB
JavaScript

/* 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'));
});
});
}
}