htmlgoogle-chromecanvassubpixel

No subpixel positioning on small HTML5 canvas on chrome


When animating an image over a large canvas, the image renders correctly on non-integer coordinates, and the animation is smooth. on a small canvas, say 200x200, the subpixel coordinates don't apply, and the image "jumps" from integer location to the next, creating a "jittery" motion.

the issue seems to apply to raster sources only (images and canvases). text, for instance, animates smoothly on all canvas sizes.

i'm currently testing with Chrome Version 58.0.3029.110 (64-bit), however the issue appeared on earlier versions as well.

has anyone stumbled upon this issue?

here's the code i test with:

<!DOCTYPE HTML>
<html>
<head>
</head>
<body>
<script>
    var outer = [200, 200];
    var inner = [200, 200];

    function CreateCanvas(w, h, hidden) {
        var canvas = document.createElement('canvas');
        if(!hidden) document.body.appendChild(canvas);
        canvas.width = w;
        canvas.height = h;
        var context = canvas.getContext('2d');
        return {canvas:canvas, context:context};
    }

    function rgba2hex(color) {
        return "rgba(" + Math.floor(color[0] * 255) + ',' + Math.floor(color[1] * 255) + ',' + Math.floor(color[2] * 255) + ',' + color[3] + ")";
    }

    function GetSystemTimeMS() {
        return (new Date()).getTime();
    }

    function GetTimeDifferenceMS(time) {
        return GetSystemTimeMS() - time;
    }

    var outerFontSize = Math.min(100, outer[1] * 0.3);
    var innerFontSize = Math.min(100, inner[1] * 0.3);

    var outerBuffer = CreateCanvas(outer[0], outer[1], false);
    outerBuffer.context.font = outerFontSize + "px times";
    outerBuffer.context.fillStyle = rgba2hex([0,0,0,1]);    

    var innerBuffer = CreateCanvas(inner[0], inner[1], true);
    innerBuffer.context.font = innerFontSize + "px times";
    innerBuffer.context.fillStyle = rgba2hex([0,0,0,1]);
    innerBuffer.context.fillText("raster", 10, inner[1] * 0.9);

    var startTime = GetSystemTimeMS();
    function draw() {
        var span = 5;
        var phase = ((GetTimeDifferenceMS(startTime) / 1000) % span) / span;
        outerBuffer.context.clearRect(0, 0, outer[0], outer[1]);
        var x = 50 + phase * 20;

        outerBuffer.context.fillText("vector", x, outer[1] * 0.5);
        outerBuffer.context.drawImage(innerBuffer.canvas, x, 0);

        window.setTimeout(draw, 10);
    }
    draw();
</script>
</body>
</html>

Solution

  • I can definitely reproduce it on both my stable chrome and on my canary.
    I reported to the chromium team. Let's hope a fix will come soon enough.

    For a workaround, you can shrink a little bit your images (minimum value I found was size * 0.99. This should force the antialiasing algorithm to kick in.

    var outer = [200, 200];
    var inner = [200, 200];
    
    function CreateCanvas(w, h, hidden) {
      var canvas = document.createElement('canvas');
      if (!hidden) document.body.appendChild(canvas);
      canvas.width = w;
      canvas.height = h;
      var context = canvas.getContext('2d');
      return {
        canvas: canvas,
        context: context
      };
    }
    
    function rgba2hex(color) {
      return "rgba(" + Math.floor(color[0] * 255) + ',' + Math.floor(color[1] * 255) + ',' + Math.floor(color[2] * 255) + ',' + color[3] + ")";
    }
    
    function GetSystemTimeMS() {
      return (new Date()).getTime();
    }
    
    function GetTimeDifferenceMS(time) {
      return GetSystemTimeMS() - time;
    }
    
    var outerFontSize = Math.min(100, outer[1] * 0.3);
    var innerFontSize = Math.min(100, inner[1] * 0.3);
    
    var outerBuffer = CreateCanvas(outer[0], outer[1], false);
    outerBuffer.context.font = outerFontSize + "px times";
    outerBuffer.context.fillStyle = rgba2hex([0, 0, 0, 1]);
    
    var innerBuffer = CreateCanvas(inner[0], inner[1], true);
    innerBuffer.context.font = innerFontSize + "px times";
    innerBuffer.context.fillStyle = rgba2hex([0, 0, 0, 1]);
    innerBuffer.context.fillText("raster", 10, inner[1] * 0.9);
    
    var startTime = GetSystemTimeMS();
    
    function draw() {
      var span = 5;
      var phase = ((GetTimeDifferenceMS(startTime) / 1000) % span) / span;
      outerBuffer.context.clearRect(0, 0, outer[0], outer[1]);
      var x = 50 + phase * 20;
    
      outerBuffer.context.fillText("vector", x, outer[1] * 0.5);
      // shrink a little bit our image
      outerBuffer.context.drawImage(innerBuffer.canvas, x, 0, 200 * 0.99, 200 * 0.99);
    
      requestAnimationFrame(draw);
    }
    draw();