Why does javascript canvas2d clipping require a path? - javascript

I was pulling my hair out over this bug for a while. I wanted to render images in three sections of a canvas, without allowing them to overlap. Basically, I wanted to use canvas.getContext('2d').clip() to keep the images separated. However, the clip only works if I call canvas.getContext('2d').beginPath() after I draw the image.
So this does not work (no clip is applied):
this.draw=function(image, cx, cy, width, height, clip){
var ctx = this.canvas.getContext('2d');
ctx.save();
ctx.rect(clip.x, clip.y, clip.width, clip.height);
ctx.clip();
ctx.fillStyle = "black";
ctx.fillRect(clip.x, clip.y, clip.width, clip.height);
ctx.drawImage(image,cx-width/2,cy-height/2,width,height);
ctx.restore();
return this;
};
But this does:
this.draw=function(image, cx, cy, width, height, clip){
var ctx = this.canvas.getContext('2d');
ctx.save();
ctx.rect(clip.x, clip.y, clip.width, clip.height);
ctx.clip();
ctx.fillStyle = "black";
ctx.fillRect(clip.x, clip.y, clip.width, clip.height);
ctx.drawImage(image,cx-width/2, cy-height/2,width,height);
ctx.beginPath();// <------WITCHCRAFT
ctx.restore();
return this;
};
It was a total accident that I discovered that beginPath() fixes the problem, and I have no idea why. Can anyone explain this to me?

Because clipping requires a path? Perhaps you missed it in the documentation. Here's what MDN documentation says:
The CanvasRenderingContext2D.clip() method of the Canvas 2D API turns the path currently being built into the current clipping path.
(emphasis mine)
The reason it needs a path is because clipping mask can be any arbitrary shape from rectangles to circles to the outline of Pikachu.
For the sake of completeness, here's what the W3C spec says about .clip():
https://www.w3.org/TR/2dcontext/#drawing-paths-to-the-canvas
context . clip()
Further constrains the clipping region to the current path.

Related

Is it possible to not render an object in canvas outside of a given region?

For example context.fillText("foobar",30,0); would render the full text "foobar" 30 pixels down, but how could I keep the rightmost 20 pixels, to throw out a random number, from rendering? One solution for this is to render a white box immediately after to hide the rest of foobar. But this solution isn't compatible with other features I want to incorporate. I need a way to really keep the rest of foobar from rendering in the first place. Is this possible in canvas or would I need to use another graphics API?
.clip() allows you to use paths to form a mask. This, combined with the various path methods, would allow you to draw a clipped version of your text.
An example, from the MDN page, uses a circle to mask a rectangle:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Create circular clipping region
ctx.beginPath();
ctx.arc(100, 75, 50, 0, Math.PI * 2);
ctx.clip();
// Draw stuff that gets clipped
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'orange';
ctx.fillRect(0, 0, 100, 100);
<canvas id="canvas"></canvas>

Canvas clipping region and the canvas stack

I have started working through the O'Reilly book "HTML5 Canvas." I'm in the second chapter, and one of the examples presents code which is not very well explained. Example 2-5:
draw a black box
push state
set small clipping region in top left
draw circle
pop state
set large clipping region
draw another circle
But I'm having trouble understanding some things:
context.fillStyle = 'black';
context.fillRect(10, 10, 200, 200);
context.save();
context.beginPath();
context.rect(0, 0, 50, 50);
context.clip();
context.beginPath();
context.strokeStyle = 'red';
context.lineWidth = 5;
context.arc(100, 100, 100, 0, 2*Math.PI, false);
context.stroke();
context.closePath();
context.restore();
context.beginPath();
context.rect(0, 0, 500, 500);
context.clip();
context.beginPath();
context.strokeStyle = 'blue';
context.lineWidth = 5;
context.arc(100, 100, 50, 0, 2*Math.PI, false);
context.stroke();
context.closePath();
My questions:
First, does context.clip() implicitly close the context path ("context.closePath()")? It is preceded by a context.beginPath(), and followed by another context.beginPath(). Like this:
context.beginPath();
context.rect(0, 0, 50, 50);
context.clip();
context.beginPath();
Second, why is it necessary to push the context state? Why can't I just change the clipping region? It seems to be necessary, because it doesn't work without pushing the state. If I don't push the state and then restore it, the big blue circle does not show up, and I don't understand why.
Does context.clip() implicitly close the context path ?...
It is preceded by a context.beginPath(), and followed by another context.beginPath(). Like this: [...]
Yes, this is only way to create a close shape which is required for clipping so if a closePath() isn't called clip() will close the path internally.
The specification states:
Open subpaths must be implicitly closed when computing the clipping region, without affecting the actual subpaths.
beginPath() will clear the current main path and all its sub-paths. The clipping still resides active though, but now you can do other path operations which will be affected by the clip region when rasterized.
Why is it necessary to push the context state?
There is no way to reset a clip region although it has been suggested and discussed (there is a resetClip() in the standard but not yet widely supported). Calling clip() several times -
The clip() method must create a new clipping region by calculating the intersection of the current clipping region [...]
In other words, it won't be replaced if we say defined a clip region for the entire draw surface.
So the only way we can remove a clip is to save the state, set clip then restore to remove it.

