web: the upscale drops its tile seams

A tile's destination rectangle was placed at x0 * scale, and that scale
is rarely whole, so every 256px boundary landed on a fraction of a pixel.
The edge was drawn half covered, stayed transparent, and the JPEG export
flattened that transparency onto black: a dark line down each seam.

Snap both destination edges to whole pixels instead, so neighbouring
tiles share the exact same boundary, and make the destination context
opaque so no partly covered pixel can survive as transparency again.

Measured on a 640px source: the seam at 2K was 46 levels darker than its
neighbours (96 at 4K); it is now within one level of them.
This commit is contained in:
2026-09-22 09:56:12 +07:00
parent c0c99a9672
commit 2f78216b6f
+13 -5
View File
@@ -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++;