hexwab
hexwab

Reputation: 1841

<canvas> at native resolution despite zooming, Retina

So I have a <canvas> element in which I am drawing things. Vectory things, not pixel-pushing. I would like this to display in the highest quality with the least resources under the largest range of viewing conditions. Specifically, I want to ensure that there is a 1:1 mapping between canvas pixels and device pixels, to eliminate blurry, time-consuming pixel scaling operations.

There are a few things I've seen about "Retina" displays (conclusion: devicePixelRatio is your friend), but they all assume viewing at 100% zoom. Equally I've seen things about detecting zoom level, but it seems messy and hacky and imprecise (not to mention prone to breakage). In particular, imprecision is a problem as the entire point is to get a ratio of precisely 1:1. (Plus I don't actually care about the zoom level itself, only about addressing native pixels.)

So my questions, from pragmatic to philosophical:

  1. What is the preferred way of getting a 1:1 canvas in modern (== released in the past 6 months) browsers? Do I really have to detect the device pixel ratio and the zoom (including checking for zoom changes) and calculate things from there?

  2. If I don't care about resource consumption, is a 2:1 canvas (native res on "Retina" displays, upto 200% zoom elsewhere) worth considering? I'm assuming downscaling is less bad than upscaling.

  3. What are browser vendors' take on this? Are there better solutions being worked on? Or is zoom a detail they think web developers should have no need to worry about? [Answered partially here: many consider it important. There seems some indecision on whether devicePixelRatio should change with zoom level or whether a new property that allows user-specified zoom detection as well as device-specific DPI detection is the way forward.]

  4. How many people regularly browse the web at !100% zoom? How many of them are using high DPI displays? Are there stats? [Answered here: about 10% of gmail users.]

Upvotes: 4

Views: 2322

Answers (2)

Ian Boyd
Ian Boyd

Reputation: 256621

What i found (for Chrome and Internet Explorer) is the very tricky, subtle, differences between

  • canvas.width, and
  • canvas.style.width

They are not interchangeable. If you want to manipulate them you must do so very carefully to achieve what you want. Some values are measured in physical pixels. Others are measured in logical pixels (the pixels if the screen was still zoomed to 96 dpi).

In my case i want the Canvas to take up the entire window.

First get the size of the window in logical pixels (which i call "CSS pixels"):

//get size in CSS pixels (i.e. 96 dpi)
var styleWidth = window.innerWidth;   //e.g. 420. Not document.body.clientWidth
var styleHeight = window.innerHeight; //e.g. 224. Not document.body.clientHeight

These values are in "logical pixels". When my browser is zoomed in, the amount of "logical pixels" decreases:

Zoom  window.innerWidth x windows.innerHeight
====  ======================================
100%  1680 x  859
200%   840 x  448
400%   420 x  224
 75%  2240 x 1193

What you then have to do is figure out the "real" size of the window; applying a zoom correction factor. For the moment we'll abstract the function that can figure out the current zoom level (using the TypeScript syntax):

function GetZoomLevel(): number
{
}

With the GetZoomLevel() function, we can calculate the real size of the window, in "physical pixels". When we need to set the width and height of the canvas to the size of the window in physical pixels:

//set canvas resolution to that of real pixels
var newZoomLevel = GetZoomLevel();
myCanvas.width = styleWidth * newZoomLevel;   //e.g. 1680. Not myCanvas.clientWidth
myCanvas.height = styleHeight * newZoomLevel; //e.g. 859.  Not myCanvas.clientHeight

The critical trick is that internally the canvas will render to a surface that is width by height pixels in size. After that rendering is complete, CSS can come along and shrink or stretch your canvas:

  • blurring it if CSS makes it larger
  • throwing away pixel data if CSS makes it smaller.

The final piece is to resize the canvas so that it takes up the entire client area window, using CSS length String:

myCanvas.style.width = styleWidth + "px";   //e.g. "420px"
myCanvas.style.height = styleHeight + "px"; //e.g. "224px"

So the canvas will be positioned and sized correctly in your browser window, yet internally is using full "real" pixel resolution.

GetZoomLevel()

Yes, you need the missing piece, GetZoomLevel. Unfortunately only IE supplies the information. Again, using TypeScript notation:

function GetZoomLevel(): number {
    /*
        Windows 7, running at 131 dpi (136% zoom = 131 / 96)
            Internet Explorer's "default" zoom (pressing Ctrl+0) is 136%; matching the system DPI
                screen.deviceYDPI: 130
                screen.systemYDPI: 131
                screen.logicalYDPI: 96

            Set IE Zoom to 150%
                screen.deviceYDPI: 144
                screen.systemYDPI: 131
                screen.logicalYDPI: 96

        So  a user running their system at 131 dpi, with IE at "normal" zoom,
        and a user running their system at  96 dpi, with IE at 136% zoom,
        both need to see the same thing; everything zoomed in from our default, assumed, 96dpi.

        http://msdn.microsoft.com/en-us/library/cc849094(v=vs.85).aspx

    Also note that the onresize event is triggered upon change of zoom factor, therefore you should make sure 
    that any code that takes into account DPI is executed when the onresize event is triggered, as well.

    http://htmldoodads.appspot.com/dimensions.html
    */


    var zoomLevel: number;

    //If the browser supports the corrent API, then use that
    if (screen && screen.deviceXDPI && screen.logicalXDPI)
    { 
        //IE6 and above
        zoomLevel = (screen.deviceYDPI / screen.logicalYDPI);
    else
    {
        //Chrome (see http://htmldoodads.appspot.com/dimensions.html)
        zoomLevel = window.outerWidth / window.innerWidth; //e.g. 1680 / 420
    }

    return zoomLevel;
}

Unfortunately no browsers besides IE support telling you:

  • device dpi
  • logical dpi

My full TS code is:

function OnWindowResize() {
    if (drawingTool == null)
        return;

    //Zoom changes trigger window resize event

    //get size in CSS pixels (i.e. 96 dpi)
    var styleWidth = window.innerWidth; //myCanvas.clientWidth;
    var styleHeight = window.innerHeight; //myCanvas.clientHeight;

    var newZoomLevel = GetZoomLevel();

    //    myCanvas.width = document.body.clientWidth;
    //    myCanvas.height = document.body.clientHeight;

    //set canvas resolution to that of real pixels
    myCanvas.width = styleWidth * newZoomLevel;
    myCanvas.height = styleHeight * newZoomLevel;
    //myCanvas.clientWidth = styleWidth;
    //myCanvas.clientHeight = styleHeight;

    myCanvas.style.width = styleWidth + "px"; //styleWidth.toString() + "px";
    myCanvas.style.height = styleHeight + "px"; //styleHeight.toString() + "px";

    drawingTool.Width = myCanvas.width;
    drawingTool.Height = myCanvas.height;

    // Only take action if zoom factor has changed (won't be triggered by actual resize of window)
    if (newZoomLevel != lastZoomLevel) {
        lastZoomLevel = newZoomLevel;
        drawingTool.CurrentScaleFactor = newZoomLevel;
    }
}

It's also important to tell your drawing code the user's current zoom level, in case you try to hard-code any lengths. E.g. attempting to draw

a 32px box at (50,50)

is wrong. You need to draw a

128 px box at (200,200)

when the zoom factor is 4.

Note: Any code is released into the public domain. No attribution required.

Upvotes: 4

achudars
achudars

Reputation: 1506

You basically cannot detect zoom level on modern browser versions of Firefox and Chrome:

On Firefox 18+ Mozilla changes the devicePixelRatio value on manual zoom (cmd/ctrl +/-), making it impossible to know whether the browser is in zoom mode or is it a retina device, ignoring what the word DEVICE represents.

On Chrome 27 (Meaning WebKit and Blink) webkitTextSizeAdjust was deprecated on desktops versions of the browser. This was the only bullet proof way to detect zoom in desktop chrome that I am aware of. There are couple of other ways, but they don't cover all the bases - one uses SVG but is not working in iFrames, the other uses window.inner/outerWidth and is not working when there is a sidebar or the DevTools are open on the side.

Source

Upvotes: 0

Related Questions