Cause
Insert a beginPath()
as rect()
adds to the path unlike fillRect()
/strokeRect()
. What happens here is that the rectangles are accumulating eventually slowing the clipping down over time.
This in combination with using setInterval
, which is not able to synchronize with monitor/screen refreshes, worsens the problem.
To elaborate on the latter:
Using setInterval()
/setTimeout()
can cause tearing which happens when the draw is in the "middle" of its operation not fully completed and a screen refresh occur.
setInterval
can only take integer values and you would need for 16.67 ms to synchronize frame-wise (@60Hz). Even if setInterval
could take floats it would not synchronize the timing with the monitor timing as the timer mechanism isn't bound to monitor at all.
To solve this always use requestAnimationFrame
to synchronize drawings with screen updates. This is directly linked to monitor refreshes and is a more low-level and efficient implementation than the other, and is made for this purpose, hence the name.
Solution embedding both fixes above
See modified bin here.
The code for future visitors:
function draw() {
context.fillStyle = '#000';
context.fillRect(0, 0, width, height);
context.save();
context.beginPath(); /// add a beginPath here
context.rect(0, 0, 100, 100);
context.clip();
context.fillStyle = '#ff0000';
context.fillRect(0, 0, 200, 200);
context.restore();
requestAnimationFrame(draw); /// use rAF here
}
canvas.width = width;
canvas.height = height;
canvas.style.width = width+'px';
canvas.style.height = height+'px';
requestAnimationFrame(draw); /// start loop
PS: If you need to stop the loop inject a condition to run rAF all inside the loop, ie:
if (isPlaying) requestAnimationFrame(draw);
There is BTW no need for closePath()
as this will be done implicit for you as by the specs. You can add it if you want, but calling clip()
(or fill()
etc.) will do this for you (specs is addressing browser implementers):
Open subpaths must be implicitly closed when computing the clipping region