This is the second of a series of learning and reviewing canvas notes. See Canvas Core Technologies for the full notes

Through the learning of canvas core technology – how to draw line segments, we know how to draw line segments. A lot of line splicing forms a graph, such as the common triangle, rectangle, circle and so on.

For drawing common graphics, see my online example: Canvas Shape

Example project repository address: Canvas Demo

graphics

triangle

Let’s see how to draw a triangle. A triangle is made up of three sides, which we can think of as three line segments. The coordinates of the three vertices of the triangle are determined and then connected by lines.

let point1 = [100.30]; / / or 1
let point2 = [50.100]; / / vertex 2
let point3 = [180.120]; / / top 3
ctx.beginPath(); // Start a new path
ctx.moveTo(point1[0], point1[1]); // Move the starting point to vertex 1
ctx.lineTo(point2[0], point2[1]); // connect vertex 1 to vertex 2
ctx.lineTo(point3[0], point3[1]); // connect vertex 2 to vertex 3
ctx.stroke(); / / stroke
// Draw the vertex coordinates to display
ctx.textAlign='center'; // Draw text horizontally centered
ctx.fillText(` (${point1[0]}.${point1[1]}) `, point1[0], point1[1]- 10); // Draw vertex 1 text
ctx.fillText(` (${point2[0]}.${point2[1]}) `, point2[0]- 25, point2[1] +5); // Draw vertex 2 text
ctx.fillText(` (${point3[0]}.${point3[1]}) `, point3[0] +30, point3[1] +5); // Draw vertex 3 text
Copy the code

As you can see from the graph, we still have one edge that is not connected, because we only show two vertices connected. And if we want to connect the third side, we can do it in two ways. The first way is that we show the connection between vertex 3 and vertex 1

// In the first way, connect vertex 3 to vertex 1
ctx.lineTo(point1[0], point1[1]);
Copy the code

In the second way, we call ctx.closepath () to press canvas to automatically connect us to the open path.

// Call ctx.closepath ()
ctx.closePath();
Copy the code

Either way we can implement it and we want triangles. The second method will be used more often, because it will automatically close the current path, that is, make the current path a closed path, which is very useful when filling, as explained below. Finally, we get the triangle as follows

quadrilateral

With three vertices, we can draw a triangle, so with four points, we can certainly draw a quadrilateral, and we can draw a rectangle with four points as usual.

let point1 = [80.30]; //p1
let point2 = [180.30]; //p2
let point3 = [80.110]; //p3
let point4 = [180.110]; //p4
ctx.strokeStyle = 'green'; // Set the stroke color to green
ctx.beginPath(); // Start a new path
ctx.moveTo(point1[0], point1[1]); // Move the starting point to p1
ctx.lineTo(point2[0], point2[1]); // connect P1 to p2
ctx.lineTo(point4[0], point4[1]); // connect P2 to p4
ctx.lineTo(point3[0], point3[1]); // connect p4 to p3
ctx.closePath(); // Close the current path, hermit connect p3 and P1
ctx.stroke(); / / stroke
// Draw vertices
ctx.textAlign = 'center';
ctx.fillText('p1', point1[0] - 10, point1[1] - 10);
ctx.fillText('p2', point2[0] + 10, point2[1] - 10);
ctx.fillText('p3', point3[0] - 10, point3[1] + 10);
ctx.fillText('p4', point4[0] + 10, point4[1] + 10);
Copy the code

Notice that our order is P1 –> P2 –>p4–P3. Since rectangle is a special quadrilateral, there is a method to quickly create a rectangle in canvas. If we know the coordinates of P1 and the width and height of the rectangle, then we can determine the coordinates of the other three points.

// Create a rectangle quickly
ctx.rect(point1[0], point1[1].100.80);
Copy the code

To create rectangles, we always use ctx.rect(left,top,width,height), but to draw a non-rectangular quadrilateral, we still have to draw each point as a line segment.

Circle and arc

A circle can be thought of as an infinite number of small line segments connected, but to draw a circle by fixing the vertex is obviously unrealistic. Canvas in drawing provides a special circular CTX. Arc (left, top, and the radius, startAngle endAngle, antiClockwise). The order of the parameters means the center of the circle X value, the center of the circle Y value, the radius, the starting radian, the ending radian, counterclockwise or not. A complete circle is drawn by specifying startAngle=0 and endAngle= math.pi *2. The last parameter, antiClockwise, can be very useful for filling images, as we’ll talk more about later.

