From 56f5239773b6f0144c7ca34bfe5e0e9c2e60d74b Mon Sep 17 00:00:00 2001 From: Prakash Meena Date: Sat, 12 Sep 2026 03:24:35 +0530 Subject: [PATCH 1/2] fix(image): account for destination pixel density in p5.Image copy and blend --- src/image/p5.Image.js | 9 +++--- test/unit/image/p5.Image.js | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 4823ad8bf4..9cbc1c48a5 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -918,6 +918,7 @@ class Image { _copyHelper(dstImage, srcImage, sx, sy, sw, sh, dx, dy, dw, dh) { const s = srcImage.canvas.width / srcImage.width; + const d = dstImage.canvas.width / dstImage.width; // adjust coord system for 3D when renderer // ie top-left = -width/2, -height/2 let sxMod = 0; @@ -951,10 +952,10 @@ class Image { s * (sy + syMod), s * sw, s * sh, - dx, - dy, - dw, - dh + d * dx, + d * dy, + d * dw, + d * dh ); } } diff --git a/test/unit/image/p5.Image.js b/test/unit/image/p5.Image.js index 9962a2dfe6..edc3a3e44d 100644 --- a/test/unit/image/p5.Image.js +++ b/test/unit/image/p5.Image.js @@ -51,6 +51,68 @@ suite('p5.Image', function () { }); }); + suite('p5.Image.prototype.copy', function () { + test('it copies correctly to destination with pixel density > 1', function () { + let src = myp5.createImage(50, 50); + src.loadPixels(); + for (let i = 0; i < src.pixels.length; i += 4) { + src.pixels[i] = 255; + src.pixels[i + 3] = 255; + } + src.updatePixels(); + + let dst = myp5.createImage(100, 100); + dst.pixelDensity(2); + // dst.width is 50, dst.height is 50, dst.canvas is 100x100 + dst.copy(src, 0, 0, 50, 50, 0, 0, 50, 50); + + // (35, 35) maps to physical (70, 70), which without the fix was outside + // the unscaled 50x50 copy region on the 100x100 canvas. + let col = dst.get(35, 35); + assert.strictEqual(col[0], 255, 'red channel at (35, 35)'); + assert.strictEqual(col[3], 255, 'alpha channel at (35, 35)'); + }); + + test('it copies correctly when both source and destination have pixel density > 1', function () { + let src = myp5.createImage(50, 50); + src.pixelDensity(2); + src.loadPixels(); + for (let i = 0; i < src.pixels.length; i += 4) { + src.pixels[i] = 255; + src.pixels[i + 3] = 255; + } + src.updatePixels(); + + let dst = myp5.createImage(100, 100); + dst.pixelDensity(2); + dst.copy(src, 0, 0, 25, 25, 0, 0, 50, 50); + + let col = dst.get(35, 35); + assert.strictEqual(col[0], 255, 'red channel at (35, 35)'); + assert.strictEqual(col[3], 255, 'alpha channel at (35, 35)'); + }); + }); + + suite('p5.Image.prototype.blend', function () { + test('it blends correctly to destination with pixel density > 1', function () { + let src = myp5.createImage(50, 50); + src.loadPixels(); + for (let i = 0; i < src.pixels.length; i += 4) { + src.pixels[i] = 255; + src.pixels[i + 3] = 255; + } + src.updatePixels(); + + let dst = myp5.createImage(100, 100); + dst.pixelDensity(2); + dst.blend(src, 0, 0, 50, 50, 0, 0, 50, 50, myp5.BLEND); + + let col = dst.get(35, 35); + assert.strictEqual(col[0], 255, 'red channel at (35, 35)'); + assert.strictEqual(col[3], 255, 'alpha channel at (35, 35)'); + }); + }); + suite.todo('p5.Image.prototype.mask', function () { for (const density of [1, 2]) { test(`it should mask the image at pixel density ${density}`, function () { From 53d30063159a2120a53e030b05b70c1aa2bcefe3 Mon Sep 17 00:00:00 2001 From: Prakash Meena Date: Tue, 15 Sep 2026 12:01:24 +0530 Subject: [PATCH 2/2] fix: prevent double-scaling in mask() with high-DPI pixel density mask() was pre-multiplying destination coords by imgScaleFactor before passing them to copy(). Since _copyHelper() now scales destination coords by the destination pixel density internally, this caused double-scaling when pixelDensity > 1. Fix: pass logical (unscaled) this.width / this.height from mask() and let _copyHelper() handle the scaling. Added unit test verifying mask() works correctly at pixelDensity 2. --- src/image/p5.Image.js | 4 ++-- test/unit/image/p5.Image.js | 42 +++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 9cbc1c48a5..455deec699 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -1010,8 +1010,8 @@ class Image { maskScaleFactor * p5Image.height, 0, 0, - imgScaleFactor * this.width, - imgScaleFactor * this.height + this.width, + this.height ]; this.drawingContext.globalCompositeOperation = 'destination-in'; diff --git a/test/unit/image/p5.Image.js b/test/unit/image/p5.Image.js index edc3a3e44d..4f92d856d8 100644 --- a/test/unit/image/p5.Image.js +++ b/test/unit/image/p5.Image.js @@ -113,6 +113,48 @@ suite('p5.Image', function () { }); }); + suite('p5.Image.prototype.mask (high-DPI)', function () { + test('mask does not double-scale destination when pixelDensity > 1', function () { + // Create a 50x50 image with pixelDensity 2 (canvas is 100x100), + // filled solid white fully opaque. + let img = myp5.createImage(100, 100); + img.pixelDensity(2); + img.loadPixels(); + for (let i = 0; i < img.pixels.length; i += 4) { + img.pixels[i] = 255; + img.pixels[i + 1] = 255; + img.pixels[i + 2] = 255; + img.pixels[i + 3] = 255; + } + img.updatePixels(); + + // Create a fully opaque mask of the same logical size. + let maskImg = myp5.createImage(100, 100); + maskImg.pixelDensity(2); + maskImg.loadPixels(); + for (let i = 0; i < maskImg.pixels.length; i += 4) { + maskImg.pixels[i] = 255; + maskImg.pixels[i + 1] = 255; + maskImg.pixels[i + 2] = 255; + maskImg.pixels[i + 3] = 255; + } + maskImg.updatePixels(); + + img.mask(maskImg); + + // Without the fix, double-scaling would cause the mask to draw at + // 2x the canvas size, leaving parts of the original image unmasked + // or producing incorrect results. Check a pixel in the bottom-right + // quadrant that would be affected by double-scaling. + let col = img.get(35, 35); + assert.strictEqual(col[3], 255, 'alpha at (35, 35) should be 255 (fully opaque)'); + + // Also check a corner pixel + let corner = img.get(49, 49); + assert.strictEqual(corner[3], 255, 'alpha at (49, 49) should be 255 (fully opaque)'); + }); + }); + suite.todo('p5.Image.prototype.mask', function () { for (const density of [1, 2]) { test(`it should mask the image at pixel density ${density}`, function () {