2

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>
Lunch
  • 35
  • 5
  • Welcome to Stack Overflow - this site is about programming issues, if you won't provide any code or at least a link it is hard to discuss the problem and this is not the right site for it please read [ask] – Picard Jun 02 '17 at 08:19
  • 1
    thanks for clarifying @Picard. added code. – Lunch Jun 02 '17 at 08:41
  • 1
    Interestingly, I filled [this bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1322125) for Firefox 6 months ago. Looks like chrome implemented it ;-) – Kaiido Jun 02 '17 at 08:46
  • Ps : if you are looking for a workaround, you could probably draw on a bigger off-screen canvas to get the antialiasing on, and then draw it back on your smaller canvas. And if you want a fix, then file a bug report. – Kaiido Jun 02 '17 at 09:00
  • Actually it does it even on quite large canvases on my chromes. So for the workaround, you would have to force the antialiasing by shrinking your image : `ctx.drawImage(source, x, y, source.width*0.99, source.height*0.99)`. And I suspect this has ben caused by the introduction of `ctx.imageSmoothingQuality` – Kaiido Jun 02 '17 at 09:31
  • @Kaiido, your workaround for forcing anti aliasing by introducing some minor scaling is awesome! thanks a ton! :-) – Lunch Jun 02 '17 at 10:31

1 Answers1

2

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();
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Thanks @Kaiido! i saw your bug report and the response from the chromium team. just noting here for anyone else reading, they said it's because of small canvases not being handled by the GPU. unless someone has another trick for forcing anti aliasing where the browser decides not to apply any, i think this issue done until that behaviour is changed on the browsers. – Lunch Jun 02 '17 at 20:59
  • Side question, is it more performant to draw multiple objects to a hidden canvas, then draw that canvas to the element canvas? As you are here. – AaronLS May 09 '21 at 06:50
  • 1
    @AaronLS That depends what's being drawn on the buffer canvas. If you have something that doesn't get updated often and takes a lot of times to render (like many texts or filters etc.) Then yes using a buffer is the way to go. But here, it's just OP's code, that they made to demonstrate the bug only applied on bitmaps and not when drawing directly using the fillText method. – Kaiido May 09 '21 at 09:07