Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 27 additions & 20 deletions src/image/p5.Image.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,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 {
Expand Down Expand Up @@ -611,7 +611,8 @@ class Image {
a = imgOrCol[3];
//this.updatePixels.call(this);
}
} else if (imgOrCol instanceof p5.Color) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limzykenneth I wanted to get your thoughts on this: if we want to have the ability to load all modules as separate files in the future then we may want to use p5. prefixes more to avoid imports, but if we're thinking of implementing different combinations of modules as separate single-file builds then switching to imports like this makes sense. It's a little confusing if we have both, e.g. for WebGPU where that feels like it should be a separate file, because it means using a different convention in one spot than another, but maybe that's ok?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @davepagurek! To avoid introducing a cross-module import of Color while also fixing the issue where referencing p5.Color threw ReferenceError: p5 is not defined (since p5 is not in module scope within p5.Image.js), I've updated this to use duck typing (imgOrCol?.isColor). This matches how Color is checked in p5.Shader and RendererWebGPU (leveraging isColor = true from p5.Color.js), removing the import and keeping separate module/bundle builds clean. Pushed the update in ebdb78c!

// Duck typing instead of instanceof Color to avoid importing Color across modules
} else if (imgOrCol?.isColor) {
if (idx < pixelsState.pixels.length) {
[r, g, b, a] = imgOrCol._getRGBA([255, 255, 255, 255]);
//this.updatePixels.call(this);
Expand Down Expand Up @@ -721,20 +722,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;
Expand All @@ -755,8 +760,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;
Expand All @@ -773,25 +778,27 @@ class Image {
this.canvas.height,
0,
0,
tempCanvas.width,
tempCanvas.height
canvasWidth,
canvasHeight
);

// 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) {
Expand Down
67 changes: 67 additions & 0 deletions test/unit/image/p5.Image.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,44 @@ suite('p5.Image', function () {
});
});

suite('p5.Image.prototype.pixelDensity', function () {
test('it sets and gets pixel density', function () {
const img = myp5.createImage(100, 100);
assert.strictEqual(img.pixelDensity(), 1);
img.pixelDensity(2);
assert.strictEqual(img.pixelDensity(), 2);
assert.strictEqual(img.width, 50);
assert.strictEqual(img.height, 50);
assert.strictEqual(img.canvas.width, 100);
assert.strictEqual(img.canvas.height, 100);
});

test('repeated calls are idempotent and can be restored', function () {
const img = myp5.createImage(100, 100);
img.pixelDensity(2);
assert.strictEqual(img.width, 50);
assert.strictEqual(img.height, 50);

// Calling again should not divide dimensions further
img.pixelDensity(2);
assert.strictEqual(img.width, 50);
assert.strictEqual(img.height, 50);

// Resetting to 1 restores original logical dimensions
img.pixelDensity(1);
assert.strictEqual(img.width, 100);
assert.strictEqual(img.height, 100);
});

test('setting non-positive density defaults to 1', function () {
const img = myp5.createImage(100, 100);
img.pixelDensity(0);
assert.strictEqual(img.pixelDensity(), 1);
assert.strictEqual(img.width, 100);
assert.strictEqual(img.height, 100);
});
});

suite('p5.Image.prototype.resize', function () {
test('it should resize the image', function () {
let img = myp5.createImage(10, 17);
Expand All @@ -49,6 +87,35 @@ suite('p5.Image', function () {
assert.strictEqual(img.width, 10);
assert.strictEqual(img.height, 30);
});

test('it should resize backing canvas with pixel density > 1', function () {
const img = myp5.createImage(100, 100);
img.pixelDensity(2);
assert.strictEqual(img.width, 50);
assert.strictEqual(img.height, 50);

img.resize(40, 60);
assert.strictEqual(img.width, 40);
assert.strictEqual(img.height, 60);
assert.strictEqual(img.canvas.width, 80);
assert.strictEqual(img.canvas.height, 120);
});

test('it allows get() and set() across full logical dimensions after resize with high pixel density', function () {
const img = myp5.createImage(100, 100);
img.pixelDensity(2);
img.resize(50, 50);

const red = myp5.color(255, 0, 0, 255);
img.set(30, 30, red);
img.updatePixels();

const pixel = img.get(30, 30);
assert.strictEqual(pixel[0], 255);
assert.strictEqual(pixel[1], 0);
assert.strictEqual(pixel[2], 0);
assert.strictEqual(pixel[3], 255);
});
});

suite.todo('p5.Image.prototype.mask', function () {
Expand Down