diff --git a/package-lock.json b/package-lock.json
index 55a2b6e8..058ac623 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
"name": "chs-js-lib",
- "version": "0.2.19",
+ "version": "0.2.20",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "version": "0.2.19",
+ "version": "0.2.20",
"license": "ISC",
"dependencies": {
"tone": "^14.7.77",
diff --git a/package.json b/package.json
index 2a1dd77f..de062a36 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "chs-js-lib",
- "version": "0.2.19",
+ "version": "0.2.20",
"description": "JavaScript graphics library used in CodeHS's platform.",
"main": "dist/chs.cjs",
"module": "dist/chs.mjs",
diff --git a/site/examples/graphics/webimage/setimage/setimage.js b/site/examples/graphics/webimage/setimage/setimage.js
new file mode 100644
index 00000000..f0fc37d8
--- /dev/null
+++ b/site/examples/graphics/webimage/setimage/setimage.js
@@ -0,0 +1,6 @@
+var img = new WebImage('https://codehs.com/uploads/056e56f7b32cdc9ed21c681c31166108');
+img.setAnchor({ vertical: 0.5, horizontal: 0.5 });
+img.setPosition(getWidth() / 2, getHeight() / 2);
+add(img);
+
+img.setImage('https://codehs.com/uploads/1e38851d76e7691d923a3d3f3f7eae1d');
diff --git a/site/examples/graphics/webimage/setimage/setimage.md b/site/examples/graphics/webimage/setimage/setimage.md
new file mode 100644
index 00000000..94fcdfef
--- /dev/null
+++ b/site/examples/graphics/webimage/setimage/setimage.md
@@ -0,0 +1,9 @@
+---
+title: WebImage - setImage
+layout: example
+code: setimage.js
+width: 500
+height: 500
+---
+
+You can replace the image content of a `WebImage` using `setImage()`
diff --git a/site/examples/index.html b/site/examples/index.html
index 78298af4..02eb2be0 100644
--- a/site/examples/index.html
+++ b/site/examples/index.html
@@ -175,6 +175,9 @@
WebImage
Manipulating ImageData
+
+ setImage
+
Rotation
diff --git a/src/graphics/group.js b/src/graphics/group.js
index 398eb196..f99a540f 100644
--- a/src/graphics/group.js
+++ b/src/graphics/group.js
@@ -23,7 +23,7 @@ class Group extends Thing {
* @private
* @type {number}
*/
- devicePixelRatio = window.devicePixelRatio ?? 1;
+ devicePixelRatio = Math.ceil(window.devicePixelRatio) ?? 1;
/**
* Constructs a new Group.
diff --git a/src/graphics/index.js b/src/graphics/index.js
index a6fe04ef..2476e108 100644
--- a/src/graphics/index.js
+++ b/src/graphics/index.js
@@ -34,7 +34,7 @@ class GraphicsManager extends Manager {
* @private
* @type {number}
*/
- devicePixelRatio = window.devicePixelRatio ?? 1;
+ devicePixelRatio = Math.ceil(window.devicePixelRatio) ?? 1;
/**
* Set up an instance of the graphics library.
diff --git a/src/graphics/webimage.js b/src/graphics/webimage.js
index deaa3508..2b4918a1 100644
--- a/src/graphics/webimage.js
+++ b/src/graphics/webimage.js
@@ -72,9 +72,14 @@ class WebImage extends Thing {
this._hiddenCanvas = document.createElement('canvas');
this._hiddenCanvas.width = 1;
this._hiddenCanvas.height = 1;
-
+ if (this.image) {
+ // if this WebImage had an existing image, it may have an unresolved onload callback.
+ // dont allow original callback to resolve, since it might attempt to load pixel data
+ // from a potentially empty canvas.
+ this.image.onload = null;
+ }
this.image = new Image();
- this.image.crossOrigin = true;
+ this.image.crossOrigin = 'anonymous';
this.image.src = filename;
this.filename = filename;
this.width = null;
diff --git a/test/graphics.test.js b/test/graphics.test.js
index 356ed6c1..76be801d 100644
--- a/test/graphics.test.js
+++ b/test/graphics.test.js
@@ -20,6 +20,14 @@ describe('Graphics', () => {
expect(canvas.width).toEqual(20 * window.devicePixelRatio);
expect(canvas.height).toEqual(20 * window.devicePixelRatio);
});
+ it('Rounds devicePixelRatio to prevent floating point sizes', () => {
+ window.devicePixelRatio = 0.89999;
+ const g = new Graphics({ shouldUpdate: false });
+ g.setSize(20, 20);
+ const canvas = document.querySelector('canvas');
+ expect(canvas.width).toEqual(20);
+ expect(canvas.height).toEqual(20);
+ });
});
describe('setFullscreen', () => {
it("Updates the canvas' size to the parent's size less padding less border", () => {
diff --git a/test/setup.js b/test/setup.js
index 91c8b027..3da8dee2 100644
--- a/test/setup.js
+++ b/test/setup.js
@@ -14,6 +14,7 @@ beforeEach(() => {
afterEach(() => {
document.body.innerHTML = '';
+ window.devicePixelRatio = 1;
Object.entries(GraphicsInstances).forEach(([id, instance]) => {
instance.cleanup();
});
diff --git a/test/webimage.test.js b/test/webimage.test.js
index 8fd521e8..35b0404a 100644
--- a/test/webimage.test.js
+++ b/test/webimage.test.js
@@ -182,6 +182,32 @@ describe('WebImage', () => {
expect(topLeftPixel).toEqual([0, 0, 255, 255]);
});
});
+ describe('setImage', () => {
+ it('Allows replacing the image of the WebImage', () => {
+ const g = new Graphics({ shouldUpdate: false });
+ const img = new WebImage('www.codehs.com/doesnt-matter.gif');
+ img.setImage(RGBURL);
+ g.add(img);
+ img.loaded(() => {
+ g.redraw();
+ expect(g.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
+ });
+ });
+ it('Cancels the original onload of the image', () => {
+ const g = new Graphics({ shouldUpdate: false });
+ const img = new WebImage('www.codehs.com/doesnt-matter.gif');
+ const firstLoadedSpy = jasmine.createSpy();
+ // we have to inspect here to get the actual onload event
+ img.image.onload = firstLoadedSpy;
+ img.setImage(RGBURL);
+ g.add(img);
+ img.loaded(() => {
+ g.redraw();
+ expect(g.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
+ expect(firstLoadedSpy).not.toHaveBeenCalled();
+ });
+ });
+ });
describe('setRed/Blue/Green/Alpha', () => {
it('Updates the underlying data', () => {
const img = new WebImage(RGBURL);