SVG rendered into canvas blurred on retina display

2019-01-15 16:44发布

问题:

I have an issue with SVG rendered into canvas. On retina displays canvas rendered as base64 url and set as SRC into is blurred.

I've tried various methods that was described in list below with no luck:

  • https://tristandunn.com/2014/01/24/rendering-svg-on-canvas/
  • How do I fix blurry text in my HTML5 canvas?
  • Canvas drawing and Retina display: doable?
  • https://www.html5rocks.com/en/tutorials/canvas/hidpi/

Now i don't know what should i do to make it better. Please look into my result: jsfiddle.net/a8bj5fgj/2/

Edit:

Updated fiddle with fix: jsfiddle.net/a8bj5fgj/7/

回答1:

Retina display

Retina and very high resolution displays have pixel sizes that are smaller than the average human eye can resolve. Rendering a single line then ends up looking a like a lighter line. To fix the issues involved pages that detect the high res display will change the default CSS pixel size to 2.

The DOM knows this and adjusts its rendering to compensate. But the Canvas is not aware and its rendering is just scaled up. The default display rendering type for the canvas is bilinear interpolation. This smooths the transition from one pixel to the next which is great for photos, but not so great for lines, text, SVG and the like.

Some solutions

  • First is to turn of bilinear filtering on the canvas. This can be done with the CSS rule image-rendering: pixelated; Though this will not create the quality of the SVG rendered on the DOM it will reduce the appearance of blurriness some users experience.

  • When rendering SVG to the canvas you should turn off image smoothing as that can reduce the quality of the svg image. SVG is rendered internally and does not need additional smoothing when the internal copy is rendered onto the canvas.

    To do this ctx.imageSmoothingEnabled = false;

  • Detect the CSS pixel size. The window variable devicePixelRatio returns the size of the CSS pixel compared to the actual screen physical pixel size. Retina and High res devices will typically have a value of 2. You can then use that to set the canvas resolution to match the physical pixel resolution.

    But there is a problem because devicePixelRatio is not supported on all browsers and devicePixelRatio is affected by the page zoom setting.

    So at the most basic using devicePixelRatio and the assumption that few people zoom past 200%.

Code Assuming that the canvas.style.width and canvas.style.height are already correctly set.

if(devicePixelRatio >= 2){        
    canvas.width *= 2;
    canvas.height *= 2;
}

Now that you have increased the resolution you must also increase the rendering size. This can be done via the canvas transform, and better yet create it as a function.

function setCanvasForRetina(canvas){
    canvas.width *= 2;
    canvas.height *= 2;
    canvas.setTransform(2,0,0,2,0,0);
}

Note I do not increase the pixel size by the value of "devicePixelRatio" This is because retina devices will only have a resolution of 2 times, if the aspect ratio is greater than 2 it is because the client has zoomed in. To honor the expected behaviour of the canvas I do not adjust for zoom setting if I can. Though that is not a rule just a suggestion.

A better Guess

The two methods above are either a stop gap solution or a simple guess. You can improve your odds by examining some of the system.

Retina displays currently come in a fixed set of resolutions for a fixed set of devices (phones, pads, notebooks).

You can query the window.screen.width and window.screen.height to determine the absolute physical pixel resolution and match that against known retina displays resolutions. You can also query the userAgent to determine the device type and brand.

Putting it all together you can improve the guess. The next function makes a guess if the display is retina. You can use something similar to determin if the device is retina and then increase the canvas resolution accordingly.

Information for the following code found at wiki Retina Display Models This information can be machine queried using Wiki's SPARQL interface if you want to keep it up to date.

Demo Guess if Retina.

rWidth.textContent = screen.width
rHeight.textContent = screen.height

aWidth.textContent = screen.availWidth
aHeight.textContent = screen.availHeight

pWidth.textContent = innerWidth
pHeight.textContent = innerHeight

dWidth.textContent = document.body.clientWidth
dHeight.textContent = document.body.clientHeight

//doWidth.textContent = document.body.offsetWidth
//doHeight.textContent = document.body.offsetHeight

//sWidth.textContent = document.body.scrollWidth
//sHeight.textContent = document.body.scrollHeight

pAspect.textContent = devicePixelRatio

userA.textContent = navigator.userAgent



