Skip to content

Commit 3d29ddc

Browse files
authored
Improve resolution on devices with >1 pixel density (#71)
* Support for >1 devicePixelRatio by CSS downscaling a larger canvas * Add getPixel to Graphics that accounts for pixel density when extracting ImageData and refactor tests to use it
1 parent f52dbe6 commit 3d29ddc

File tree

8 files changed

+114
-103
lines changed

8 files changed

+114
-103
lines changed

src/graphics/index.js

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ class GraphicsManager extends Manager {
2222
elementPool = [];
2323
elementPoolSize = 0;
2424
accessibleDOMElements = [];
25+
/**
26+
* The ratio of physical pixels to CSS pixels for the current device.
27+
* This allows the canvas to be scaled for higher resolution drawing.
28+
* For example, a devicePixelRatio of 2 indicates that the device will use
29+
* 2 physical pixels to draw a single css pixel.
30+
* https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
31+
* @private
32+
* @type {number}
33+
*/
34+
devicePixelRatio = window.devicePixelRatio ?? 1;
2535

2636
/**
2737
* Set up an instance of the graphics library.
@@ -275,7 +285,7 @@ class GraphicsManager extends Manager {
275285
*/
276286
getWidth() {
277287
const canvas = this.getCanvas();
278-
return parseFloat(canvas.getAttribute('width'));
288+
return parseFloat(canvas.getAttribute('width') / this.devicePixelRatio);
279289
}
280290

281291
/**
@@ -284,7 +294,7 @@ class GraphicsManager extends Manager {
284294
*/
285295
getHeight() {
286296
const canvas = this.getCanvas();
287-
return parseFloat(canvas.getAttribute('height'));
297+
return parseFloat(canvas.getAttribute('height') / this.devicePixelRatio);
288298
}
289299

290300
/**
@@ -437,14 +447,18 @@ class GraphicsManager extends Manager {
437447
const temporaryCanvas = document.createElement('canvas');
438448
temporaryCanvas.width = canvas.width;
439449
temporaryCanvas.height = canvas.height;
450+
temporaryCanvas.style.width = `${w / this.devicePixelRatio}px`;
451+
temporaryCanvas.style.height = `${h / this.devicePixelRatio}px`;
440452
const temporaryContext = temporaryCanvas.getContext('2d');
441453
temporaryContext.drawImage(canvas, 0, 0);
442454

443-
canvas.width = w;
444-
canvas.height = h;
445-
canvas.style['max-height'] = h;
446-
canvas.style['max-width'] = w;
447-
this.getContext().drawImage(temporaryCanvas, 0, 0);
455+
canvas.width = w * this.devicePixelRatio;
456+
canvas.height = h * this.devicePixelRatio;
457+
canvas.style.width = `${w}px`;
458+
canvas.style.height = `${h}px`;
459+
const context = this.getContext();
460+
context.drawImage(temporaryCanvas, 0, 0);
461+
context.scale(this.devicePixelRatio, this.devicePixelRatio);
448462
temporaryCanvas.remove();
449463
}
450464

@@ -551,6 +565,7 @@ class GraphicsManager extends Manager {
551565
document.body.appendChild(currentCanvas);
552566
}
553567
this.currentCanvas = currentCanvas;
568+
this.setSize(currentCanvas.width, currentCanvas.height);
554569

555570
// On changing the canvas reset the state.
556571
this.fullReset();
@@ -580,6 +595,26 @@ class GraphicsManager extends Manager {
580595
return this.getCanvas()?.getContext?.('2d');
581596
}
582597

598+
/**
599+
* Return the RGBA value of the pixel at the x, y coordinate.
600+
* @param {number} x - X coordinate
601+
* @param {number} y - Y coordinate
602+
* @returns {Array<number>} pixel - the [r, g, b, a] values for the pixel.
603+
*/
604+
getPixel(x, y) {
605+
const context = this.getContext();
606+
x *= this.devicePixelRatio;
607+
y *= this.devicePixelRatio;
608+
const pixelData = context.getImageData(x, y, 1, 1).data;
609+
const index = 0;
610+
return [
611+
pixelData[index + 0],
612+
pixelData[index + 1],
613+
pixelData[index + 2],
614+
pixelData[index + 3],
615+
];
616+
}
617+
583618
/**
584619
* Redraw this graphics canvas.
585620
*/
@@ -767,9 +802,10 @@ class GraphicsManager extends Manager {
767802
const calculateCoordinates = e => {
768803
const canvas = e.target;
769804
const rect = canvas.getBoundingClientRect();
805+
debugger;
770806
return {
771-
x: Math.round(((e.clientX - rect.left) * canvas.width) / canvas.clientWidth),
772-
y: Math.round(((e.clientY - rect.top) * canvas.height) / canvas.clientHeight),
807+
x: e.clientX - rect.left,
808+
y: e.clientY - rect.top,
773809
};
774810
};
775811

test/arc.test.js

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,8 @@ describe('arc', () => {
124124
a.setColor('red');
125125
g.add(a);
126126
g.redraw();
127-
const context = g.getContext();
128-
const pixel = context.getImageData(0, 0, 1, 1);
129-
expect(pixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
127+
const pixel = g.getPixel(0, 0);
128+
expect(pixel).toEqual([255, 0, 0, 255]);
130129
});
131130
});
132131

@@ -265,15 +264,13 @@ describe('arc', () => {
265264
a.setColor('red');
266265
g.add(a);
267266
g.redraw();
268-
let topLeftPixel = g.getContext().getImageData(0, 0, 1, 1);
269-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([0, 0, 0, 0]));
267+
let topLeftPixel = g.getPixel(0, 0);
268+
expect(topLeftPixel).toEqual([0, 0, 0, 0]);
270269
a.setPosition(g.getWidth() - a.radius / 2, g.getHeight() - a.radius / 2);
271270

272271
g.redraw();
273-
const bottomRightPixel = g
274-
.getContext()
275-
.getImageData(g.getWidth() - 1, g.getHeight() - 1, 1, 1);
276-
expect(bottomRightPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
272+
const bottomRightPixel = g.getPixel(g.getWidth() - 1, g.getHeight() - 1);
273+
expect(bottomRightPixel).toEqual([255, 0, 0, 255]);
277274
});
278275
it('{vertical: 0.5, horizontal: 0.5}', () => {
279276
const a = new Arc(10, 0, 359, Arc.DEGREES);
@@ -283,15 +280,13 @@ describe('arc', () => {
283280
a.setColor('red');
284281
g.add(a);
285282
g.redraw();
286-
let topLeftPixel = g.getContext().getImageData(0, 0, 1, 1);
287-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
283+
let topLeftPixel = g.getPixel(0, 0);
284+
expect(topLeftPixel).toEqual([255, 0, 0, 255]);
288285
a.setPosition(g.getWidth(), g.getHeight());
289286

290287
g.redraw();
291-
const bottomRightPixel = g
292-
.getContext()
293-
.getImageData(g.getWidth() - 1, g.getHeight() - 1, 1, 1);
294-
expect(bottomRightPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
288+
const bottomRightPixel = g.getPixel(g.getWidth() - 1, g.getHeight() - 1);
289+
expect(bottomRightPixel).toEqual([255, 0, 0, 255]);
295290
});
296291
});
297292

@@ -303,12 +298,12 @@ describe('arc', () => {
303298
g.shouldUpdate = false;
304299
g.add(a);
305300
g.redraw();
306-
let topLeftPixel = g.getContext().getImageData(0, 0, 1, 1);
307-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([0, 0, 0, 0]));
301+
let topLeftPixel = g.getPixel(0, 0);
302+
expect(topLeftPixel).toEqual([0, 0, 0, 0]);
308303
a.setAnchor({ vertical: 0.5, horizontal: 0.4 });
309304
g.redraw();
310-
topLeftPixel = g.getContext().getImageData(1, 1, 1, 1);
311-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
305+
topLeftPixel = g.getPixel(1, 1);
306+
expect(topLeftPixel).toEqual([255, 0, 0, 255]);
312307
});
313308
});
314309