HTML5 Canvas - Redrawing new circles after erasing them with clip

I have a unique problem.
I am creating a game of snake with HTML5 and Canvas
I have a function that generates apples on the board randomly and removes them after a set period of time. In order to remove circles, you have to use the clip() function followed by clearRect().
However, after you use the clip function, you can no longer draw new circles.
The solution I found was using ctx.save() and ctx.restore(). However, if you play the game, you will learn that the snake acts crazy when circles disappear and new circles appear.
I suspect this has to do with my use of the save and restore functions.
Here's the specific code in question
var width = canvas.width;
var height = canvas.height;
var applesArray = []; // Store the coordinates generated randomly
// Generates a random coordinate within the confines of the canvas and pushes it to the apples array
function randCoord() {
var coord = Math.floor(Math.random() * height);
applesArray.push(coord);
return coord;
}
function generateApples() {
ctx.beginPath();
ctx.fillStyle = "green";
ctx.arc(randCoord(),randCoord(),3,0, 2 * Math.PI);
ctx.fill();
ctx.save(); // To redraw circles after clip, we must use save
ctx.clip(); // Allows only the circle region to be erased
setTimeout(function() {
ctx.clearRect(0, 0, width, height);
},3000);
ctx.restore(); // We must restore the previous state.
}
setInterval(function() {
generateApples();
},4000);
You can play the game here
https://jsfiddle.net/2q1svfod/9/
Can anyone explain this weird behavior? I did not see it coming?
The code has multiple issues.
The code that draws the snake (e.g. upArrow function) simply extends the current path. This is a problem because the code that draws the apple starts a new path. Note that save/restore in apple drawing code does not help because path is not part of the state that is saved/restored. The code that draws the snake will need to start a new path. For example...
function upArrow() {
if (direction == "up") {
goUp = setInterval(function() {
ctx.beginPath();
ctx.moveTo(headX, headY);
ctx.lineTo(headX, headY - 10);
ctx.stroke();
headY -= 10;
}, 400);
}
}
The save/clip/restore calls are in the code that draws the apple. These methods need to be moved into the timeout callback function that erases the apple. Also, the code that erases the apple will need to recreate the path (because the snake drawing could have changed the path between when the apple is drawn and when the apple is erased). For example...
function generateApples() {
var cx = randCoord();
var cy = randCoord();
ctx.beginPath();
ctx.fillStyle = "green";
ctx.arc(cx, cy,3,0, 2 * Math.PI);
ctx.fill();
setTimeout(function() {
ctx.beginPath();
ctx.arc(cx,cy,3,0, 2 * Math.PI);
ctx.save();
ctx.clip();
ctx.clearRect(0, 0, width, height);
ctx.restore();
},40000);
}
These changes will get you close to what you intended. However, there will still be a couple minor issues.
When drawing the apple, there will be some anti-aliasing occuring around the edge of the apple's path. The clear operation can miss clearing some of these pixels. After the clear operation, you might see a semi-transparent outline of where the apple was. You could work around this issue by using a slightly larger circle radius when clearing the apple.
Another issue is that apples could be drawn on top of the snake. Erasing the apple will also erase the snake. There is not an easy fix for this issue. You would need to store all the coordinates for the snake and then redraw all or part of the snake.
In the long term, you may want to consider the suggestions in the comments about restructuring your logic to track all objects and redraw everything each frame (or redraw everything after each change).

How do I clip a image obtained from the getImageData() function?