let center = [100.75]; // Center coordinates
let radius = 50; / / radius
let startAngle = 0; // Start radians
let endAngle = Math.PI * 2; // End radian value, 360 degrees = math.pi * 2
let antiClockwise = false; // Whether counterclockwise
ctx.strokeStyle = 'blue'; // Stroke color
ctx.lineWidth = 1;
ctx.arc(center[0], center[1], radius, startAngle, endAngle, antiClockwise);
ctx.stroke(); // Draw the circle stroke
// Draw the center and radius diagram, the reader can ignore the next part of the code
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.arc(center[0], center[1].2, startAngle, endAngle, antiClockwise);
ctx.fill();
ctx.beginPath();
ctx.moveTo(center[0], center[1]);
ctx.lineTo(center[0] + radius, center[1]);
ctx.stroke();
ctx.fillStyle = 'blue';
ctx.font = '24px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('r', center[0] + radius / 2, center[1] - 10);
Copy the code

let center = [50.75]; // Center coordinates
let radius = 20; / / radius
let startAngle = 0; // The initial radian is 0
let antiClockwise = false; // Whether counterclockwise
let angles = [1 / 8.1 / 4.1 / 2.3 / 4]; // Radian length
let colors = ['red'.'blue'.'green'.'orange']; // Stroke color
for (let [i, angle] of angles.entries()) {
  let endAngle = Math.PI * 2 * angle; // Calculate the end radian
  ctx.strokeStyle = colors[i]; // Set the stroke color
  ctx.beginPath(); // Start a new path
  ctx.arc(center[0] + i * radius * 3, center[1], radius, startAngle, endAngle, antiClockwise); // Draw an arc
  ctx.stroke(); / / stroke
}
Copy the code

Arbitrary polygon

The above are some relatively simple and common graphics, how we can draw any polygon, such as pentagons, hexagons, octagons, etc. In fact, as we said when we drew the quadrilateral, we can do this by determining the coordinates of the vertices, and then connecting them in a certain order. Next, to achieve a general polygon drawing method.

class Polygon {
  constructor(ctx, points) {
    this.ctx = ctx;
    this.points = points;
  }
  draw() {
    if (!this.ctx instanceof CanvasRenderingContext2D) {
      throw new Error('Polygon#ctx must be an CanvasRenderingContext2D instance');
    }
    if (!Array.isArray(this.points)) {
      throw new Error('Polygon#points must be an Array');
    }
    if (!this.points.length) {
      return;
    }
    let firstPoint = this.points[0];
    let restPoint = this.points.slice(1);
    ctx.beginPath();
    ctx.moveTo(firstPoint[0], firstPoint[1]);
    for (let point of restPoint) {
      ctx.lineTo(point[0], point[1]); } ctx.closePath(); }}Copy the code

By instantiating the Polygon and passing in the vertex coordinates of the Polygon, we can draw different polygons. For example, the following code draws a pentagon and a hexagon respectively.

// Draw the pentagon
let points = [[30.40], [80.40], [100.80], [55.120], [10.80]].let pentagon = new Polygon(ctx, points);
ctx.strokeStyle = 'blue';
pentagon.draw();
ctx.stroke();

// Draw the hexagon
points = [[160.40], [210.40], [230.80], [210.120], [160.120], [140.80]].let hexagon = new Polygon(ctx, points);
ctx.strokeStyle = 'green';
hexagon.draw();
ctx.stroke();
Copy the code

fill

Above, we use stroke to draw the graph, there is a more used is filled. Fill is to fill the area of the graph with a specific color.

let point1 = [100.30]; / / or 1
let point2 = [50.100]; / / vertex 2
let point3 = [180.120]; / / top 3
ctx.strokeStyle = 'red'; // Use a red stroke
ctx.fillStyle = 'yellow'; // Fill it with yellow
ctx.lineWidth = 2; // Set the width of line segment to 2
ctx.beginPath(); // Start a new path
ctx.moveTo(point1[0], point1[1]); // Move the starting point to vertex 1
ctx.lineTo(point2[0], point2[1]); // connect vertex 1 to vertex 2
ctx.lineTo(point3[0], point3[1]); // connect vertex 2 to vertex 3
ctx.closePath(); // Close the current path
ctx.stroke(); / / stroke
ctx.fill(); / / fill
Copy the code

Note that if the current path is not closed, it will be closed by default, and then populated, as follows: we comment out ctx.closepath ().

let point1 = [100.30]; / / or 1
let point2 = [50.100]; / / vertex 2
let point3 = [180.120]; / / top 3
ctx.strokeStyle = 'red'; // Use a red stroke
ctx.fillStyle = 'yellow'; // Fill it with yellow
ctx.lineWidth = 2; // Set the width of line segment to 2
ctx.beginPath(); // Start a new path
ctx.moveTo(point1[0], point1[1]); // Move the starting point to vertex 1
ctx.lineTo(point2[0], point2[1]); // connect vertex 1 to vertex 2
ctx.lineTo(point3[0], point3[1]); // connect vertex 2 to vertex 3
// ctx.closePath(); // Close the current path
ctx.stroke(); / / stroke
ctx.fill(); / / fill
Copy the code

