HTML5 Canvas - Redrawing new circles after erasing them with clip - javascript

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).

Related

ctx.clearRect canvas sprite

I am wondering how I could alter my Javascript to only clear the falling sprites, and not the entire canvas (as it does currently).
I hope to place multiple other (animated) sprites on the canvas, which do not appear with the way my function animate is structured.
Is there a way so that if there was another image/sprite was on the canvas, it would not be affected by the function animate.
I'm thinking that this line needs to change:
ctx.clearRect(0, 0, canvas.width, canvas.height);
Though I have no idea what parameters I would need to place inside.
The falling sprites draw at a size of 60x60, but as they fall downwards this is where I am a bit stuck with clearing the only the sprite path.
Any help would be appreciated :)
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d");
canvas.width = 1408;
canvas.height = 640;
canvasWidth = canvas.width;
canvasHeight = canvas.height;
var orangeEnemy = new Image();
orangeEnemy.src = "http://www.catholicsun.org/wp-content/uploads/2016/09/cropped-sun-favicon-512x512-270x270.png";
var yellowEnemy = new Image();
yellowEnemy.src = "http://www.clker.com/cliparts/o/S/R/S/h/9/transparent-red-circle-hi.png";
var srcX;
var srcY;
var enemySpeed = 2.75;
var images = [orangeEnemy, yellowEnemy];
var spawnLineY=-50;
var spawnRate=2500;
var spawnRateOfDescent=1.50;
var lastSpawn=-1;
var objects=[];
var startTime=Date.now();
animate();
function spawnRandomObject() {
var object = {
x: Math.random() * (canvas.width - 15),
y: spawnLineY,
image: images[Math.floor(Math.random() * images.length)]
}
objects.push(object);
}
function animate(){
var time=Date.now();
if(time>(lastSpawn+spawnRate)){
lastSpawn=time;
spawnRandomObject();
}
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// move each object down the canvas
for(var i=0;i<objects.length;i++){
var object=objects[i];
object.y += enemySpeed;
ctx.drawImage(object.image, object.x, object.y, 60, 60);
}
}
<html>
<canvas id="canvas" style="border:3px solid"></canvas>
</html>
The easiest and quickest way would be to overlay another canvas, specifically for your sprites, atop your current canvas (requires a bit of CSS). Put all your sprites in one, everything else in the other. The clearRect() in your animate() function will then only apply to your sprite canvas, and not the other.
Otherwise, you will have to keep track of the positions of the sprites, and clear each programatically with 60x60 rectangles using clearRect(offsetX, offsetY, 60, 60).
P.S. excuse the non-formatted answer... still figuring SO out
Clear once for performance.
You are much better off clearing the whole canvas and redrawing the sprites. Using the previous position, checking for overlap and then clearing each sprite in turn, making sure you don't clear an existing sprite will take many more CPU cycles than clearing the screen once.
The clear screen function is very fast and is done in hardware, the following is the results of a performance test on Firefox (currently the quickest renderer) of clearing 65K pixels using just one call for whole area then 4 calls each a quarter, then 16 calls each clearing a 16th. (µs is 1/1,000,000th second)
Each test clears 256*256 pixels Each sample is 100 tests
'Clear 1/1' Mean time: 213µs ±4µs 1396 samples
'Clear 4/4' Mean time: 1235µs ±14µs 1390 samples
'Clear 16/16' Mean time: 4507µs ±42µs 1405 samples
As you can see clearing 65K pixels is best done in one call with the actual javascript call adding about 100µs to do.
On Firefox the number of pixels to clear does not affect the execution time of the call to clearRect with a call clearRect(0,0,256,256) and clearRect(0,0,16,16) both taking ~2µs
Apart from the speed, trying to clear overlapping animated sprites per sprite becomes extremely complicated and can result in 100s of clear calls with only a dozen sprites. This is due to the overlapping.

How to change radius of drawn ellipse in p5.js?

I've been learning how to use the p5.js library and got stuck when making a simple sketch. The goal of the code is to have a slider that changes the radius of the drawn ellipse. The slider works when increasing the value, but when decreasing it, the biggest ellipse that was drawn covers up the smaller ones. This makes sense, as an ellipse is constantly being drawn. But is there a way to have just 1 ellipse and change its radius with the slider?
Here's the awful code I managed to put together that works.
JS:
var spr;
var slider;
function setup() {
createCanvas(500,500);
background(51);
spr = new Sphere();
slider = createSlider(0, width, 1);
slider.position(6, 6);
slider.style('width','500px');
}
function draw() {
var val = slider.value();
spr.show(val);
}
function Sphere() {
this.x = width/2;
this.y = height/2;
this.show = function(val){
noStroke();
fill(100,0,250);
ellipse(this.x, this.y, val, val);
}
}
Here is a picture of how it looks like in the browser.
Before asking, I'd searched for an answer but couldn't find one. It's probably really simple and I've just missed something very important.
You're never clearing out old frames, so everything you draw stays on the screen. That's why your largest drawing is always covering up your smaller drawings.
To clear out old screens, just add a call to the background() function as the first line in the draw() function.
More info can be found in the reference.

Canvas - Fill a shape created with multiple paths

