Canvas2D coordinate system

First, the problem of coordinate system needs to be solved. The coordinate origin of the Canvas2D canvas is in the upper left corner of the screen, and the lower right corner is the width and height of the canvas. But it’s going to be a little bit awkward, but we can use some API to manipulate the coordinate system a little bit, to get the coordinate system that we’re used to.

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.scale(1, -1);
Copy the code

The changed coordinate system simplifies computation, which not only makes the code easier to understand, but also saves CPU time.

vector

A point can be represented directly using the array [x, y].

Vectors (let’s just talk about two-dimensional vectors for a moment) are much richer, not only a little bit, but also length and direction information, and even some operations between vectors, such as vector addition and multiplication.

The length of the vector

The vector length formula is as follows:


v The length of the = x 2 + y 2 SQRT {x^2 + y^2} = SQRT {x^2 + y^2}

The JavaScript implementation is as follows:

// Find the square root of the sum of the squares of x and y
const length = Math.hypot(x, y);
Copy the code

The Angle between the vector and the X-axis

The Angle ranges from -π to π, with a negative number indicating below the X-axis and a positive number indicating above the X-axis. The Angle formula is shown below:


v with x Weeks of Angle = arctan ( y x ) \vec v = \arctan (\frac {y} {x})

The JavaScript implementation is as follows:

const angle = Math.atan2(y, x);
Copy the code

In addition, a set of relations can also be derived according to the length and included Angle of the vector:

// The x-coordinate of the vector
const x = length * Math.cos(angle);
// The y coordinate of the vector
const y = length * Math.sin(angle);
Copy the code

Vector addition

The following figure is a schematic diagram of the addition of two vectors, which means that the end point of vector v1 (x1, y1) is moved along the direction of vector v2 by a distance equal to the length of vector V2.

The vector addition formula is as follows:


v 1 + v 2 = ( x 1 + x 2 . y 1 + y 2 ) \vec {v1} + \vec {v2} = (x1 + x2, y1 + y2)

The JavaScript implementation is as follows:

const x = x1 + x2;
const y = y2 + y2;
Copy the code

Vector scaling

Vector scaling is a vector multiplied by a scalar, as shown below:


v = [ x . y ] n v = [ n x . x y ] \vec v = [x, y] \\ n\vec v = [nx, xy]

The JavaScript implementation is as follows:

x = n * x;
y = n * y;
Copy the code

Vector rotation

A little matrix knowledge may be needed here, listing the rotation matrix as follows:


[ x y ] [ cos Alpha. sin Alpha. sin Alpha. cos Alpha. ] = [ x cos Alpha. y sin Alpha. x sin Alpha. + y cos Alpha. ] \begin{bmatrix} x\\ y\\ \end{bmatrix} \begin{bmatrix} \cos \alpha & -\sin \alpha \\ \sin \alpha & \cos \alpha \\ \end{bmatrix} = \begin{bmatrix} x \cos \alpha – y \sin \alpha \\ x \sin \alpha + y \cos \alpha \\ \end{bmatrix}

Vector rotation formula is as follows:


v rotating Alpha. The coordinates after degrees = ( x cos Alpha. y sin Alpha. . x sin Alpha. + y cos Alpha. ) \vec v = (x \cos \ alpha-y \sin \alpha, x \sin \alpha + y \cos \alpha)

The JavaScript implementation is as follows:

// Rad is the radian corresponding to Angle α
x = x * Math.cos(rad) - y * Math.sin(rad);
y = x * Math.sin(rad) + y * Math.cos(rad);
Copy the code

The vector dot product

It’s the physical equivalent of the work done by a force applied to the object to produce b’s displacement.

The vector dot product formula is as follows:


a = [ a 1 . a 2 ] b = [ b 1 . b 2 ] a f. b = a 1 b 1 + a 2 b 2 \vec a = [a1, a2] \\ \vec b = [b1, b2] \\ \vec a \bullet \vec b = a1b1 + a2b2

It means that the length of the projection of vector B onto vector A is multiplied by the length of vector A. The Angle between the two vectors is α, so the following formula is also true:


a f. b = a b cos Alpha. \vec a \bullet \vec b = |a||b| \cos \alpha

The JavaScript implementation of two-dimensional vector dot product is as follows:

function dot(a, b) {
  return a.x * b.x + b.x * b.y;
}
Copy the code

Vector cross-product