function isRetina(){
  // source https://en.wikipedia.org/wiki/Retina_Display#Models
  var knownRetinaResolutions = [[272,340], [312,390], [960,640], [1136,640 ], [1334,750 ], [1920,1080], [2048,1536], [2732,2048], [2304,1440], [2560,1600], [2880,1800], [4096,2304], [5120,2880]];
  var knownPhones =  [[960,640], [1136,640 ], [1334,750 ], [1920,1080]];
  var knownPads =  [[2048,1536], [2732,2048]];
  var knownBooks = [[2304,1440], [2560,1600], [2880,1800], [4096,2304], [5120,2880]];

  var hasRetinaRes = knownRetinaResolutions.some(known => known[0] === screen.width && known[1] === screen.height);
  var isACrapple = /(iPhone|iPad|iPod|Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh)/.test(navigator.userAgent);
  var hasPhoneRes =  knownPhones.some(known => known[0] === screen.width && known[1] === screen.height);
  var isPhone = /iPhone/.test(navigator.userAgent);
  var hasPadRes =  knownPads.some(known => known[0] === screen.width && known[1] === screen.height);
  var isPad = /iPad/.test(navigator.userAgent);
  var hasBookRes =  knownBooks.some(known => known[0] === screen.width && known[1] === screen.height);
  var isBook = /Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh/.test(navigator.userAgent);

  var isAgentMatchingRes = (isBook && hasBookRes && !isPad && !isPhone) ||
      (isPad && hasPadRes && !isBook && !isPhone) ||
      (isPhone && hasPhoneRes && !isBook && !isPad)
  return devicePixelRatio >= 2 && 
         isACrapple && 
         hasRetinaRes && 
          isAgentMatchingRes;
}

guess.textContent = isRetina() ? "Yes" : "No";
    
div, h1, span {
  font-family : arial;
}
span {
  font-weight : bold
}
<div class="r-display" id="info">
  <h1>System info</h1>
  <div>Device resolution : 
    <span id = "rWidth"></span> by <span id = "rHeight"></span> pixels
  </div>
  <div>Availabe resolution : 
    <span id = "aWidth"></span> by <span id = "aHeight"></span> pixels
  </div>
  <div>Page resolution : 
    <span id = "pWidth"></span> by <span id = "pHeight">  </span> CSS pixels
  </div>
  <div>Document client res : 
    <span id = "dWidth"></span> by <span id = "dHeight">  </span> CSS pixels
  </div>
  <div>Pixel aspect : 
    <span id = "pAspect"></span>
  </div>
  <div>User agent :
    <span id="userA"></span>
  </div>
  <h3>Best guess is retina "<span id = "guess"></span>!"</h3>
</div>
  

From your snippet

This may do what you want. As I don't own any apple products I can not test it apart from forcing true on isRetina.

    function isRetina() {
        // source https://en.wikipedia.org/wiki/Retina_Display#Models
        var knownRetinaResolutions = [[272, 340], [312, 390], [960, 640], [1136, 640], [1334, 750], [1920, 1080], [2048, 1536], [2732, 2048], [2304, 1440], [2560, 1600], [2880, 1800], [4096, 2304], [5120, 2880]];
        var knownPhones = [[960, 640], [1136, 640], [1334, 750], [1920, 1080]];
        var knownPads = [[2048, 1536], [2732, 2048]];
        var knownBooks = [[2304, 1440], [2560, 1600], [2880, 1800], [4096, 2304], [5120, 2880]];

        var hasRetinaRes = knownRetinaResolutions.some(known => known[0] === screen.width && known[1] === screen.height);
        var isACrapple = /(iPhone|iPad|iPod|Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh)/.test(navigator.userAgent);
        var hasPhoneRes = knownPhones.some(known => known[0] === screen.width && known[1] === screen.height);
        var isPhone = /iPhone/.test(navigator.userAgent);
        var hasPadRes = knownPads.some(known => known[0] === screen.width && known[1] === screen.height);
        var isPad = /iPad/.test(navigator.userAgent);
        var hasBookRes = knownBooks.some(known => known[0] === screen.width && known[1] === screen.height);
        var isBook = /Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh/.test(navigator.userAgent);

        var isAgentMatchingRes = (isBook && hasBookRes && !isPad && !isPhone) ||
            (isPad && hasPadRes && !isBook && !isPhone) ||
            (isPhone && hasPhoneRes && !isBook && !isPad);
      
      
        return devicePixelRatio >= 2 && isACrapple && hasRetinaRes && isAgentMatchingRes;
    }
    
    function svgToImage(svg){
        function svgAsImg() {
            var canvas, ctx;
            canvas = document.createElement("canvas");
            ctx = canvas.getContext("2d");
            var width = this.width;
            var height = this.height;
            var scale = 1;
            if(isRetina()){
                width *= 2;
                height *= 2;
                scale = 2;
            }

            canvas.width = width;
            canvas.height = height;
            ctx.setTransform(scale, 0, 0, scale, 0, 0);
            ctx.imageSmoothingEnabled = false;  // SVG rendering is better with smoothing off
            ctx.drawImage(this,0,0);
            DOMURL.revokeObjectURL(url);
            try{
                var image = new Image();
                image.src = canvas.toDataURL();              
                imageContainer.appendChild(image);
                image.width = this.width;
                image.height = this.height;
            }catch(e){  // in case of CORS error fallback to canvas
                canvas.style.width = this.width + "px";  // in CSS pixels not physical pixels
                canvas.style.height = this.height + "px";
                imageContainer.appendChild(canvas);  // just use the canvas as it is an image as well
            }
        };
        var url;
        var img = new Image();
        var DOMURL = window.URL || window.webkitURL || window;
        img.src = url = DOMURL.createObjectURL(new Blob([svg], {type: 'image/svg+xml'}));           
        img.onload = svgAsImg;
    }
    
    svgToImage(svgContainer.innerHTML);
   