test/circle.test.js

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,11 @@ describe('Circle', () => {
9494
g.shouldUpdate = false;
9595
const circle = new Circle(2);
9696
circle.setColor(Color.RED);
97-
const context = g.getContext();
9897
circle.setPosition(20, 20);
9998
g.add(circle);
10099
g.redraw();
101-
const pixel = context.getImageData(20, 20, 1, 1);
102-
expect(pixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
100+
const pixel = g.getPixel(20, 20);
101+
expect(pixel).toEqual([255, 0, 0, 255]);
103102
});
104103
});
105104
it('getRadius returns radius', () => {
@@ -181,30 +180,26 @@ describe('Circle', () => {
181180
// a circle at the top left should be placed exactly
182181
// at the origin, with the bottom right quadrant visible
183182
g.redraw();
184-
let topLeftPixel = g.getContext().getImageData(0, 0, 1, 1);
185-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
183+
let topLeftPixel = g.getPixel(0, 0);
184+
expect(topLeftPixel).toEqual([255, 0, 0, 255]);
186185

187186
// a circle at the top left with anchor 0, 0 should be drawn
188187
// down and to the right of the origin, with its entire
189188
// self visible
190189
c.setAnchor({ vertical: 0, horizontal: 0 });
191190
g.redraw();
192-
topLeftPixel = g.getContext().getImageData(0, 0, 1, 1);
193-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([0, 0, 0, 0]));
191+
topLeftPixel = g.getPixel(0, 0, 1, 1);
192+
expect(topLeftPixel).toEqual([0, 0, 0, 0]);
194193

195194
c.setPosition(g.getWidth(), g.getHeight());
196195
c.setAnchor({ vertical: 1, horizontal: 1 });
197196
g.redraw();
198-
let bottomRightPixel = g
199-
.getContext()
200-
.getImageData(g.getWidth() - 1, g.getHeight() - 1, 1, 1);
201-
expect(bottomRightPixel.data).toEqual(new Uint8ClampedArray([0, 0, 0, 0]));
197+
let bottomRightPixel = g.getPixel(g.getWidth() - 1, g.getHeight() - 1);
198+
expect(bottomRightPixel).toEqual([0, 0, 0, 0]);
202199
c.setAnchor({ vertical: 0.5, horizontal: 0.5 });
203200
g.redraw();
204-
bottomRightPixel = g
205-
.getContext()
206-
.getImageData(g.getWidth() - 1, g.getHeight() - 1, 1, 1);
207-
expect(bottomRightPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
201+
bottomRightPixel = g.getPixel(g.getWidth() - 1, g.getHeight() - 1);
202+
expect(bottomRightPixel).toEqual([255, 0, 0, 255]);
208203
});
209204
});
210205

