Most appropriate sub-area of p5.js?
p5.js version
2.3.2
Web browser and version
All browsers (tested on Chrome / Playwright Chromium)
Operating system
macOS / Linux / Windows
Steps to reproduce this
Steps:
- Create an image (e.g.
img = createImage(100, 100)).
- Set
img.pixelDensity(2) — logical dimensions become 50x50 as expected.
- Call
img.pixelDensity(2) a second time — logical dimensions become 25x25 (it divides img.width by 2 again instead of remaining 50).
- Call
img.pixelDensity(1) to reset density — img.width stays 25 instead of returning to 100.
- Additionally, on an image with
pixelDensity > 1, call img.resize(50, 50) — this.canvas.width is set to 50 instead of 50 * pd (100). Subsequent calls to img.loadPixels() read width * pd (100x100) from a 50x50 canvas, filling 75% of the pixel buffer with out-of-bounds transparent black ([0, 0, 0, 0]).
Snippet:
function setup() {
createCanvas(200, 200);
// 1. Create a 100x100 image
const img = createImage(100, 100);
console.log('Initial:', img.width, img.height); // 100 100
// 2. Set density to 2
img.pixelDensity(2);
console.log('After density 2:', img.width, img.height); // 50 50
// 3. Re-assert density 2 (should be idempotent)
img.pixelDensity(2);
console.log('After 2nd density 2 call:', img.width, img.height);
// Actual: 25 25 (BUG! exponentially shrinks on repeated calls)
// Expected: 50 50
// 4. Reset density to 1
img.pixelDensity(1);
console.log('After reset to density 1:', img.width, img.height);
// Actual: 25 25 (BUG! fails to restore original dimensions)
// Expected: 100 100
// 5. Resize high-DPI image
img.pixelDensity(2);
img.resize(50, 50);
console.log('Canvas physical buffer width:', img.canvas.width);
// Actual: 50 (BUG! canvas is downscaled to logical size, ignoring density)
// Expected: 100 (50 * 2)
img.loadPixels();
console.log('img.pixels.length:', img.pixels.length); // 40000 (100*100*4)
// Because canvas is only 50x50, 75% of pixels are read out of bounds as [0, 0, 0, 0]
}
Root Cause Analysis
In src/image/p5.Image.js:
-
In pixelDensity(density) (lines 66–67):
this._pixelDensity = density;
// Adjust canvas dimensions based on pixel density
this.width /= density;
this.height /= density;
this.width and this.height represent logical dimensions. Dividing this.width by density in place causes compounding division on successive calls ($W_k = W_0 / \prod d_i$) and makes resetting pixelDensity(1) a no-op.
Logical dimensions should be derived directly from the physical buffer dimensions:
this.width = this.canvas.width / density;
this.height = this.canvas.height / density;
-
In resize(width, height) (line 787):
this.canvas.width = this.width = width;
this.canvas.height = this.height = height;
The backing canvas is assigned the logical width without multiplying by this._pixelDensity. High-DPI images lose their physical pixel buffer resolution, and loadPixels() requests width * pd by height * pd from an undersized canvas.
The canvas dimensions must be scaled by pd:
const pd = this._pixelDensity;
const canvasWidth = Math.floor(width * pd);
const canvasHeight = Math.floor(height * pd);
...
this.width = width;
this.height = height;
this.canvas.width = canvasWidth;
this.canvas.height = canvasHeight;
Proposed Fix
diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js
index 423d7f1c..a4abf0a5 100644
--- a/src/image/p5.Image.js
+++ b/src/image/p5.Image.js
@@ -62,9 +62,9 @@ class Image {
this._pixelDensity = density;
- // Adjust canvas dimensions based on pixel density
- this.width /= density;
- this.height /= density;
+ // Adjust logical dimensions based on physical canvas dimensions and pixel density
+ this.width = this.canvas.width / density;
+ this.height = this.canvas.height / density;
return this; // Return the image instance for chaining if needed
} else {
@@ -727,20 +727,24 @@ class Image {
// auto-resize
if (width === 0 && height === 0) {
- width = this.canvas.width;
- height = this.canvas.height;
+ width = this.width;
+ height = this.height;
} else if (width === 0) {
- width = (this.canvas.width * height) / this.canvas.height;
+ width = (this.width * height) / this.height;
} else if (height === 0) {
- height = (this.canvas.height * width) / this.canvas.width;
+ height = (this.height * width) / this.width;
}
width = Math.floor(width);
height = Math.floor(height);
+ const pd = this._pixelDensity;
+ const canvasWidth = Math.floor(width * pd);
+ const canvasHeight = Math.floor(height * pd);
+
const tempCanvas = document.createElement('canvas');
- tempCanvas.width = width;
- tempCanvas.height = height;
+ tempCanvas.width = canvasWidth;
+ tempCanvas.height = canvasHeight;
if (this.gifProperties) {
const props = this.gifProperties;
@@ -761,8 +765,8 @@ class Image {
};
for (let i = 0; i < props.numFrames; i++) {
const resizedImageData = this.drawingContext.createImageData(
- width,
- height
+ canvasWidth,
+ canvasHeight
);
nearestNeighbor(props.frames[i].image, resizedImageData);
props.frames[i].image = resizedImageData;
@@ -784,20 +788,22 @@ class Image {
);
// Resize the original canvas, which will clear its contents
- this.canvas.width = this.width = width;
- this.canvas.height = this.height = height;
+ this.width = width;
+ this.height = height;
+ this.canvas.width = canvasWidth;
+ this.canvas.height = canvasHeight;
//Copy the image back
this.drawingContext.drawImage(
tempCanvas,
0,
0,
- width,
- height,
+ canvasWidth,
+ canvasHeight,
0,
0,
- width,
- height
+ canvasWidth,
+ canvasHeight
);
if (this.pixels.length > 0) {
Most appropriate sub-area of p5.js?
p5.js version
2.3.2
Web browser and version
All browsers (tested on Chrome / Playwright Chromium)
Operating system
macOS / Linux / Windows
Steps to reproduce this
Steps:
img = createImage(100, 100)).img.pixelDensity(2)— logical dimensions become50x50as expected.img.pixelDensity(2)a second time — logical dimensions become25x25(it dividesimg.widthby 2 again instead of remaining 50).img.pixelDensity(1)to reset density —img.widthstays25instead of returning to100.pixelDensity > 1, callimg.resize(50, 50)—this.canvas.widthis set to50instead of50 * pd(100). Subsequent calls toimg.loadPixels()readwidth * pd(100x100) from a50x50canvas, filling 75% of the pixel buffer with out-of-bounds transparent black ([0, 0, 0, 0]).Snippet:
Root Cause Analysis
In
src/image/p5.Image.js:In
pixelDensity(density)(lines 66–67):this.widthandthis.heightrepresent logical dimensions. Dividingthis.widthbydensityin place causes compounding division on successive calls (pixelDensity(1)a no-op.Logical dimensions should be derived directly from the physical buffer dimensions:
In
resize(width, height)(line 787):The backing canvas is assigned the logical
widthwithout multiplying bythis._pixelDensity. High-DPI images lose their physical pixel buffer resolution, andloadPixels()requestswidth * pdbyheight * pdfrom an undersized canvas.The canvas dimensions must be scaled by
pd:Proposed Fix