If the current path is cyclic, or contains multiple intersecting subpaths, how does the canvas fill? For example, when I fill, why is the middle part not filled?

let point1 = [100.30];
let point2 = [50.100];
let point3 = [180.120];
let point4 = [50.60];
let point5 = [160.80];
let point6 = [70.120];
ctx.strokeStyle = 'red';
ctx.fillStyle = 'yellow';
ctx.lineWidth = 2;
ctx.beginPath(); // Start a new path
// Draw triangle 1, sequence: p1--p2--p3--p1
ctx.moveTo(point1[0], point1[1]);
ctx.lineTo(point2[0], point2[1]);
ctx.lineTo(point3[0], point3[1]);
ctx.lineTo(point1[0], point1[1]);
// Draw triangle 2, p4--p5--p6--p4
ctx.moveTo(point4[0], point4[1]);
ctx.lineTo(point5[0], point5[1]);
ctx.lineTo(point6[0], point6[1]);
ctx.lineTo(point4[0], point4[1]);
ctx.stroke(); / / stroke
ctx.fill(); / / fill
Copy the code

Let’s look specifically at the fill function and look at the explanation on the MDN,

The CanvasRenderingContext2D.fill() method of the Canvas 2D API fills the current or given path with the current fill style using the non-zero or even-odd winding rule

void ctx.fill([fillRule]);
void ctx.fill(path[, fillRule]);
Copy the code

The fillRule parameter is optional. The value can be nonzero or evenodd. That is, the fill function can fill the current path or a given path with either a non-zero wrap rule or a parity rule. The path parameter is a Path2D object, which is a given path. The default in canvas is the current path. This parameter is not supported by all browsers, and is not well supported by Internet Explorer and mobile devices.

Non-zero wrap rule

For any given region in the path, draw a line segment long enough from inside the region so that the end of the line segment falls completely outside the path range. Next, you initialize the counter to 0, and then change the counter every time the line intersects a line or curve along the path. If it intersects the clockwise part of the path, it increases by 1, and if it intersects the counterclockwise part of the path, it decreases by 1. Finally, if the counter value is not zero, the region is in the path and is filled when a call to fill is made. If the final value of the counter is 0, then the region is not in the path and will not be filled when you call fill. Canvas fill uses this non-zero wrap rule by default.

Look again at the figure above, why the middle cross area is not filled in. We drew two triangles, the first drawing order is P1 –> P2 –> P3 –> P1, the second drawing order is P4 –> P5 –> P6 –> P4. As you can see, the first triangle is drawn counterclockwise, the second triangle is drawn clockwise, and the counter in the middle intersection area ends up being 0, so it should not be included in this path.

To demonstrate the non-zero Wrap rule, see my example: Non-zero Wrap Example

Odd-even rule

Similar to the non-zero wrap rule, a line is drawn from any region long enough that the end of the segment falls completely outside the path range. If the line segment intersects the path by an odd number of digits, the region is included in the path; if it is even, it is not included in the path.

For example, let’s change the above example to draw the second triangle counterclockwise p4–> P6 –>p5– p4, and then fill it with the non-zero wrap rule and the odd-even rule to see what happens.

// Draw triangle 2, notice the order changed :p4-p6-p5-p4
ctx.moveTo(point4[0], point4[1]);
ctx.lineTo(point6[0], point6[1]);
ctx.lineTo(point5[0], point5[1]);
ctx.lineTo(point4[0], point4[1]);
ctx.stroke(); / / stroke
ctx.fill(); // padding, default is non-zero wrap rule
Copy the code

The order of the two triangles above is counterclockwise, so according to the non-zero wrapping rule, the counter of the intersection region like a triangle with a final value of -2, not 0, is included in the path, and is filled.

The same order, we are using the parity rule to fill.

ctx.fill('evenodd'); // Fill with the parity rule instead
Copy the code

summary

In this chapter, we mainly learned how to draw graphics in canvas, such as common triangles, quadrilateral, circle center, and any polygon. When drawing a graph, some canvas, such as rectangle and circle, have provided built-in functions, such as ctx.rect() and ctx.arc, which can be drawn directly. However, for any polygon, we need to draw each line segment by ourselves.

When you draw a path, it’s sequential. By understanding the path in canvas and the current drawing order, you can have a good understanding of the filling rule in canvas. The canvas is filled with non-zero wrap rules and parity rules. For the same path, different rules may produce different fill areas, is used, pay attention to the path order.