The beginning of the story, I want a ball

Objective: Canvas is used to make a ball that simulates free fall. The ball is elastic.

I used to study gravity in physics in junior high school, and I still remember that there is height, speed and gravitational acceleration. After being dropped from high altitude, the ball will fall freely. If the ball is elastic, it will bounce back.

  • Build HTML template, initialize canvas canvas, brush

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title></title>
  <style>
    html.body.canvas {
      margin: 0;
      padding: 0;
      height: 100%;
      width: 100%;
    }
  </style>
</head>

<body>
  <canvas id="canvas"></canvas>
  <script>
	window.onload = () = > {
          const canvas = document.getElementById('canvas');
          // How big the world stage must be how big 😄
          // ps: the width and height here are not the width and height of the CSS style layer, but pixels
          canvas.width = window.document.body.clientWidth;
          canvas.height = window.document.body.clientHeight;
          const ctx = canvas.getContext('2d');
          // next do some things
          // ...
    	}
  </script>
</body>
</html>
Copy the code

There you have it, a clean canvas,

  • So let’s draw a ball, let’s define a class not car today, not foo today butBallClass.

class Ball {
  // Initialization characteristics
  constructor(options = {}) {
    const {
      x = 0./ / x coordinate
      y = 0./ / y
      ctx = null.// The Magic Brush 🖌️
      radius = 0.// Radius of the ball
      color = '# 000' / / color
    } = options
    this.x = x;
    this.y = y;
    this.ctx = ctx;
    this.radius = radius;
    this.color = color
  }
  / / rendering
  render() {
    this.ctx.beginPath();
    this.ctx.fillStyle = this.color;
    / / draw circles
    this.ctx.arc(this.x, this.y, this.radius, 0.2 * Math.PI)
    this.ctx.fill()
  }
}
Copy the code
  • With thisBallClass to generate a ball

window.onload = () = > {
  // ...
  const ctx = canvas.getContext('2d');
  const ball = new Ball({
    ctx,
    x: ctx.canvas.width * 0.5.// In the center of the canvas
    y: ctx.canvas.height * 0.5.radius: 20.color: '#66cccc'
   })
  ball.render();
}
class Ball {
 // ...
 }

Copy the code

Little ball is born

  • Get him moving, use speed and acceleration,

We’ll use the requestAnimationFrame method, which allows us to call the specified function at the start of the next frame, requestAnimationFrame details.

window.onload = () = > {
  // ...
  ball.render();
  // loop painting
  const loopDraw = () = > {
    requestAnimationFrame(loopDraw);
    ball.render();
  }
  loopDraw(); // Start the animation
}
class Ball {
 // ...
 }
Copy the code

Ugh ~~~~ the ball hasn’t moved yet, it’s dripping! You also need a method to update the position of the ball. Keep working on the Ball and add a updata method

window.onload = () = > {
  // ...
  const loopDraw = () = > {
    requestAnimationFrame(loopDraw);
	// Clear the canvas, otherwise you can see the movement of each frame, this piece has a chew, you can also make cool things.
    ctx.clearRect(0.0, ctx.canvas.width, ctx.canvas.height);
    ball.render();
    ball.updata(); // Update the location
  }
  loopDraw();
}
class Ball {
  // ...
  this.radius = radius;
  this.color = color