test/graphics.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ describe('Graphics', () => {
3939
});
4040
});
4141
describe('setSize', () => {
42-
it('Changes the size of the backed canvas', () => {
42+
it('Changes the size of the backed canvas, considering the devicePixelRatio of the device', () => {
43+
window.devicePixelRatio = 2;
4344
const g = new Graphics({ shouldUpdate: false });
4445
g.setSize(20, 20);
4546
const canvas = document.querySelector('canvas');
46-
expect(canvas.width).toEqual(20);
47-
expect(canvas.height).toEqual(20);
47+
expect(canvas.width).toEqual(20 * window.devicePixelRatio);
48+
expect(canvas.height).toEqual(20 * window.devicePixelRatio);
4849
});
4950
});
5051
describe('setFullscreen', () => {
@@ -53,8 +54,7 @@ describe('Graphics', () => {
5354
g.setFullscreen();
5455
const canvas = g.getCanvas();
5556
expect(canvas.width).toEqual(document.body.offsetWidth - FULLSCREEN_PADDING);
56-
// what is the origin of this off-by-one?
57-
expect(canvas.height).toEqual(document.body.offsetHeight - FULLSCREEN_PADDING + 1);
57+
expect(canvas.height).toEqual(document.body.offsetHeight - FULLSCREEN_PADDING);
5858
});
5959
});
6060
describe('Mouse events', () => {

test/group.test.js

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,13 @@ describe('Groups', () => {
5252
g.add(group);
5353
g.redraw();
5454

55-
let context = g.getContext();
56-
let topLeftPixel = context.getImageData(0, 0, 1, 1);
57-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([0, 0, 0, 128]));
55+
let topLeftPixel = g.getPixel(0, 0);
56+
expect(topLeftPixel).toEqual([0, 0, 0, 128]);
5857

5958
group.add(r);
6059
g.redraw();
61-
context = g.getContext();
62-
topLeftPixel = context.getImageData(0, 0, 1, 1);
63-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([0, 0, 0, 128]));
60+
topLeftPixel = g.getPixel(0, 0);
61+
expect(topLeftPixel).toEqual([0, 0, 0, 128]);
6462
});
6563
});
6664
describe('Rotation', () => {
@@ -80,8 +78,8 @@ describe('Groups', () => {
8078
g.rotate(180);
8179
gfx.add(g);
8280
gfx.redraw();
83-
const topLeftPixel = gfx.getContext().getImageData(0, 0, 1, 1);
84-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([0, 0, 255, 255]));
81+
const topLeftPixel = gfx.getPixel(0, 0);
82+
expect(topLeftPixel).toEqual([0, 0, 255, 255]);
8583
});
8684
it('Should rotate the entire content of the group around its center', () => {
8785
const g = new Group();
@@ -95,8 +93,8 @@ describe('Groups', () => {
9593
g.add(chartreuseRect);
9694
gfx.add(g);
9795
gfx.redraw();
98-
const topLeftPixel = gfx.getContext().getImageData(0, 0, 1, 1);
99-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([127, 255, 0, 255]));
96+
const topLeftPixel = gfx.getPixel(0, 0);
97+
expect(topLeftPixel).toEqual([127, 255, 0, 255]);
10098
});
10199
});
102100
describe('Bounds calculations', () => {
@@ -204,13 +202,13 @@ describe('Groups', () => {
204202
gfx.add(g);
205203
gfx.redraw();
206204

207-
let topLeftPixel = gfx.getContext().getImageData(0, 0, 1, 1);
208-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([0, 0, 255, 255]));
205+
let topLeftPixel = gfx.getPixel(0, 0);
206+
expect(topLeftPixel).toEqual([0, 0, 255, 255]);
209207

210208
red.layer = 2;
211209
gfx.redraw();
212-
topLeftPixel = gfx.getContext().getImageData(0, 0, 1, 1);
213-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
210+
topLeftPixel = gfx.getPixel(0, 0);
211+
expect(topLeftPixel).toEqual([255, 0, 0, 255]);
214212
});
215213
});
216214
});

