4

I just started playing with the HTML5 canvas and I was hoping to make a couple games with it. However, as soon I started rendering the mouse coordinates to it, it grinded to a near halt:

http://jsfiddle.net/mnpenner/zHpgV/

All I did was render 38 lines and some text, it should be able to handle that, no?

Am I doing something wrong? I'd like to be able to render at least 30 FPS, but for something like this I would expect it to be able to draw 1000s of times.

Or am I just using the wrong tool for the job? Is WebGL up for the task? Why would one be so much slower than the other?

String.prototype.format = function() {
    var args = arguments;
    return this.replace(/\{(\d+)\}/g, function(m, n) {
        return args[n];
    });
};
var $canvas = $('#canvas');
var c = $canvas[0].getContext('2d');
var scale = 20;
var xMult = $canvas.width() / scale;
var yMult = $canvas.height() / scale;
var mouseX = 0;
var mouseY = 0;
c.scale(xMult, yMult);
c.lineWidth = 1 / scale;
c.font = '1pt Calibri';

function render() {
    c.fillStyle = '#dcb25c';
    c.fillRect(0, 0, scale, scale);
    c.fillStyle = '#544423';
    c.lineCap = 'square';
    for (var i = 0; i <= 19; ++i) {
        var j = 0.5 + i;
        c.moveTo(j, 0.5);
        c.lineTo(j, 19.5);
        c.stroke();
        c.moveTo(0.5, j);
        c.lineTo(19.5, j);
        c.stroke();
    }
    c.fillStyle = '#ffffff';
    c.fillText('{0}, {1}'.format(mouseX, mouseY), 0.5, 1.5);
}
render();
$canvas.mousemove(function(e) {
    mouseX = e.clientX;
    mouseY = e.clientY;
    render();
});
<canvas id="canvas" width="570" height="570"></canvas>
mpen
  • 272,448
  • 266
  • 850
  • 1,236

4 Answers4

11

Here's the code made much better.

http://jsfiddle.net/zHpgV/3/

Here's a breakdown of the things that you should take into consideration that I changed:

  • Continuous adding to a path instead of stopping and creating a new path with beginPath. This is by far the biggest performance killer here. You're ending up with a path with thousands and thousands of line segements that never gets cleared.
  • Continuously making the same path over and over when it only needs to be made once, on initialization. That is, the only thing you need to call inside of render is stroke. You do not need to call lineTo/moveTo ever again, and certainly not continuously. See note 1.
  • Stroking twice for one path
  • Stroking inside a for loop
  • Redrawing a background instead of setting CSS background
  • Setting the line cap over and over

Note 1: If you plan to have more than one path in your application then you should probably cache paths like this one since they never change. I have a a tutorial on how to do that here.

Of course, if you are doing all of this to just make a background, it should be saved as a png and you should be using a CSS background-image.

Like so: http://jsfiddle.net/zHpgV/4/

Then suddenly your render routine is rather small:

function render() {
    c.clearRect(0, 0, scale, scale);
    c.fillText('{0}, {1}'.format(mouseX, mouseY), 0.5, 1.5);
}
Simon Sarris
  • 62,212
  • 13
  • 141
  • 171
  • I didn't know paths worked that! I think having a `path` object would have been more intuitive. Makes sense why it was so slow now though, thank you! – mpen Jun 23 '12 at 16:18
  • 3
    There is now a path object in the HTML5 Canvas specification and you will be able to make a Path and call `drawPath` in the future. But no browser has implemented it yet and it may be months before you'll be able to make use of it. Buy someday! – Simon Sarris Jun 23 '12 at 17:18
  • I am using a canvas produced with adobe cc. So I couldn't see anything like '$canvas[0].getContext('2d');'. Where can I add 'beginPath' command to ? – Nurullah Macun Mar 13 '20 at 08:46
  • @SimonSarris could you pls help me for this : https://stackoverflow.com/questions/63873735/canvas-drawing-is-very-slow – micronyks Sep 16 '20 at 05:25
  • The link to your tutorial leads to 404. Could you update it pleases? – Meglio Nov 21 '21 at 04:10
9

As i said in comments I was surprised by the slowness of this code as I draw much much more complex things with very fast animations without even bothering about double buffering.

