This is the fifth in the canvas series of notes to study and review. For the full notes, see canvas Core Technologies.

In the last Canvas Core Technology – How to achieve simple animation notes, we have studied in detail how to achieve some simple and single animation through translation, zooming, rotation and other operations of canvas coordinate system. However, in actual animation, there are many factors affecting an animation, such as a ball in free fall. We should not only consider the initial speed and direction of the ball, but also consider external factors such as gravitational acceleration and air resistance. In this note, we will study complex animation in detail.

The core logic

We understand animation as a period of time in which certain properties of an object, such as color, size, position, transparency, etc., change. The unit of animation flow is the rate at which the animation is refreshed, which in browsers is generally the browser frame rate. The higher the frame rate, the smoother the animation. In modern browsers, we typically use requestAnimationFrame to perform animations.

let raf = null;
let lastFrame = 0;

/ / animation
function animate(frame) {
  // Todo: Some animation updates can be performed here
  console.log(frame)
  raf = requestAnimationFrame(animate);
  lastFrame = frame;
}

function start() {
  // Some initialization operations
  init();

  // Perform the animation
  animate(performance.now());
}

function stop() {
  cancelAnimationFrame(raf);
}
Copy the code

The general structure is as follows: requestAnimationFrame continuously executes animate in the next frame of the browser, and the animate function takes a timestamp for the current frame to start execution. If you want to interrupt the animation, simply call cancelAnimationFrame and animate will not be performed in the next frame. Use frame-lastFrame as the last time frame was executed, and then calculate the current frame rate based on this difference, as follows:

let fps = 0;
let lastCalculateFpsTime = 0;
function calculateFps(frame) {
  if (lastFrame && (fps === 0 || frame - lastCalculateFpsTime > 1000)) {
    fps = 1000/ (frame - lastFrame); lastCalculateFpsTime = frame; }}/ / animation
function animate(frame) {
  // Todo: Some animation updates can be performed here
  calculateFps(frame);
  raf = requestAnimationFrame(animate);
  lastFrame = frame;
}
Copy the code

When calculating the FPS, we divide the time of the previous frame by 1s. Since the frame is in milliseconds, we divide by 1000. We also made an optimization to calculate the FPS every 1s, instead of every frame, because it doesn’t make sense to calculate every frame and adds extra computation.

Time factor

When drawing animations, we must design them in a time-based fashion, not the current browser frame rate. Different browsers have different frame rates, and the same browser may have different frame rates under different GPU loads. Therefore, our animation must be time-based, so as to ensure that the animation changes are consistent at the same speed in the same time. For example, when we think about the ball falling vertically, we have to set the velocity v of the ball falling, and then according to the formula, the moving distance of the ball in the current time period is obtained, and the coordinates in the current frame are calculated.

  /* Initialize */
  private init() {
    this.fps = 0; 
    this.lastFrameTime = 0;
    this.speed = 5; // Set the ball's initial velocity to 5m/s
    this.distance = 50; // Set the height of the ball from the ground to 50m
    let pixel = this.height - this.padding * 2;
    if (this.distance <= 0) {
      this.pixelPerMiter = 0;
    } else {
      this.pixelPerMiter = (this.height - this.padding * 2) / this.distance; }}Copy the code