test/oval.test.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ describe('Oval', () => {
3434
o.setPosition(20, 20);
3535
g.add(o);
3636
g.redraw();
37-
const pixel = context.getImageData(20, 20, 1, 1);
38-
expect(pixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
37+
const pixel = g.getPixel(20, 20);
38+
expect(pixel).toEqual([255, 0, 0, 255]);
3939
});
4040
});
4141
describe('containsPoint', () => {
@@ -73,30 +73,26 @@ describe('Oval', () => {
7373
// a circle at the top left should be placed exactly
7474
// at the origin, with the bottom right quadrant visible
7575
g.redraw();
76-
let topLeftPixel = g.getContext().getImageData(0, 0, 1, 1);
77-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
76+
let topLeftPixel = g.getPixel(0, 0);
77+
expect(topLeftPixel).toEqual([255, 0, 0, 255]);
7878

7979
// a circle at the top left with anchor 0, 0 should be drawn
8080
// down and to the right of the origin, with its entire
8181
// self visible
8282
o.setAnchor({ vertical: 0, horizontal: 0 });
8383
g.redraw();
84-
topLeftPixel = g.getContext().getImageData(0, 0, 1, 1);
85-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([0, 0, 0, 0]));
84+
topLeftPixel = g.getPixel(0, 0);
85+
expect(topLeftPixel).toEqual([0, 0, 0, 0]);
8686

8787
o.setPosition(g.getWidth(), g.getHeight());
8888
o.setAnchor({ vertical: 1, horizontal: 1 });
8989
g.redraw();
90-
let bottomRightPixel = g
91-
.getContext()
92-
.getImageData(g.getWidth() - 1, g.getHeight() - 1, 1, 1);
93-
expect(bottomRightPixel.data).toEqual(new Uint8ClampedArray([0, 0, 0, 0]));
90+
let bottomRightPixel = g.getPixel(g.getWidth() - 1, g.getHeight() - 1);
91+
expect(bottomRightPixel).toEqual([0, 0, 0, 0]);
9492
o.setAnchor({ vertical: 0.5, horizontal: 0.5 });
9593
g.redraw();
96-
bottomRightPixel = g
97-
.getContext()
98-
.getImageData(g.getWidth() - 1, g.getHeight() - 1, 1, 1);
99-
expect(bottomRightPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
94+
bottomRightPixel = g.getPixel(g.getWidth() - 1, g.getHeight() - 1);
95+
expect(bottomRightPixel).toEqual([255, 0, 0, 255]);
10096
});
10197
});
10298

test/rectangle.test.js

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ describe('Rectangle', () => {
1414
g.add(r);
1515
g.redraw();
1616

17-
const topLeftPixel = g.getContext().getImageData(0, 0, 1, 1);
18-
expect(topLeftPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
17+
const topLeftPixel = g.getPixel(0, 0);
18+
expect(topLeftPixel).toEqual([255, 0, 0, 255]);
1919
});
2020
it('Will draw down and to the right of a 0, 0 (default) anchor', () => {
2121
const g = new Graphics();
@@ -25,14 +25,10 @@ describe('Rectangle', () => {
2525
r.setColor('red');
2626
g.add(r);
2727
g.redraw();
28-
const bottomRightPixel = g
29-
.getContext()
30-
.getImageData(g.getWidth() - 1, g.getHeight() - 1, 1, 1);
31-
expect(bottomRightPixel.data).toEqual(new Uint8ClampedArray([255, 0, 0, 255]));
32-
const oobPixel = g
33-
.getContext()
34-
.getImageData(g.getWidth() - 11, g.getHeight() - 11, 1, 1);
35-
expect(oobPixel.data).toEqual(new Uint8ClampedArray([0, 0, 0, 0]));
28+
const bottomRightPixel = g.getPixel(g.getWidth() - 1, g.getHeight() - 1);
29+
expect(bottomRightPixel).toEqual([255, 0, 0, 255]);
30+
const oobPixel = g.getPixel(g.getWidth() - 11, g.getHeight() - 11);
31+
expect(oobPixel).toEqual([0, 0, 0, 0]);
3632
});
3733
it('Affects containsPoint() calculations', () => {
3834
const r = new Rectangle(5, 5);

0 commit comments

Comments
 (0)