'Does Canvas redraw itself every time anything changes?

I have done some research on how canvas works. It is supposed to be "immediate mode" means that it does not remember what its drawing looks like, only the bitmap remains everytime anything changes.

This seems to suggest that canvas does not redraw itself on change.
However, when I tested canvas on iPad (basically I keep drawing parallel lines on the canvas), the frame rate degrades rapidly when there are more lines on the canvas. Lines are drawn more slowly and in a more jumpy way.

Does this mean canvas actually have to draw the whole thing on change? Or there is other reason for this change in performance?



Solution 1:[1]

The HTML canvas remembers the final state of pixels after each stroke/fill call is made. It never redraws itself. (The web browser may need to re-blit portions of the final image to the screen, for example if another HTML object is moved over the canvas and then away again, but this is not the same as re-issuing the drawing commands.

The context always remembers its current state, including any path that you have been accumulating. It is probable that you are (accidentally) not clearing your path between 'refreshes', and so on the first frame you are drawing one line, on the second frame two lines, on the third frame three lines, and so forth. (Are you calling ctx.closePath() and ctx.beginPath()? Are you clearing the canvas between drawings?)

Here's an example showing that the canvas does not redraw itself. Even at tens of thousands of lines I see the same frame rate as with hundreds of lines (capped at 200fps on Chrome, ~240fps on Firefox 8.0, when drawing 10 lines per frame).

var lastFrame = new Date, avgFrameMS=5, lines=0;
function drawLine(){
  ctx.beginPath();
  ctx.moveTo(Math.random()*w,Math.random()*h);
  ctx.lineTo(Math.random()*w,Math.random()*h);
  ctx.closePath();
  ctx.stroke();
  var now = new Date;
  var frameTime = now - lastFrame;
  avgFrameMS += (frameTime-avgFrameMS)/20;
  lastFrame = now;
  setTimeout(drawLine,1);
  lines++;
}
drawLine();

// Show the stats infrequently
setInterval(function(){
  fps.innerHTML = (1000/avgFrameMS).toFixed(1);
  l.innerHTML = lines;
},1000);

Seen in action: http://phrogz.net/tmp/canvas_refresh_rate.html

For more feedback on what your code is actually doing versus what you suspect it is doing, share your test case with us.

Solution 2:[2]

Adding this answer to be more general.

It really depends on what the change is. If the change is simply to add another path to the previously drawn context, then the canvas does not have to be redrawn. Simply add the new path to the present context state. The previously selected answer reflects this with an excellent demo found here.

However, if the change is to translate or "move" an already drawn path to another part of the canvas, then yes, the whole canvas has to be redrawn. Imagine the same demo linked above accumulating lines while also rotating about the center of the canvas. For every rotation, the canvas would have to be redrawn, with all previously drawn lines redrawn at the new angle. This concept of redrawing on translation is fairly self-evident, as the canvas has no method of deleting from the present context. For simple translations, like a dot moving across the canvas, one could draw over the dot's present location and redraw the new dot at the new, translated location, all on the same context. This may or may not be more operationally complex than just redrawing the whole canvas with the new, translated dot, depending on how complex the previously drawn objects are.

Another demo to demonstrate this concept is when rendering an oscilloscope trace via the canvas. The below code implements a FIFO data structure as the oscilloscope's data, and then plots it on the canvas. Like a typical oscilloscope, once the trace spans the width of the canvas, the trace must translate left to make room for new data points on the right. To do this, the canvas must be redrawn every time a new data point is added.

function rand_int(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1) + min); //The maximum is inclusive and the minimum is inclusive
}

function Deque(max_len) {
  this.max_len = max_len;
  this.length = 0;
  this.first = null;
  this.last = null;
}

Deque.prototype.Node = function(val, next, prev) {
  this.val = val;
  this.next = next;
  this.prev = prev;
};

Deque.prototype.push = function(val) {
  if (this.length == this.max_len) {
    this.pop();
  }
  const node_to_push = new this.Node(val, null, this.last);
  if (this.last) {
    this.last.next = node_to_push;
  } else {
    this.first = node_to_push;
  }
  this.last = node_to_push;
  this.length++;
};

Deque.prototype.pop = function() {
  if (this.length) {
    let val = this.first.val;
    this.first = this.first.next;
    if (this.first) {
      this.first.prev = null;
    } else {
      this.last = null;
    }
    this.length--;
    return val;
  } else {
    return null;
  }
};

Deque.prototype.to_string = function() {
  if (this.length) {
    var str = "[";
    var present_node = this.first;
    while (present_node) {
      if (present_node.next) {
        str += `${present_node.val}, `;
      } else {
        str += `${present_node.val}`
      }
      present_node = present_node.next;
    }
    str += "]";
    return str
  } else {
    return "[]";
  }
};

Deque.prototype.plot = function(canvas) {
  const w = canvas.width;
  const h = canvas.height;
  const ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, w, h);
  //Draw vertical gridlines
  ctx.beginPath();
  ctx.setLineDash([2]);
  ctx.strokeStyle = "rgb(124, 124, 124)";
  for (var i = 1; i < 9; i++) {
    ctx.moveTo(i * w / 9, 0);
    ctx.lineTo(i * w / 9, h);
  }
  //Draw horizontal gridlines
  for (var i = 1; i < 10; i++) {
    ctx.moveTo(0, i * h / 10);
    ctx.lineTo(w, i * h / 10);
  }
  ctx.stroke();
  ctx.closePath();
  if (this.length) {
    var present_node = this.first;
    var x = 0;
    ctx.setLineDash([]);
    ctx.strokeStyle = "rgb(255, 51, 51)";
    ctx.beginPath();
    ctx.moveTo(x, h - present_node.val * (h / 10));
    while (present_node) {
      ctx.lineTo(x * w / 9, h - present_node.val * (h / 10));
      x++;
      present_node = present_node.next;
    }
    ctx.stroke();
    ctx.closePath();
  }
};

const canvas = document.getElementById("canvas");
const deque_contents = document.getElementById("deque_contents");
const button = document.getElementById("push_to_deque");
const min = 0;
const max = 9;
const max_len = 10;

var deque = new Deque(max_len);

deque.plot(canvas);

button.addEventListener("click", function() {
  push_to_deque();
});

function push_to_deque() {
  deque.push(rand_int(0, 9));
  deque_contents.innerHTML = deque.to_string();
  deque.plot(canvas);
}
body {
  font-family: Arial;
}

.centered {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
}
<div class="centered">
  <p>Implementation of a FIFO deque data structure in JavaScript to mimic oscilloscope functionality. Push the button to push random values to the deque object. After the maximum length is reached, the first item pushed in is popped out to make room for the next value. The values are plotted in the canvas. The canvas must be redrawn to translate the data, making room for the new data.
  </p>
  <div>
    <button type="button" id="push_to_deque">push</button>
  </div>
  <div>
    <h1 id="deque_contents">[]</h1>
  </div>
  <div>
    <canvas id="canvas" width="800" height="500" style="border:2px solid #D3D3D3; margin: 10px;">
    </canvas>
  </div>
</div>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 jacob