Canvas - Fill a shape created with multiple paths - javascript

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>

Related

How to check if mouse position is inside HTML5 Canvas path?

I'm new to HTML5 Canvas and Javascript. I'm currently working on a HTML5 Canvas project (I wrote a separate question about it here and here).
The logic of the game is pretty simple: track the mouse position and if the mouse position exits the blue region, the game resets. The user starts on level 1 (function firstLevel). If their mouse position enters the red box region, they advance onto the next level (return secondLevel()). I did this previously by a hefty list of if statements that compared the mouse's x and y coordinates.
I've corrected the code to create a global constructor function I can reference in each of the level functions:
function Path(array, color) {
let path = new Path2D();
path.moveTo(array[0][0], array[0][1]);
for (i = 1; i < array.length; i++) {
path.lineTo(array[i][0], array[i][1]);
}
path.lineTo(array[0][0], array[0][1]);
return path;
}
And its referenced in the level functions as such:
// In the individual levels, put code that describes the shapes locally
var big = [[350,200], [900,200], [900,250], [700,250], [600,250], [600,650], [350,650]];
var blue = Path(big);
var small = [[900,200], [900,250], [850,250], [850, 200], [900, 200]];
var red = Path(small);
// 2. create cursor
c.beginPath();
c.rect(mouseX, mouseY, mouseWidth, mouseHeight);
c.fillStyle = "#928C6F";
c.fill();
// Local code that draws shapes:
c.beginPath()
c.fillStyle = '#C1EEFF';
c.fill(blue);
c.beginPath()
c.fillStyle = "#FF4000";
c.fill(red);
I want to know how can I check for the mouse position so it can run those conditional statements using the isPointInPath method? I now have a reference (refs. blue and red) for the Canvas shape, so I'm hoping there's a way I can check if the mouse's x and y position is a point that is inside the shape/path.
Link to my project: https://github.uconn.edu/pages/ssw19002/dmd-3475/final-project/maze-page-1.html
Source code: https://github.uconn.edu/ssw19002/dmd-3475/tree/master/final-project
You could use the MouseEvent.offsetX and MouseEvent.offsetY properties of a mouse event such as mousemove. (You would need to add an event listener for mouse events to the canvas element of course.)
Then use the x and y offset values obtained to call isPointInPath like
ctx.isPointInPath(path, x, y)
where path is a return value from function Path.
While MDN lists the offset properties as "experimental" they seem to have been around since IE9 at least.

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

Html5 canvas graph fill on hover

I'm drawing a graph on a html 5 canvas tag from a array with numbers like
arr = [6,3,16,6,53,1,3,54,67,6,3,21,6,49,7,8,31,66,51,32,56,49,4,78,9,65,43,1,3,54,67,6,3];
These numbers will be the height of the rectangle that is drawn on the canvas and it will be filled white with a transparent background;
for (var i = 0; i < arr.length; i += 1) {
ctx.fillStyle = "#ffffff";
// Fill rectangle with gradient
ctx.fillRect(
arr[i] * 10,
c_height - arr[i],
8,
arr[i]
);
}
Users can hover these rectangles and then see some more data.
I can make them change color but if there are to many rectangles the site laggs a little bit, so my question is if it is possible to make some kind of big horizontal rectangle that will mask(white rectangles) without filling the transparent background?
1) You can define the array as a typed array instead:
var arr = new Uint8Array([6,3,16,6,53,1,3,...,3]);
Just make sure the type (here unsigned 8-bit) fits the values. If you have higher values than 255 then use a 16-bit, or 32-bit, if floating point use Float32Array and so on.
2) If the color is the same don't set the fill style inside the loop. fillStyle is rather expensive as it has to parse the string and convert it to the color it defines.
3) use path to add the rectangle to, defining and filling each time is slower than to define all rects, then fill all at the same time outside the loop.
4) use a smarter for-loop by using the array entry as a conditional statement as well. Not only is this faster in itself but by storing the array entry to a value will be faster too as JS does not have to look up an array entry every time you use arr[i]:
ctx.fillStyle = "#ffffff"; // set fill style outside loop
ctx.beginPath(); // make sure we use a clean path
for (var i = 0, a; a = arr[i]; i++) { // fetch item and use as cond. for loop
ctx.rect(a * 10, c_height - a, 8, a); // add rect to path, but not fill yet
}
ctx.fill(); // fill all rects with fillstyle
Hope this helps!

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 simulate z-index in canvas

I have asked a question before: How can I control z-index of canvas objects? and we reached to a solution that may not be a good one for complicated situations.
I found that canvas doesn't have a z-index system, but a simple ordered drawing one. Now there is a new question: how can I simulate z-index system to make this problem fixed in complicated situations?
The good answer can solve a big problem.
It's not that canvas doesn't have a z-index, it's that canvas doesn't keep objects drawn contrary to the HTML page. It just draws on the pixel matrix.
There are basically two types of drawing models :
object ones (usually vector) : objects are kept and managed by the engine. They can usually be removed or changed. They have a z-index
bitmap ones : there are no objects. You just change a pixel matrix
The Canvas model is a bitmap one. To have objects drawn over other ones, you must draw them after. This means you must manage what you draw.
The canvas model is very fast, but if you want a drawing system managing your objects, maybe you need SVG instead.
If you want to use a canvas, then the best is to keep what you draw as objects.
Here's an example I just made : I keep a square list and every second I randomize their zindex and redraw them :
var c = document.getElementById('c').getContext('2d');
function Square(x, y, s, color) {
this.x = x; this.y = y; this.s = s; this.color = color;
this.zindex=0;
}
Square.prototype.draw = function(c) {
c.fillStyle = this.color;
c.fillRect(this.x, this.y, this.s, this.s);
}
var squares = [
new Square(10, 10, 50, 'blue'), new Square(40, 10, 40, 'red'), new Square(30, 50, 30, 'green'),
new Square(60, 30, 40, '#111'), new Square(0, 30, 20, '#444'), new Square(70, 00, 40, '#999')
];
function draw() {
c.fillStyle = "white";
c.fillRect(0, 0, 1000, 500);
for (var i=0; i<squares.length; i++) squares[i].draw(c);
}
setInterval(function(){
// give all squares a random z-index
squares.forEach(function(v){v.zindex=Math.random()});
// sort the list accordingly to zindex
squares.sort(function(a,b){return a.zindex-b.zindex});
draw();
}, 1000);
Demonstration
The idea is that the square array is sorted accordingly to zindex. This could be easily extended to other types of objects.
As dystroy has said, z-index is, at its simplest, just an index to tell you in what order to draw things on the canvas, so that they overlap properly.
If you mean to do more than this, say to replicate the existing workings of a browser, then you would have more work to do. The order in which objects are drawn in a browser is a complicated calculation that is driven by:
The DOM tree
Elements' position attributes
Elements' z-index attributes
The canonical source to this is the Elaborate description of Stacking Contexts, part of the CSS specification.

Categories

Resources