The cross product of two-dimensional vectors A and B is the product of the projection of vector A and vector B along the vertical direction, and its geometric meaning is the area of the parallelogram formed by vectors A and B. Note: a new vector perpendicular to the coordinate plane formed by the original vector cannot be obtained in two-dimensional space, so only the scalar of the product can be obtained.


a x b = a b sin Alpha. | \ vec x \ vec b | = | a | b | | \ sin \ alpha

The physical meaning of the vector cross product in two-dimensional space is the torque of two forces A and B, which can be understood as the tendency of an object to rotate around an axis under the action of a force. It’s a vector equal to the cross product of the moment arm L and the force F.


a x b = [ x 1 y 1 ] x [ x 2 y 2 ] = [ x 1 y 1 x 2 y 2 ] = x 1 y 2 x 2 y 1 \vec a × \vec b = \begin{bmatrix} x1 & y1 \\ end{bmatrix} × \begin{bmatrix} x2 & y2 \\ end{bmatrix} = \begin{bmatrix} x1 & y1 \\ end{bmatrix} × \begin{bmatrix} x2 & y2 \\ end{bmatrix} = \begin{bmatrix} x1 & y1 \\ x2 & y2 \\ \end{bmatrix} = x1y2 – x2y1

In fact, the result of the cross product is not a scalar but a vector, and the new vector is the coordinate plane of the two original vectors. Where I, j and k are unit vectors of x, y and Z axes respectively.


a x b = [ x 1 y 1 z 1 ] x [ x 2 y 2 z 2 ] = [ i j k x 1 y 1 z 1 x 2 y 2 z 2 ] = [ y 1 z 2 y 2 z 1 ( x 1 z 2 x 2 z 1 ) x 1 y 2 x 2 y 1 ] \vec a × \vec b = \begin{bmatrix} x1 &y1&z1 \\ end{bmatrix} × \begin{bmatrix} x2 &y2&z2 \\ end{bmatrix} = \vec a × \vec b = \begin{bmatrix} x1 &y1&z1 \\ end{bmatrix} = \begin{bmatrix} i & j & k \\ x1 & y1 & z1 \\ x2 & y2 & z2 \\ \end{bmatrix} = \begin{bmatrix} y1z2 – y2z1 \\ -(x1z2 – x2z1) \\ x1y2 – x2y1 \\ \end{bmatrix}

The way to determine the direction of the cross product of A and b is that the coordinate system with x to the right and y down is right-handed. When taking the direction of the cross product of vectors A and B in the right hand system, we can point our right index finger toward A and our right middle finger toward B, so that the direction of our thumb is the direction of the cross product of a and B, which is facing out of the page. So the direction of the cross product in the right hand system is the direction of the right thumb, and the direction of the cross product in the left hand system is the direction of the left thumb.

The 2d vector cross product is implemented in JavaScript as follows:

function cross(a, b) {
  return a.x * b.y - b.x * a.y;
}
Copy the code

Two-dimensional vector class implementation

Now we can implement a two-dimensional vector Class based on some basic vector operations as follows:

class Vector2D {
  constructor(x = 1, y = 0) {
    this.x = x;
    this.y = y;
  }

  get length() {
    return Math.hypot(this.x, this.y);
  }

  get angle() {
    return Math.atan2(this.y, this.x);
  }

  add(vec) {
    this.x += vec.x;
    this.y += vec.y;
    return this;
  }
  
  sub(vec) {
    this.x -= vec.x;
    this.y -= vec.y;
    return this;
  }

  scale(rate) {
    this.x *= rate;
    this.y *= rate;
    return this;
  }

  cross(vec) {
    return this.x * vec.y - vec.x * this.y;
  }

  dot(vec) {
    return this.x * vec.x + vec.y * this.y;
  }

  rotate(rad) {
    const cos = Math.cos(rad);
    const sin = Math.sin(rad);
    const x = this.x;
    const y = this.y;

    this.x = x * cos - y * sin;
    this.y = x * sin + y * cos;

    return this;
  }
  
  copy() {
    return new Vector2D(this.x, this.y); }}Copy the code

Example 1

We can use the two-dimensional vector implementation above to draw a tree with randomly generated branches:

codesandbox

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Vector2D</title>
  <script src="Vector2D.js"></script>
</head>