So I looked a little more and found a bug as expected.

The main problem is the accumulation of the drawing path.

Add a c.beginPath(); each time you draw one path.

Here's a fast rendering of the same thing, to prove it now flies.

Canvas drawing is fast and can be used for animations.

Denys Séguret
  • 372,613
  • 87
  • 782
  • 758
7

You don't have to draw the whole grid in every animation frame. Put it on another underlying canvas (it is common to call them “layers”, but they are just separate canvas elements), so you'll be able to redraw coordinates only.

<div id="canv">
 <canvas id="bgLayer" width="500" height="500" style="z-index: 0"></canvas>
 <canvas id="fgLayer"  width="500" height="500" style="z-index: 1"></canvas>
</div>

Here is the example I've been playing with layered canvas. The table drawn on the bottom canvas, balls are drawn on the top canvas. It's just a playground, so there is a lot to fix and optimize there, for example to draw every ball only once on another hidden canvas and use getImageData/putImageData to improve performance.

Also, it is recommended to use requestAnimationFrame to update the canvas. Your example draws on every mouse movement instead, this is a lot more often then needed (when mouse moves of course).

There is a good article on improving canvas performance. Also, there is a great SO post on this subject.

Community
  • 1
  • 1
Artem Koshelev
  • 10,548
  • 4
  • 36
  • 68
  • Took me awhile to figure out you literally meant layer the canvas elements. I thought "layers" were a concept within the canvas context. That's a good idea. Thanks for the tips! – mpen Jun 22 '12 at 07:00
  • Sorry, I changed it to avoid confusing someone else. – Artem Koshelev Jun 22 '12 at 09:41
  • 1
    Those are good comment but there is another problem I don't find in this case. I draw much much more complex things with very fast animations without even bothering about double buffering. – Denys Séguret Jun 22 '12 at 17:32
  • i am stuck with canvas.my animation is not working with canvas.Can you please help me with it.This is my question:http://stackoverflow.com/questions/37208156/update-html-canvas-tag-on-every-ajax-request-with-new-data – I Love Stackoverflow May 13 '16 at 13:20
1

The problem that I ran into was different than the answers listed here. Can you see the problem?

Bad code

  const drawSegment = (key: number, lastAngle: number, angle: number) => {
    const ctx = canvasContext!;
    const value = segments[key];
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(centerX, centerY);
    ctx.arc(centerX, centerY, size, lastAngle, angle, false);
    ctx.lineTo(centerX, centerY);
    ctx.closePath();
    ctx.fillStyle = segColors[key];
    ctx.fill();
    ctx.stroke();
    ctx.save();
    ctx.translate(centerX, centerY);
    ctx.rotate((lastAngle + angle) / 2);
    ctx.fillStyle = contrastColor;
    ctx.font = "bold 2em " + fontFamily;
    ctx.fillText(value.substring(0, 21), size / 2 + 20, 0);
    ctx.restore();
  };

The problem with this sample code is there are 2 calls to ctx.save() and only one call to ctx.restore(). This was tough to debug because it would work fine and then all of the sudden, it would slow down considerably.

ctx.save() creates a new entry on a stack and ctx.restore() pops it from the stack. So if you have a stack that grows infinitely large over time, it will eventually hit a limit and the browser will slow down.

More info: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save

Fixed

  const drawSegment = (key: number, lastAngle: number, angle: number) => {
    const ctx = canvasContext!;
    const value = segments[key];
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(centerX, centerY);
    ctx.arc(centerX, centerY, size, lastAngle, angle, false);
    ctx.lineTo(centerX, centerY);
    ctx.closePath();
    ctx.fillStyle = segColors[key];
    ctx.fill();
    ctx.stroke();
    // ctx.save(); <-- get rid of this line of code!
    ctx.translate(centerX, centerY);
    ctx.rotate((lastAngle + angle) / 2);
    ctx.fillStyle = contrastColor;
    ctx.font = "bold 2em " + fontFamily;
    ctx.fillText(value.substring(0, 21), size / 2 + 20, 0);
    ctx.restore();
  };
PaulMest
  • 12,925
  • 7
  • 53
  • 50