In the code above, we set the initial velocity of the ball as zero during initialization, the height of the ball from the ground isAnd calculated the ratio of the physical height to the pixel heightpixelPerMiterThis value will be useful later when calculating the coordinates of the ball.

  / * update * /
  private update() {
    if (this.fps) {
      this.ball.update(1000 / this.fps); // Update the ball}}Copy the code

Then, when we update the ball position each frame, we pass the time value of the previous frame to ball.update.

  Move / * * /
  static move(ball: Ball, elapsed: number) {
    // The ball is static and does not update
    if (ball.isStill) {
      return;
    }
    let { currentSpeed } = ball;
    let t = elapsed / 1000; // Elapsed is milliseconds and the speed is in m/s, so divide by 1000
    let distance = ball.currentSpeed * t; 
    if (ball.offset + distance > ball.verticalHeight) {
      //// If the ball has exceeded its actual height, it has fallen to the ground
       ball.isStill = true;
       ball.currentSpeed = 0;
       ball.offset = ball.verticalHeight;
    } else{ ball.offset += distance; }}Copy the code

Then calculate the falling distance of the ball in the last frame according to the formula, add up the falling distance of each frame, then the total falling distance of the ball can be obtained. If the total falling distance of the ball is greater than the actual height between the ball and the ground, it means that the ball has fallen to the ground, and stop the ball falling.

  /* Draw the ball */
  public render(ctx: CanvasRenderingContext2D) {
    let { x, y, radius, offset, pixelPerMiter } = this;
    ctx.save();
    ctx.translate(x, y + offset * pixelPerMiter); //offset * pixelPerMiter gets the falling pixels
    ctx.beginPath();
    ctx.arc(0.0, radius, 0.Math.PI * 2.false);
    ctx.fill();
    ctx.stroke();
    ctx.restore();
  }
Copy the code

Finally, when drawing the ball, you should first consider the actual height of the falloffsetAnd the ratio of the actual height calculated above to the pixel height to get the pixel value of the ball falling on the screen ().

This is the general idea for writing the ball in free fall. The main idea is to set the ball’s initial falling speed, calculate the ball’s falling distance in each frame, and finally calculate the ball’s falling pixel height on the screen based on the actual height versus pixel height. In this process, we have not considered the acceleration of gravity and air resistance and other physical factors, let’s consider the impact of physical factors on animation.

Physical factors

In order to make the animation or the game even more real, usually need to consider the effect of physical factors in the real world, such as we continue to consider the ball movement, free fall in the real world, small ball games receive free fall acceleration of gravity, air resistance, air flow, the effect of the rebound, thus changing the speed of the ball drop.

  /* Create a ball */
  private createBall() {
    let { width, height, padding, speed, radius, pixelPerMiter, distance } = this;
    this.ball = new Ball(width / 2, padding - radius, radius, { verticalHeight: distance, pixelPerMiter, useGravity: true });
    this.ball.setSpeed(speed);
    this.ball.addBehavior(Ball.move);
  }
Copy the code

When creating the ball, we give a parameter userGravity to indicate whether to use the acceleration of gravity, which we set to true, and we pass the ball’s initial coordinates and radius, as well as its initial velocity, etc.

const GRAVITY = 9.8; // Acceleration of gravity is 9.8m/s
  Move / * * /
  static move(ball: Ball, elapsed: number) {
    // ...
    // If gravity is applied, the speed is updated
    if (ball.useGravity) {
      ball.currentSpeed += GRAVITY * t;
    }
   // ...
  }
Copy the code

Then when updating the ball, we added the calculation of the ball’s current velocity according to the formulaCalculate the speed of the last frame, so that, over time, the ball is actually increasing in speed, and the ball is falling faster and faster.

When we dealt with the ball falling to the ground, we simply let the ball rest on the ground. But in real life, when we drop a ball, it hits the ground, bounces up a certain height, and then falls back down again until the ball comes to rest on the ground. To better simulate the ball’s fall, let’s consider the physics of rebound.

// Create a small ball
this.ball = new Ball(width / 2, padding - radius, radius, { verticalHeight: distance, pixelPerMiter, useGravity: true.useRebound: true });
Copy the code

When we created the ball, we passed useRebound:true, which means that the bounceeffect is applied to the ball. When updating the ball, we need to check that when it lands, the speed of the ball is reversed and the size is reduced by 0.6 times. The 0.6 coefficient is just a rule of thumb and can be adjusted to achieve the desired effect in a specific game. The higher the coefficient, the higher the rebound.

  Move / * * /
  static move(ball: Ball, elapsed: number) {
    // The ball is static and does not update
    if (ball.isStill) {
      return;
    }
    let { currentSpeed } = ball;
    let t = elapsed / 1000; // Elapsed is milliseconds and the speed is in m/s, so divide by 1000
    // Update speed
    if (ball.useGravity) {
      ball.currentSpeed += GRAVITY * t;
    }
    let distance = ball.currentSpeed * t; 
    if (ball.offset + distance > ball.verticalHeight) {
      // Fall to the ground
      // Use rebound effect
      if (ball.useRebound) {
        ball.offset = ball.verticalHeight;
        ball.currentSpeed = -ball.currentSpeed * 0.6; // Take the velocity in the opposite direction and multiply by 0.6
        if ((distance * ball.pixelPerMiter) / t < 1) {
          // The current movement distance is less than 1px, should be stationary,
          ball.isStill = true;
          ball.currentSpeed = 0; }}else {
        ball.isStill = true;
        ball.currentSpeed = 0; ball.offset = ball.verticalHeight; }}else{ ball.offset += distance; }}}Copy the code

In the application of rebound effect, we judge that the current speed within 1s falling displacement is less than 1px, we will stop the ball, so as to prevent the ball when the rebound distance is very small, unnecessary calculation.

As for other physical factors, such as wind direction and resistance, we will not discuss them in detail. The specific idea is the same as above: physical modeling is carried out first, then the affected properties are calculated according to the physical formula in the updating process, and finally the properties are drawn according to the attribute values.

Here is my complete online example of a ball in free fall

Timeline distortion

Animation lasts for a period of time. We can specify a specific duration value in advance and make the animation continue to execute within this period of time, just like animation-duration in CSS3. Then, by distorting the time axis, we can make the animation perform non-linear motion, such as the common effect of slow in, slow out, slow in and slow out, etc.

The time warp is calculated as effectPercent according to the current time completion ratio compeletePercent by a series of corresponding slow functions. Finally, the warp time value is calculated according to the two values of Elapsed


Linear function,

  static linear() {
    return function(percent: number) {
      return percent;
    };
  }
Copy the code

Well, the buffer function,

  static easeIn(strength: number = 1) {
    return function(percent: number) {
      return Math.pow(percent, strength * 2);
    };
  }
Copy the code

The cache function,

  static easeOut(strength: number = 1) {
    return function(percent: number) {
      return 1 - Math.pow(1 - percent, strength * 2);
    };
  }
Copy the code

Slow in and slow out function,

  static easeInOut() {
    return function(percent: number) {
      return percent - Math.sin(percent * Math.PI * 2)/(2 * Math.PI);
    };
  }
Copy the code

Here is a complete online example of my timeline distortion.

More complex EasingFunctions include spring effects, bezier curves, etc. See EasingFunctions for details.

summary

This note is mainly discussed how to realize the complex animations in the canvas, from a small ball of free fall to the ground movement as the example, we when calculating the ball falling distance, calculated based on time dimension, rather than the current browser frame rate, because the frame rate is not a constant and reliable value, it can make the movement of the ball becomes unclear. When we calculate time, we can calculate how far the ball falls in the same amount of time, which is an exact value independent of the frame rate. In order to make the ball fall more realistic, we also consider the physical factors that affect the ball fall, such as gravitational acceleration, rebound effect and so on. When making other nonlinear motion animations, we can use common easing functions, such as, easing in, easing out, etc. Their essence is to distort the time axis so that the current motion is affected by the time factor.

When making Canvas games, animation is basically used. When objects move, collisions will definitely occur. For example, when our ball falls, collisions will occur between the ball and the ground. In the next note, we’ll discuss collision detection in more detail on a Canvas.