Scale Pixels for the Canvas with devicePixelRatio in React

Josh Comeau
InstructorJosh Comeau
Share this video with your friends

Social Share Links

Send Tweet
Published 7 years ago
Updated 6 years ago

When working with HTML Canvas, HiDPI displays like Apple's Retina display require a bit of extra coaxing, to make sure they look crisp and sharp. This lesson covers how to write a method that automatically scales our component for the user's displays.

Happily, once it's written, you can forget all about the DPI of your users' monitors. It's totally abstracted within our component.

Learn more about creating the component here

Instructor: [00:00] Here, we have our canvas component element. Right now, all we're using it for is to draw our border around our canvas. You'll notice on Apple Retina displays or any other high-DPI display that the canvas, by default, is a little fuzzy.

[00:12] This remains true for anything we want to draw to our canvas. If I add a fill rect, it shares the same problem of being a little fuzzy on certain monitors.

[00:20] To fix this, we need to add a scale method. We'll invoke it after the component mounts, right before we draw.

[00:26] The key to this working is to get the ratio between the number of hardware pixels to software pixels, from window.devicePixelRatio. You'll see if I log this out, I get a ratio of 2. That's because I'm filming this on a retina iMac.

[00:37] This means that this 20x20 box that we're drawing is actually being spread over 40x40 hardware pixels.

[00:43] The first thing we need to do is make it so that our canvas is drawing over the right number of pixels. We'll set up the canvas width to be equal to the width that we want it to be, times this ratio. We'll do the same thing for the height.

[00:57] If I do that, we see that there are two problems left to fix. The first is that our canvas is now far too large. It's doubled in size. The other is that the stuff we're drawing to the canvas hasn't scaled accordingly.

[01:08] Let's start by fixing the size issue. To do that, we can use CSS style. The idea here is that while the canvas will be drawing to a lot of pixels, we can constrain the size of the canvas with CSS to be the width and height that we pass in.

[01:22] Once we do that, we see that our canvas sits comfortably at the 200x200 pixels that we wanted, but the box that we're drawing within it is really tiny now. To fix this, we need to scale up our context.

[01:33] Now we don't have the context yet. We never made that available. We could pass it in as an argument to the scale function, but I think it's nicer and easier if we just store the context on the canvas instance.

[01:43] We update our draw method to use this.context, and then we can use this.context within our scale method as well. We'll call this.context.scale, and we'll pass in the ratio for both the horizontal and the vertical axis.

[01:56] This is looking pretty good. Our box is the right size. It's looking sharp, but there's something funky about our border. We have the top one and the left one, but we seem to have lost the bottom and the right one.

[02:06] The reason for that is that by scaling the context, we can no longer trust the canvas' reported dimensions. Remember, the border that we're drawing takes its width and height from the canvas. If I was to log out those values on my Retina iMac, I'd get 400x400.

[02:21] We've scaled our context though, precisely to counteract the fact that our canvas is twice as large. From our context perspective, we are in a 200x200 drawing area. We can test this by supplying a 200-pixel width and height to our stroke rect. You'll see that when we do that, we get our perfect, nice borders back.

[02:40] Of course, we can't just leave 200 pixels hard-coded in our draw method like that. Thankfully, the app already has the data that we need. Why don't we make a width constant of 200 and a height constant of 200, then we can supply those both as the width and the height to the canvas, but also on our draw method for those times where we need to know the size of our drawing area.

[03:00] Finally, one last tweak we need to make -- window.devicePixelRatio is not available in legacy browsers like IE10. If we default it to 1, it won't yell at us when we try to multiply with it.

[03:10] In review, after our component mounts, we call our new scale method right before drawing to the canvas. Inside our scale method, we figure out the ratio between software and hardware pixels. We enlarge the canvas' width and height so that there's a 1:1 ratio between software pixels and hardware pixels.

[03:27] Then we constrain the canvas using CSS so that it takes up the amount of screen space we intended. This also ensures that each software pixel maps to a hardware one. Finally, we scale up the context so that we preserve the coordinate system that we want, in this case 200x200.

[03:42] This ensures that the 20x20 box we added is sized correctly, allowing us to draw sharp shapes without redoing all the math for retina displays.

egghead
egghead
~ an hour ago

Member comments are a way for members to communicate, interact, and ask questions about a lesson.

The instructor or someone from the community might respond to your question Here are a few basic guidelines to commenting on egghead.io

Be on-Topic

Comments are for discussing a lesson. If you're having a general issue with the website functionality, please contact us at support@egghead.io.

Avoid meta-discussion

  • This was great!
  • This was horrible!
  • I didn't like this because it didn't match my skill level.
  • +1 It will likely be deleted as spam.

Code Problems?

Should be accompanied by code! Codesandbox or Stackblitz provide a way to share code and discuss it in context

Details and Context

Vague question? Vague answer. Any details and context you can provide will lure more interesting answers!

Markdown supported.
Become a member to join the discussionEnroll Today