  / / speed
  this.vy = 0;  // At first it is static
  / / acceleration
  this.gvy = 1;
  render() {
      // ...
  }
  updata() {
    this.y += this.vy; // Change y by speed per frame
    this.vy += this.gvy; // The speed increases with acceleration per frame
    // Hit bottom collision detection, otherwise the ball will fly off the screen.
    if (this.y >= this.ctx.canvas.height - this.radius) {
      this.y = this.ctx.canvas.height - this.radius;
      // Rebound is a 180 degree change in the direction of movement
      this.vy = -this.vy * 0.75;// Speed loss, roughly imitate the effect of gravity, arbitrarily adjust to your favorite value, about.}}}Copy the code

Little ball it’s moving! It’s moving!

  • summary

    1. Animation needs to use requestAnimationFrame, of course you can use setTimeout or setInterval to simulate loop.

    2. Clear the canvas of the previous frame before drawing the next frame, otherwise the effect of the previous frame will remain on the canvas. Of course you can keep it if you want.

    3. The speed of motion can be understood as the momentum of motion needed for each frame. It takes time to draw each frame, and the time of each frame is not necessarily fixed. According to this feature, the animation can also be optimized to be smoother.

Two, continue to play ball

  • Let the ball move in a chaotic manner, if in space, the effects of gravity are ignored

Experience with y motion, add x motion, remove acceleration, because we’re in space, add omnidirectional collision detection

window.onload = () = > {
  // ...
}
class Ball {
  // ...

  / / speed
  this.vx = -2; // This is the new member
  this.vy = 2;
  / / acceleration
  this.gvx = 0;
  this.gvy = 0;  I don't need you this time
  render() {
      // ...
  }
  updata() {
    this.x += this.vx;
    this.y += this.vy;
    this.vy += this.gvy;
    this.vx += this.gvx;
    / / peaked
    if (this.y - this.radius <= 0) {
      this.y = this.radius
      this.vy = -this.vy * 0.99  / /
    }
    / / bottom
    if (this.y >= this.ctx.canvas.height - this.radius) {
      if (this.vy <= this.gvy * 2 + this.vy * 0.8) this.vy = 0;
      this.y = this.ctx.canvas.height - this.radius;
      this.vy = -this.vy * 0.75; / / then
    }
    / / right
    if (this.x - this.radius <= 0) {
      this.x = this.radius
      this.vx = -this.vx * 0.5 / / set
    }
    / / touch to the left
    if (this.x + this.radius >= this.ctx.canvas.width) {
      this.x = this.ctx.canvas.width - this.radius
      this.vx = -this.vx * 0.5 / / buy}}}Copy the code

look! Bouncing balls, hitting walls everywhere.

  • More ball movement

Ball is a class, so initialize it several times and give the Ball a random speed

window.onload = () = > {
 // ...
 const num = 100;
 let balls = []
 // It is colorful
 const colors = ['#66cccc'.'#ccff66'.'#ff99cc'.'#ff9999'.'# 666699'.'#ff0033'.'#FFF2B0'];
 // I'll take 100
 for (let i = 0; i < num; i++) {
   balls.push(new Ball({
     ctx,
     // Randomly appear anywhere on the canvas
     x: Math.floor(Math.random() * ctx.canvas.width),
     y: Math.floor(Math.random() * ctx.canvas.height),
     radius: 10.color: colors[Math.floor(Math.random() * 7)]}}))// loop painting
 const loopDraw = () = > {
   requestAnimationFrame(loopDraw);
   ctx.clearRect(0.0, ctx.canvas.width, ctx.canvas.height);
   balls.forEach((ball, index) = >{ ball.render(); ball.updata(); }}})class Ball {
 constructor(options = {}) {
   // ...
   / / speed
   this.vx = (Math.random() - 0.5) * 10;
   this.vy = (Math.random() - 0.5) * 10;
   / / acceleration
   this.gvx = (Math.random() - 0.5) * 0.01;
   this.gvy = (Math.random() - 0.5) * 0.01
 }
 // ...
}
Copy the code

Well ~

Third, let neighbors more contact

Neighborhood can only be within a certain range, too far is not oh, then you need to know the distance between two balls, calculate the distance between two points, very familiar ah, an unknown enthusiastic children’s shoes instantly said junior high school (probably) learned the formula

  • Connecting action: Connecting two balls with a wire

A new member of the Ball enters renderLine, drawing lines between points;

// js version of the calculation of the distance between two points formula
function twoPointDistance(p1, p2) {
 let distance = Math.sqrt(Math.pow((p1.x - p2.x), 2) + Math.pow((p1.y - p2.y), 2));
 return distance;
}
window.onload = () = > {
 // ...
 // loop painting
 const loopDraw = () = > {
   requestAnimationFrame(loopDraw);
   ctx.clearRect(0.0, ctx.canvas.width, ctx.canvas.height);
   balls.forEach((ball, index) = > {
     ball.render();
     ball.updata();
     balls.forEach(ball2= > {
       const distance = twoPointDistance(ball, ball2)
       // Exclude yourself and those beyond 100 pixels
       if (distance && distance < 100) {
         ball.renderLine(ball2)
        }
     })
   })
}
}
class Ball {
 // ...
 render() {
     // ...
 }
 updata() {
   // ...
 }
 renderLine(target) {
   this.ctx.beginPath();
   this.ctx.strokeStyle = "ddd";
   this.ctx.moveTo(this.x, this.y);
   this.ctx.lineTo(target.x, target.y);
   this.ctx.stroke(); }}Copy the code

  • Add a special bond

If our colors are different, let’s draw a bond to make the balls smaller and more numerous

window.onload = () = > {
  // ...
}
class Ball {
  // ...
  render() {
      // ...
  }
  updata() {
    // ...
  }
  renderLine(target) {
    // ...
    // Gradient, composed of me and Target
    var lingrad = this.ctx.createLinearGradient(this.x, this.y, target.x, target.y);
    lingrad.addColorStop(0.this.color);
    lingrad.addColorStop(1, target.color);
    this.ctx.strokeStyle = lingrad;
	// ...}}Copy the code

  • Add a drag shadow to the colorful phantom

window.onload = () = > {
  // ...
  // loop painting
 const loopDraw = () = > {
    / /...
    // Replace clearRect to make the last effect transparency 0.3
    ctx.fillStyle = 'rgba (255255255,0.3)';
    ctx.fillRect(0.0, canvas.width, canvas.height);
    // ..}}class Ball {
  // ...
}
Copy the code

  • One more circle,

Ball added renderCircle

// ...
class Ball {
  // ...
  renderCircle(target, radius) {
    this.ctx.beginPath();
    this.ctx.strokeStyle = this.color;
    this.ctx.arc((this.x + target.x) / 2, (this.y + target.y) / 2, radius, 0.2 * Math.PI);
    this.ctx.stroke(); }}Copy the code

  • One more… Well, I don’t have enough space.

Four, optimization

  • Optimize to each frame

The time of each frame is different, so the speed of both x and Y axis should be the same every millisecond. In this way, we need to get the elapsed time of each frame, and then adjust the speed increment of updata to make the animation smoother

  let delayTime = 0;
  // The time of the last frame
  let lastTime =  +new Date;
      // loop painting
  const loopDraw = () = > {
  requestAnimationFrame(loopDraw);
  // The current time
  const now = +new Date;
  delayTime = now - lastTime;
  lastTime = now;
  if (delayTime > 50) delayTime = 50;
  balls.forEach((ball, index) = > {
    ball.render();
    // Adjust the increment in updata according to the time
    ball.updata(delayTime && delayTime);
    // ...})}// ...
updata(delayTime) {
    // The time of each frame is different, so use every millisecond
    this.x += this.vx / (delayTime || 1) * 3;
    this.y += this.vy / (delayTime || 1) * 3;
    // ...
  }
Copy the code
  • Along with a frame rate monitor

Animation has frame rates, so there’s a way to check it to see if the animation is smooth. < 30 frames -> red > 30 frames -> green.

If the frame rate is too low, consider optimizing the callback function in the requestAnimationFrame to see if you are doing too much. Of course, there are a lot of optimization methods, animation this I do not understand. I don’t teach fish to swim

;

The main drawing method in the small plug-in

// Draw method
const FPS = (fpsList) = > {
  ctx.font = '14px serif';
  ctx.fillStyle = "#fff"
  const len = fpsList.length;
  ctx.fillText(`FPS: ${fpsList[len - 1]}`.5.14);
  ctx.lineWidth = '2'
  fpsList.forEach((time, index) = > {
    if (time < 30) {
      ctx.strokeStyle = '#fd5454';
    } else {
      ctx.strokeStyle = '#6dc113';
    }
    ctx.beginPath();
    ctx.moveTo(ctx.canvas.width - ((len - index) * 2), ctx.canvas.height);
    ctx.lineTo(ctx.canvas.width - ((len - index) * 2), (ctx.canvas.height - time * 0.5));
    ctx.stroke();
  });
  // Delete the extra ones
  if (len > 50) {
    fpsList.shift()
  }
}
Copy the code

The last

Based on these, we can continue to expand, such as moving the cursor on the canvas, and automatically connecting the ball near the mouse; It can also pull its motion; The effect of the collision between the balls; Ideas come out one by one, based on a simple ball, generating various ideas, from drawing a circle to various cool effects, the more I try, the more surprises, this is an interesting label. And it doesn’t use very complicated apis, such as canvas moveTo lineTo arc and other common apis, plus some math or physics knowledge. Because of these surprises, I will not be bored on the way of learning.

update

  • Mouse will not get lost in animation, focus is me, 2020-11-11 21:30

// Load the image
const loadImage = (src) = > new Promise(resolve= > {
  const img = document.createElement('img');
  img.src = src;
  img.onload = () = > {
    returnresolve(img); }})window.onload = async() = > {// ...
  const bg = await loadImage('./media/bg.jpg');
  let mouseBall;
  // ...
  // loop painting
  const loopDraw = () = > {
   	// ...
    balls.forEach((ball, index) = > {
      // ...
      if (mouseBall) {
        const lineMouse = twoPointDistance(ball, mouseBall);
        if (lineMouse && lineMouse < 100) {
          ball.renderLine(mouseBall)
        }
      }
    // ...})}window.addEventListener('mousemove'.(e) = > {
    mouseBall = new Ball({
      ctx,
      x: e.pageX,
      y: e.pageY,
      radius: 1.color: "#fff"})})}Copy the code