<body>
  <canvas width="512" height="512"></canvas>
  <script>
    const canvas = document.querySelector('canvas');
    const ctx = canvas.getContext('2d');

    ctx.translate(0, canvas.height);
    ctx.scale(1, -1);
    ctx.lineCap = 'round';

    function drawBranch(context, v0, length, thickness, angle, offset) {
      const v = new Vector2D().rotate(angle).scale(length);
      const v1 = v0.copy().add(v);

      context.lineWidth = thickness;
      context.beginPath();
      context.moveTo(v0.x, v0.y);
      context.lineTo(v1.x, v1.y);
      context.stroke();

      if (thickness > 2) {
        const left = Math.PI / 4 + 0.5 * (angle + 0.2) + offset * (Math.random() - 0.5);
        drawBranch(context, v1, length * 0.9, thickness * 0.8, left, offset * 0.9);
        const right = Math.PI / 4 + 0.5 * (angle - 0.2) + offset * (Math.random() - 0.5);
        drawBranch(context, v1, length * 0.9, thickness * 0.8, right, offset * 0.9);
      }

      // Draw the flowers on the tree
      if (thickness < 5 && Math.random() < 0.3) {
        context.save();
        context.strokeStyle = '#c72c35';
        const th = Math.random() * 6 + 3;
        context.lineWidth = th;
        context.beginPath();
        context.moveTo(v1.x, v1.y);
        context.lineTo(v1.x, v1.y - 2); context.stroke(); context.restore(); }}const v0 = new Vector2D(canvas.width / 2.0);
    drawBranch(ctx, v0, 50.10.1.3);
  </script>
</body>

</html>
Copy the code

Effect:

Example 2

Draw some regular polygons using vectors:

codesandbox

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Vector2D</title>
  <script src="Vector2D.js"></script>
  <script src="draw.js"></script>
</head>

<body>
  <canvas width="512" height="512"></canvas>
  <script>
    const canvas = document.querySelector('canvas');
    const ctx = canvas.getContext('2d');

    ctx.translate(canvas.width / 2, canvas.height / 2);
    ctx.scale(1, -1);

    function regularPolygon(edges = 3, center, sideLength) {
      const coordinates = [];
      // Configure the vector starting point
      let vec = new Vector2D(center.x, center.y);
      coordinates.push(vec);
      // The Angle of each rotation of the vector
      const angle = Math.PI * (1 - (edges - 2) / edges);
      // A base vector generated based on the configured length
      const basicVec = new Vector2D(sideLength, 0);

      for (let i = 0; i < edges; i++) {
        vec = vec.copy().add(basicVec.rotate(angle));
        coordinates.push(vec);
      }
      return coordinates;
    }

    draw(ctx, regularPolygon(3, { x: 128.y: 128 }, 120));
    draw(ctx, regularPolygon(6, { x: -128.y: 128 }, 60));
    draw(ctx, regularPolygon(12, { x: -128.y: -128 }, 40));
    draw(ctx, regularPolygon(60, { x: 128.y: -128 }, 10));
  </script>
</body>

</html>
Copy the code

Effect:

curve

As long as there are enough visible sides, it can be drawn like a circle. However, it is not a very good way to draw a circle in this way. It is tedious and not precise enough, and it cannot draw other types of curves, such as ellipses, parabola, Bezier curves, etc. So, the best way to draw a curve is to use parametric equations.

round

For a circle centered at (x0, y0) with radius r, the parametric equation is as follows:


