diff --git a/docker/frontend/src/engine/superRes.ts b/docker/frontend/src/engine/superRes.ts index 72cc7af..2a79e2a 100644 --- a/docker/frontend/src/engine/superRes.ts +++ b/docker/frontend/src/engine/superRes.ts @@ -78,7 +78,9 @@ export async function upscaleJpeg( const srcCanvas = new OffscreenCanvas(w, h); const srcCtx = srcCanvas.getContext('2d', { willReadFrequently: true }); const dstCanvas = new OffscreenCanvas(Math.max(1, Math.round(w * scale)), Math.max(1, Math.round(h * scale))); - const dstCtx = dstCanvas.getContext('2d'); + // Opaque: a partly covered edge pixel would otherwise survive as transparency + // and the JPEG export flattens that onto black — a dark line down every seam. + const dstCtx = dstCanvas.getContext('2d', { alpha: false }); if (!srcCtx || !dstCtx) return bytes; srcCtx.drawImage(bitmap, 0, 0); const src = srcCtx.getImageData(0, 0, w, h); @@ -138,16 +140,22 @@ export async function upscaleJpeg( img.data[d + 3] = 255; } tileCtx.putImageData(img, 0, 0); + // Destination edges snapped to whole pixels: neighbouring tiles then share + // the exact same boundary, so nothing is left half-covered (and half-black) + // where they meet. `scale` is rarely an integer, so `x0 * scale` alone + // lands on a fraction and antialiases the seam. + const dx0 = Math.round(x0 * scale); + const dy0 = Math.round(y0 * scale); dstCtx.drawImage( tile, left * MODEL_SCALE, top * MODEL_SCALE, tw * MODEL_SCALE, th * MODEL_SCALE, - x0 * scale, - y0 * scale, - tw * scale, - th * scale + dx0, + dy0, + Math.round((x0 + tw) * scale) - dx0, + Math.round((y0 + th) * scale) - dy0 ); done++;