Another dive into the HTML5 canvas element, last time I made a client-side image placeholder, this time I'll be working with existing images. Dry reading.
This time around I thought I'd try and make something a bit more visually
interesting, so I'll be adding an image onto the canvas element. I'll be using
the following SVG
image (but any image should work):
I've found that it's not entirely straight-forward to place an image on the
canvas, certainly not an SVG
image. The issue seems to be how the image is
scaled in the context of the canvas element and independent of the rest of the
browser.
Take for example this naive implementation:
function improperScaling() {
var image = new Image();
var canvas = document.querySelector("#canvas-1");
var context = canvas.getContext("2d");
image.onload = function() {
context.drawImage(image, 0, 0);
};
image.src = "/2016/static/lcat.svg";
};
improperScaling();
The .onload
function is used to ensure the image is entirely loaded before it
is added to the canvas. You'll notice of course that there are several things
wrong with the result.
The second is especially an issue on a "retina" display.
The first issue seems to be a result of the image's embedded width and height,
rather than fitting within the bounds of the canvas it simply runs over. The
.drawImage
function allows for several options beyond those specified above
(where 0, 0
specify the placement of the image on the canvas as x and
y-coordinates). Most promisingly, .drawImage
allows for an
arity of 4:
ctx.drawImage(image, dx, dy, dWidth, dHeight);
where dWidth
and dHeight
are the image's width and height on the
destination canvas. The only tricky bit here is that there is no option to
scale down one dimension from the other. It isn't really an issue once you
remember to apply the scaling yourself (height as a function of width or vice
versa).
context.drawImage(image, 0, 0, 100, 100*(image.height / image.width));
Here I am setting the destination width to 100 and scaling the height based on
the fraction of height to width. If the image were square, it would come out an
even 1 and result in 100x100
, in reality it's about 1.189 so the result is
100x118
.
function betterScaling() {
var image = new Image();
var canvas = document.querySelector("#canvas-2");
var context = canvas.getContext("2d");
image.onload = function() {
context.drawImage(image, 0, 0, 100, 100*(image.height / image.width));
};
image.src = "/2016/static/lcat.svg";
};
betterScaling();
This required some searching, but I eventually found my answer here, which turned up the useful search term "devicePixelRatio"; as MDN explains it:
The Window.devicePixelRatio read-only property returns the ratio of the (vertical) size of one physical pixel on the current display device to the size of one device independent pixels(dips).
Basically, high-DPI displays pack multiple (in my case 2) pixels into what
would traditionally be 1 pixel. So giving a canvas element a value of 100px
causes havoc because it is getting upscaled and not taking advantage of the S
in SVG
(scalable).
So, the solution is to first define the canvas element's style width/height and
then promptly redefine the canvas's width and height property by a factor of
this devicePixelRatio
, to "pack in" the correct number of pixels for the
context. The contents of the canvas are then scaled back up with the call to
.scale
so that you don't have a half-sized image. Round-about, but it works.
function scaledDownNotFuzzy() {
var image = new Image();
var ratio = window.devicePixelRatio || 1;
var canvas = document.querySelector("#canvas-3");
var context = canvas.getContext("2d");
canvas.style.width = canvas.width + "px";
canvas.style.height = canvas.height + "px";
canvas.width *= ratio;
canvas.height *= ratio;
image.onload = function() {
context.scale(ratio, ratio);
context.drawImage(image, 0, 0, 100, 100 * image.height / image.width);
};
image.src = "/2016/static/lcat.svg";
};
scaledDownNotFuzzy();
This whole thing was a bit of a rabbit hole. I now have a better appreciation for how involved working with the canvas can be, certainly more than creating just the image placeholder. I plan on picking back up later with something more interesting because this whole thing was pretty anti-climactic.