{ x = x 0 + r cos ( Theta. ) y = y 0 + r sin ( Theta. ) \begin{cases} x = x_0 + r \cos (\theta) \\ y = y_0 + r \sin (\theta) \\ \end{cases}

codesandbox

// The default is 60 segments
const CircleSegments = 60;
// Angle of the circle
const CircleAngle = Math.PI * 2;

function arc(centre, radius, startAngle = 0, endAngle = Math.PI * 2) {
  // The actual Angle to draw
  const angle = Math.min(Math.PI * 2, endAngle - startAngle);
  // If you are drawing a full circle, the first point will not be drawn, because it will be drawn at the end
  const coordinates = angle === CircleAngle ? [] : [centre];
  // Modify the actual number of drawn segments
  const segments = Math.round(CircleSegments * angle / CircleAngle);

  for (let i = 0; i <= segments; i++) {
    const x = centre.x + radius * Math.cos(startAngle + angle * i / segments);
    const y = centre.y + radius * Math.sin(startAngle + angle * i / segments);
    coordinates.push({ x, y });
  }
  return coordinates;
}

draw(ctx, arc({ x: 0.y: 0 }, 100));
Copy the code

The renderings are as follows:

Note: Canvas2D provides a drawing API, but WebGL does not provide a drawing API, so this function is useful in WebGL.

Canvas2D API implementation:

codesandbox

/ / draw circles
ctx.beginPath();
ctx.arc(0.0.50.0.2 * Math.PI);
ctx.stroke();
Copy the code

The ellipse

A circle is a special case of an ellipse. It is a circle when its short and short axes (a and b) are equal. The formula is as follows:


{ x = x 0 + a cos ( Theta. ) y = y 0 + b sin ( Theta. ) \begin{cases} x = x_0 + a \cos (\theta) \\ y = y_0 + b \sin (\theta) \\ \end{cases}

The code only needs to be modified slightly

codesandbox

function ellipse(centre, a, b, startAngle = 0, endAngle = Math.PI * 2) {
  // The actual Angle to draw
  const angle = Math.min(Math.PI * 2, endAngle - startAngle);
  // If you are drawing a full circle, the first point will not be drawn, because it will be drawn at the end
  const coordinates = angle === CircleAngle ? [] : [centre];
  // Modify the actual number of drawn segments
  const segments = Math.round(CircleSegments * angle / CircleAngle);

  for (let i = 0; i <= segments; i++) {
    const x = centre.x + a * Math.cos(startAngle + angle * i / segments);
    const y = centre.y + b * Math.sin(startAngle + angle * i / segments);
    coordinates.push({ x, y });
  }
  return coordinates;
}

draw(ctx, ellipse({ x: 0.y: 0 }, 150.100));
Copy the code

Effect:

parabolic

A parabola is the trajectory of a point in a plane equidistant from a fixed point F (focus) and a fixed line L (directrix). P is a constant, is the distance from the focus to the directrix, and t is a parameter. The parametric equation of the parabola is as follows:


{ x = x 0 + 2 p t 2 y = y 0 + 2 p t or { x = x 0 + 2 p t y = y 0 + 2 p t 2 \begin{cases} x = x_0 + 2pt^2 \\ y = y_0 + 2pt \\ \end{cases} \\ 或 \\ \begin{cases} x = x_0 + 2pt \\ y = y_0 + 2pt^2 \\ \end{cases}

codesandbox

const LineSegments = 60;
function parabola(x0, y0, p, t) {
  const coordinates = [];

  for (let i = 0; i <= LineSegments; i++) {
    const s = i / LineSegments;
    const paramT = t.min + s * (t.max - t.min);

    const x = x0 + 2 * p * paramT ** 2;
    const y = y0 + 2 * p * paramT;
    // const x = x0 + 2 * p * paramT;
    // const y = y0 + 2 * p * paramT ** 2;
    coordinates.push({ x, y });
  }
  return coordinates;
}

draw(ctx, parabola(0.0.5, { min: -10.max: 10 }));
Copy the code

Effect:

Other Common curves

In order to simplify this part of the operation, we can implement a higher-order function.

codesandbox

// parametric.js
// Draw graphs according to points
function draw(
  coordinates,
  context,
  { strokeStyle = "black", fillStyle = null, close = false, rule = 'nonzero' } = {}
) {
  context.strokeStyle = strokeStyle;
  context.beginPath();
  context.moveTo(coordinates[0].x, coordinates[0].y);
  for (let i = 1; i < coordinates.length; i++) {
    context.lineTo(coordinates[i].x, coordinates[i].y);
  }
  if (close) context.closePath();
  if (fillStyle) {
    context.fillStyle = fillStyle;
    context.fill(rule);
  }
  context.stroke();
}

function parametric(xFunc, yFunc) {
  return function (start, end, segments = 100. args) {
    const coordinates = [];
    for (let i = 0; i <= segments; i++) {
      const s = i / segments;
      const t = start + s * (end - start);

      constx = xFunc(t, ... args);consty = yFunc(t, ... args); coordinates.push({ x, y }); }return {
      draw: draw.bind(null, coordinates)
    };
  };
}

Copy the code
<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>conic</title>
    <script src="parametric.js"></script>
  </head>

  <body>
    <canvas width="512" height="512"></canvas>
    <script>
      const canvas = document.querySelector("canvas");
      const ctx = canvas.getContext("2d");

      ctx.translate(canvas.width / 2, canvas.height / 2);
      ctx.scale(1, -1);

      / / the parabola
      const parabola = parametric(
        (t, p) = > 2 * p * t, // x
        (t, p) = > 2 * p * t ** 2 // y
      );

      parabola(-5.5.5.5.60.10).draw(ctx, { strokeStyle: "green" });

      / / spiral
      const helical = parametric(
        (t, l) = > l * t * Math.cos(t), // x
        (t, l) = > l * t * Math.sin(t) // y
      );

      helical(0.50.500.5).draw(ctx, { strokeStyle: "blue" });

      / / astroid
      const star = parametric(
        (t, l) = > l * Math.cos(t) ** 3.// x
        (t, l) = > l * Math.sin(t) ** 3 // y
      );

      star(0.Math.PI * 2.80.120).draw(ctx, { strokeStyle: "red" });
    </script>
  </body>
</html>
Copy the code

Effect:

Bessel curve

With a starting point, an end point, and a small number of control points, parametric equations can be used to generate complex smooth Curves, so Bezier Curves are often used to draw irregular shapes. Here is a typical Bezier curve, and adjusting its control points produces a different curve:

Bezier curves can be divided into Quadratic Bezier curves and third-order Qubic Bezier curves.

The second-order Bezier curve is determined by three points, P0 is the starting point, P2 is the end point, P1 is the control point, and T is the parameter, as shown in the figure below:

The parameter equation of second-order Bezier curve is shown as follows:


B t = ( 1 t ) 2 P 0 + 2 ( 1 t ) t P 1 + t 2 P 2 ( 0 Or less t Or less 1 ) B_t = (1 – t)^2 P_0 + 2(1 – t)t P_1 + t^2 P_2 (0 \leq t \leq 1)

The third-order Bezier curve is determined by four points, with one more control point than the second-order Bezier curve. P0 is the starting point, P3 is the end point, P1 and P2 are the control points, and T is the parameter, as shown in the figure below:

The parameter equation of the third-order Bessel curve is as follows:


B t = ( 1 t ) 3 P 0 + 3 ( 1 t ) 2 t P 1 + 3 ( 1 t ) 2 P 2 + t 3 P 3 ( 0 Or less t Or less 1 ) B_t = (1 – t)^3 P_0 + 3(1 – t)^2t P_1 + 3(1-t)^2 P_2 + t^3 P_3 (0 \leq t \leq 1)

The code implementation is as follows:

codesandbox

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>bezier</title>
    <script src="Vector2D.js"></script>
    <script src="parametric.js"></script>
  </head>

  <body>
    <canvas width="512" height="512"></canvas>
    <script>
      const canvas = document.querySelector("canvas");
      const ctx = canvas.getContext("2d");

      ctx.translate(canvas.width / 2, canvas.height / 2);
      ctx.scale(1, -1);

      // Second order Bezier curve
      const quadricBezier = parametric(
        (t, [{x: x0}, {x: x1}, {x: x2}]) = > (1 - t) ** 2 * x0 + 2 * t * (1 - t) * x1 + t ** 2 * x2,
        (t, [{y: y0}, {y: y1}, {y: y2}]) = > (1 - t) ** 2 * y0 + 2 * t * (1 - t) * y1 + t ** 2 * y2,
      );

      const p0 = new Vector2D(0.0);
      const p1 = new Vector2D(100.200);
      const p2 = new Vector2D(200, -50);
      quadricBezier(0.1.100, [p0, p1, p2]).draw(ctx, {strokeStyle: 'red'});

      // Third order Bezier curve
      const cubicBezier = parametric(
        (t, [{x: x0}, {x: x1}, {x: x2}, {x: x3}]) = > (1 - t) ** 3 * x0 + 3 * t * (1 - t) ** 2 * x1 + 3 * (1 - t) * t ** 2 * x2 + t ** 3 * x3,
        (t, [{y: y0}, {y: y1}, {y: y2}, {y: y3}]) = > (1 - t) ** 3 * y0 + 3 * t * (1 - t) ** 2 * y1 + 3 * (1 - t) * t ** 2 * y2 + t ** 3 * y3,
      );

      const P0 = new Vector2D(0.0);
      const P1 = new Vector2D(-10.100);
      const P2 = new Vector2D(150.100);
      const P3 = new Vector2D(200, -50);
      cubicBezier(0.1.100, [P0, P1, P2, P3]).draw(ctx, {strokeStyle: 'blue'});
    </script>
  </body>
</html>
Copy the code

Effect:

Note: Canvas2D provides a drawing API, but WebGL does not provide a drawing API, so this function is useful in WebGL.

Canvas2D API implementation:

codesandbox

// Draw a third-order Bezier curve
ctx.beginPath();
// The color of the line, the thickness of the line
ctx.strokeStyle = "blue";
ctx.lineWidth = 4;
/ / starting point
ctx.moveTo(0.0);
ctx.bezierCurveTo(-10.100.150.100.200, -50);

ctx.stroke();
Copy the code