<div id="svgContainer"><svg width="31" height="40" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 43 55" fill="#736b9e"><path d="m 40.713968,30.966202 c 0.0028,0.05559 -0.01078,0.114956 -0.044,0.178882 -1.545645,2.974287 -2.853499,5.591663 -4.339695,7.673668 -0.788573,1.104704 -2.095869,2.778673 -2.874223,3.773068 -0.994236,1.02684 -6.879641,7.657944 -6.167884,7.049648 -1.292899,1.235403 -5.717368,5.476022 -5.717368,5.476022 0,0 -4.323294,-3.985179 -5.928388,-5.591297 C 14.037321,47.920078 10.708239,43.994015 9.6976253,42.770306 8.6870114,41.546601 8.5086687,40.900753 6.8441265,38.818752 5.8958518,37.63265 4.1376268,34.24638 3.0745121,32.156026 2.9037625,31.86435 2.7398218,31.568267 2.5826899,31.268005 2.5509386,31.228498 2.5238331,31.18779 2.5044312,31.145084 2.4575955,31.041974 2.4164305,30.951055 2.3805569,30.87146 0.95511134,28.003558 0.15221914,24.771643 0.15221914,21.351725 c 0,-11.829154 9.58943056,-21.41858234 21.41858286,-21.41858234 11.829152,0 21.418583,9.58942834 21.418583,21.41858234 0,3.457576 -0.820406,6.72314 -2.275417,9.614477 z M 21.52596,1.5031489 c -10.866018,0 -19.6746717,8.8086521 -19.6746717,19.6746741 0,10.866016 8.8086537,19.674669 19.6746717,19.674669 10.866018,0 19.674672,-8.808648 19.674672,-19.674669 0,-10.866022 -8.808654,-19.6746741 -19.674672,-19.6746741 z" /><g transform="translate(6.5,6) scale(0.060546875)"><path d="M32 384h272v32H32zM400 384h80v32h-80zM384 447.5c0 17.949-14.327 32.5-32 32.5-17.673 0-32-14.551-32-32.5v-95c0-17.949 14.327-32.5 32-32.5 17.673 0 32 14.551 32 32.5v95z"></path><g><path d="M32 240h80v32H32zM208 240h272v32H208zM192 303.5c0 17.949-14.327 32.5-32 32.5-17.673 0-32-14.551-32-32.5v-95c0-17.949 14.327-32.5 32-32.5 17.673 0 32 14.551 32 32.5v95z"></path></g><g><path d="M32 96h272v32H32zM400 96h80v32h-80zM384 159.5c0 17.949-14.327 32.5-32 32.5-17.673 0-32-14.551-32-32.5v-95c0-17.949 14.327-32.5 32-32.5 17.673 0 32 14.551 32 32.5v95z"></path></g></g></svg>
</div>    
<div id="imageContainer"></div>

Please Note.

Most people who have 6/6 vision (20/20 for imperial countries) will be hard pressed to see the difference between the slightly blurry display of the canvas and the crisp DOM. You should ask yourself, did you need to have a closer look to make sure? can you see the blur at normal viewing distance?

Also some people who have zoomed the display to 200% do so for good reason (vision impaired) and will not appreciate you circumventing their settings.