What I want to do
I would like to draw a custom shape (for example a simple rectangle) which has different colors for each edge. I can do it with four paths, it works like a charm. BUT, in this way, it seems I can not fill the shape.
Trying the other way, I can draw the shape with one path and fill it, BUT in this case, I can not use different colors for the edges, because the last fillStyle will override the previous ones, even if I stroke the subpaths individually.
Is it possible to mix the two, by coloring subpaths individually, or by filling a shape consisting multiple paths?
Use different "layers" on the canvas, one for the filled with color shape, and a new one for each color path you have, z-index doesn't work on canvas, just make sure you draw what goes underneath first, and just wrap everything on a group <g> tag to make it easier to manipulate
After some experiment, I managed to solve my problem. It is not an ideal solution, because it has some overhead, but it works fine.
In the beginning of the drawing operation, I store the target coordinates in an array, and draw the whole stuff again and again. Each run is a new path. With .globalCompositeOperation = "destination-over" I can draw the lines under the existing ones, so each line can have a different color.
At the end of the drawing operation, the array contains all the coordinates of the shape, so the .fill() method can fill the path.
I hope it can help others:
// get the canvas context
var ctx = document.getElementById("myCanvas").getContext("2d");
// init shape array
var shape = [];
shape.push({
x: 0,
y: 0
}); // or any other starting point
// let's try
draw(20, 20);
draw(40, 40);
draw(60, 60);
// this is how we draw
function draw(x, y) {
// this is important
// see: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
ctx.globalCompositeOperation = "destination-over";
// this is just to be more visible
ctx.lineWidth = 10;
// get a random color
ctx.strokeStyle = myRandomColor();
// save target coordinates
shape.push({
x: x,
y: y
});
// reset the path
ctx.beginPath();
// jump to the start point
ctx.moveTo(shape[0].x, shape[0].y);
// draw the whole stuff
for (var i = 0; i < shape.length; i++) {
ctx.lineTo(shape[i].x, shape[i].y);
}
ctx.stroke();
}
function myRandomColor() {
var colors = ["red", "green", "blue", "yellow", "pink"];
var rand = Math.round(Math.random() * 5);
return colors[rand];
}
<canvas id="myCanvas"></canvas>

how can we take advantage of beginPath() canvas method?

please I need your help I am new to HTML5 and I am a little confused , I have written the following code in my editor
var canvas = document.getElementById('paper');
var c= canvas.getContext('2d');
c.beginPath();
c.lineWidth="5";
c.moveTo(0,75);
c.lineTo(250,75);
c.stroke(); // Draw it
c.beginPath();
c.lineWidth="5";
c.moveTo(0,0);
c.lineTo(250,75);
c.stroke(); // Draw it
but when I removed the second c.beginPath() , no thing changed!!!
so how can we take the advantage of beginPath() method.
can any one explain for me using clear example.
thank you so much every body.
Here's the solution to the mystery :
• beginPath creates a new path.
• moveTo creates a new sub-path within the current path.
So when using two times beginPath, you are drawing two lines.
When using beginPath only once, you draw one single figure that contains two sub-path that are lines.
The principle of sub-path allows you to build whatever you want to fill/stroke as you want, then stroke all those sub-path at once.
You can use the way you prefer.
About style : when using fill or stroke, the current path will be drawn in the current style ( fillStyle / strokeStyle / lineWidth / font / ...).
So you are obliged, to draw with a different style, to create a new path with beginPath.
On the other hand, if you are drawing a lot of figures with the same style, it is more logical to set the style once, create all the sub-paths, and fill/stroke everything.
Rq : it is a good habit when drawing to :
1) set your style
2) build your path / sub-paths,
3) then fill and/or stroke.
Because mixing styles, paths and strokes/fills, will just confuse things.
Edit : When you come to more complex drawings, you have to change also the transform : you scale, rotate and translate.
It can become quite hard to know the current status of the canvas.
In fact, even when dealing only with regular style only, it might be difficult to both avoid setting everything on each draw AND know what is the current setting.
The solution is to save the context before your draw, and restore it afterwise :
1) save context
2) set style
3) set transform
4) build path / sub- path
5) restore context.
here's a simple-but-not-too simple example :
function drawSmile(ctx, x, y, faceRadius, eyeRadius) {
ctx.save(); // save
ctx.fillStyle = '#FF6'; // face style : fill color is yellow
ctx.translate(x, y); // now (x,y) is the (0,0) of the canvas.
ctx.beginPath(); // path for the face
ctx.arc(0, 0, faceRadius, 0, 6.28);
ctx.fill();
ctx.fillStyle = '#000'; // eye style : fill color is black
ctx.beginPath(); // path for the two eyes
ctx.arc(faceRadius / 2, - faceRadius /3, eyeRadius, 0, 6.28);
ctx.moveTo(-faceRadius / 2, - faceRadius / 3); // sub path for second eye
ctx.arc(-faceRadius / 2, - faceRadius / 3, eyeRadius, 0, 6.28);
ctx.fill();
ctx.restore(); // context is just like before entering drawSmile now.
}
drawSmile(ctx, 100, 100, 60, 12);
.
For the record, code to draw Two lines :
c.lineWidth="5";
c.beginPath(); // new path
c.moveTo(0,75);
c.lineTo(250,75);
c.stroke(); // Draw it
c.lineWidth="10";
c.beginPath(); // new path
c.moveTo(0,0);
c.lineTo(250,75);
c.stroke(); // Draw it
One path having two lines as sub-paths :
c.lineWidth="5";
c.beginPath(); // new path
c.moveTo(0,75);
c.lineTo(250,75);
c.moveTo(0,0); // new sub-path within current path
c.lineTo(250,75);
c.stroke(); // Draw the two lines at once.

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