I'm trying to get the contents of a source canvas, clip it, and then draw it on another canvas. Even though my code works like a charm using a src PNG / new Image() combo, it does not when the source content comes from another canvas.
the code is:
var imgData = src_ctx.getImageData(x, y, w, h);
dest_ctx.putImageData(imgData, x, y+h);
ctx.beginPath(); // Filled triangle
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.lineTo(x2,0);
ctx.lineTo(x1,0);
ctx.clip();
After defining the clipping region, draw the source canvas using drawImage, instead of setting the imagedata.
dest_ctx.beginPath(); // Filled triangle
dest_ctx.moveTo(x1,y1);
dest_ctx.lineTo(x2,y2);
dest_ctx.lineTo(x2,0);
dest_ctx.lineTo(x1,0);
dest_ctx.clip();
// You can control wich region to draw using all the arguments
// drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
dest_ctx.drawImage (srcCanvas, x, y);
getImageData is an almost useless function unless you know what you're doing (ie. checking for hit detection, filtering pixels) but even then it is painfully slow.
I created a JSfiddle example for you fiddle around with (see what I did there!)
The heart of the code is as follows:
1 canvas = document.getElementById('canvas');
2 ctx = canvas.getContext("2d");
3 _canvas=document.createElement('canvas');
4 _ctx = _canvas.getContext("2d");
5 _canvas.width = 200;
6 _canvas.height = 200;
7
8 _ctx.beginPath();
9 _ctx.arc(100, 100, 100,0,Math.PI*2,true);
10 _ctx.clip();
11 _ctx.drawImage(img1, 0, 0);
12
13 ctx.drawImage(_canvas, 1.25 * i * _canvas.width, 500);
Essentially what you are doing is clipping to a cache canvas (_canvas, lines 10 and 11) and drawing that to the main canvas (canvas, line 13).
Note: Ideally you would translate your image so it would be in the center of the clip, but I still can not get my head around translations, especially when coupled with other transformations such as clips.

How can I clear an arc or circle in HTML5 canvas?

I found that there's a clearRect() method, but can't find any to clear an arc (or a full circle).
Is there any way to clear an arc in canvas?
There is no clearArc however you can use Composite Operations to achieve the same thing
context.globalCompositeOperation = 'destination-out'
According to MDC the effect of this setting is
The existing content is kept where it doesn't overlap the new shape.
https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Compositing
So any filled shape with this mode on will end up erasing current canvas content.
This is a circular equivalent of clearRect(). The main thing is setting up a composite operation per #moogoo's answer.
var cutCircle = function(context, x, y, radius){
context.globalCompositeOperation = 'destination-out'
context.arc(x, y, radius, 0, Math.PI*2, true);
context.fill();
}
See https://developer.mozilla.org/samples/canvas-tutorial/6_1_canvas_composite.html:
Nope, once you've drawn something on a canvas there is no object to clear, just the pixels you've drawn. The clearRect method doesn't clear a previously drawn object, it just clears the pixels in the space defined by the parameters. You can use the clearRect method to clear the arc if you know a rectangle which contains it. This will of course clear any other pixels in the area, so you'll have to redraw them.
Edit: MooGoo has given a much better answer below
You can use the clearRect() method to erase a portion of the canvas (including your arc), but when you're using clearRect() with arcs or anything else that you used beginPath() and closePath() for while drawing, you'll need to handle the paths while erasing, too. Otherwise, you may end up with a faded version of your arc still appearing.
//draw an arc (in this case, a circle)
context.moveTo(x, y);
context.beginPath();
context.arc(x,y,radius,0,Math.PI*2,false);
context.closePath();
context.strokeStyle = "#ccc";
context.stroke();
//now, erase the arc by clearing a rectangle that's slightly larger than the arc
context.beginPath();
context.clearRect(x - radius - 1, y - radius - 1, radius * 2 + 2, radius * 2 + 2);
context.closePath();
Make sure to call beginPath()
function animate (){
requestAnimationFrame(animate)
c.clearRect(0,0,canvas.width,canvas.height);
c.beginPath();
c.arc(x,y,40,0,Math.PI * 2,false);
c.strokeStyle='rgba(200,0,0,1)';
c.stroke();
c.fillStyle ='rgba(0,0,0,1)';
c.fillRect(x,y,100,100);
x++
} animate()
Credit to #Gabriele Petrioli in this answer: Why doesn't context.clearRect() work inside requestAnimationFrame loop?
Here's an updated fiddle for you too (uses clearRect): https://jsfiddle.net/x9ztn3vs/2/
It has a clearApple function:
block.prototype.clearApple = function() {
ctx.beginPath();
ctx.clearRect(this.x - 6, this.y - 6, 2 * Math.PI, 2 * Math.PI);
ctx.closePath();
}

Categories

Resources