I. Core concepts of Canvas animation

Those who have no canvas foundation are advised to brush it first
The basic usage of Canvas – Web API interface reference | MDN]


The focus is on understanding the basic steps of Canvas animation. In basic Animation-MDN, animation is divided into 4 steps



Beginners can make it easier, let’s ignore the state save and go straight to two steps:

  • Empty canvas
  • Draws a new frame of animation

Use a timer or window. RequestAnimationFrame timing can repeat the above two steps


Two, grab the core principle of gold coins

To imagine the overall business scenario, let’s first tease out three core issues to be addressed:

  • 1. There are two solutions to generate red packets
    • All red envelope objects are uniformly generated and distributed on the Y-axis from top to bottom. After triggering the movement, the whole movement is downward
    • Continue to generate new red envelope objects at the top of the screen, once the red envelope is generated, it starts to move immediately (select this scheme this time)
  • 2. Motion, principle of Canvas animation
  • 3. The user clicks on the red envelope and calculates whether to click on the red envelope (events can only be bound to the Canvas layer and need to be calculated according to the clicking position)


Iii. Core functions

  • 1. Pre-cached images/off-screen canvas
  • 2. Canvas draws multiple pictures and changes each frame to form animation
  • 3, judge the click position, bubble +1 effect


The following is vUE based code, which cannot be run directly, and is mainly used to understand the core functions

It is best to do a simple demo after you understand the core principles

1. Pre-cached images/off-screen canvas

It feels like lots and lots of coins are falling at various angles

In fact, there are 4 gold coins on the page, but they are different in size, speed and look different

We can load all four images first

 // Cache several gold images as DOM elements to avoid asynchronously reading images during canvas drawing
loadImgs(arr) {
  return new Promise(resolve= > {
    let count = 0;
    // Loop through an array of images, each image generates a new image object
    const len = arr.length;
    for (let i = 0; i < len; i++) {
      // Create a picture object
      const image = new Image();
      // Successful asynchronous callback
      image.onload = (a)= > {
        count++;
        arr.splice(i, 1, {
	  // The loaded image objects are cached here. Canvas can be drawn directly
          img: image,
	  // The off-screen canvas can be generated and cached directly to optimize performance, but not this time, just as an example
          offScreenCanvas: this.createOffScreenCanvas(image)
        });
	// All images in arR are loaded
        if (count == len) {
          this.preloaded = true; resolve(); }}; image.src = arr[i].img; }}); },Copy the code

Here’s how to create an off-screen canvas

createOffScreenCanvas(image) {
  const offscreenCanvas = document.createElement("canvas");
  const offscreenContext = offscreenCanvas.getContext("2d");
  // Here can be dynamic width and height
  offscreenContext.width = 30;
  offscreenContext.height = 30;
  offscreenContext.drawImage(
    image,
    0.0,
    offscreenContext.width,
    offscreenContext.height
  );
  // return this offscreenCanvas
  return offscreenCanvas;
},Copy the code

2. Canvas draws multiple pictures and changes each frame to form animation

First initialize the canvas

Here we directly store the canvas context CTX in data to facilitate reading in various methods.

Unlike a single JS module, you can use closures to encapsulate a separate context in vUE, and declaring global variables is not recommended in VUE

initCanvas() {
  const canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    this.ctx = canvas.getContext("2d");
    // Preload images synchronously during initialization
    this.loadImgs(this.imgArr); }},Copy the code

To draw multiple images, loop through the image array imgArr created above, and then call this.ctx.drawImage() for each image object

Now let’s change the image to change the gold object

The image array imgArr is replaced by the Coin object array coinArr, this array is composed of a Coin object Coin, the Coin object itself in addition to the picture, there are size, physical location, falling speed and other parameters, that is to say, each Coin object cache all their drawing information, here is the object-oriented thinking

const Coin = {
  x: X position.y: 'Y position'.// The key to motion is to change y every frame
  radius: 'Gold coin size'.img: 'Previously cached gold image'.speed: 'How fast the gold coins fall'
};Copy the code


Each frame, loop through the gold array and draw all the gold objects

To get moving, drop the y position of each coin a little bit per frame, which is y: coin.y + coin.speed

So when you draw the next frame, all else being the same, each coin moves down a little bit, coheres, and these different frames add up to a moving animation

Look at the drawn code first

drawCoins() {
  // Walk through the array of gold objects
  this.coinArr.forEach((coin, index) = > {
    const newCoin = {
      x: coin.x,
      // The key to movement is only y
      y: coin.y + coin.speed,
      radius: coin.radius,
      img: coin.img,
      speed: coin.speed
    };
    // When a coin object is drawn, a new coin object is also created, replacing the original one. The only difference is that its y is changed. The next frame draws the coin and it moves a little distance
    this.coinArr.splice(index, 1, newCoin);
    this.ctx.drawImage(
      coin.img,
      coin.x,
      coin.y,
      coin.radius,
      coin.radius * 1.5
    );
  });
},Copy the code

So how do you do this? Do this. DrawCoins ()

Since to do animation, we definitely need to know the window. RequestAnimationFrame 】 【 API


Remember the two steps at the core of animation

  • Empty canvas
  • Draws a new frame of animation

moveCoins() {
  / / empty canvas
  this.ctx.clearRect(0.0.this.innerWidth, this.innerHeight);
  // Draw a new frame
  this.drawCoins();
  // Keep drawing to animate
  this.moveCoinAnimation = window.requestAnimationFrame(this.moveCoins);
},Copy the code

At this point, we can actually get the coins moving, but what we need to do is keep dropping lots and lots of coins, so we choose to keep creating new coins as we move, and then push them into this.coinarr

pushCoins() {
  // Randomly generate 1~3 coins at a time
  const random = this.randomRound(3.6);
  let arr = [];
  for (let i = 0; i < random; i++) {
    // Create a new gold object
    const newCoin = {
      x: this.random(
        this.calculatePos(10),
        this.innerWidth - this.calculatePos(150)),// Horizontal random gold should not be close to the edge
      y: 0 - this.calculatePos(Math.random() * 150), // -150 is highly random
      radius: this.calculatePos(120 + Math.random() * 30), // 100 wide size float 15
      img: this.coinObjs[this.randomRound(0.3)].img, // Randomly pick a gold image object, which is already cached when the page is initialized
      speed: this.calculatePos(Math.random() * 7 + 5) // The falling speed is random
    };
    arr.push(newCoin);
  }
  // Each time a new batch of gold objects arr is inserted into the moving gold array this.coinarr
  this.coinArr = [...this.coinArr, ...arr];
  // How often do you generate a batch of coins
  this.addCoinsTimer = setTimeout((a)= > {
    this.pushCoins();
  }, 600);
},Copy the code

Since the initial y position of each coin is at the top of the screen, it looks like coins are constantly being generated and falling

And the way to calculate the size, this is a little bit arbitrary

Finally, putting it all together, the way to start animation is like this

start() {
  this.pushCoins(); // Keep adding coins
  this.moveCoins(); // Gold starts to move
  // Start the 10-second countdown
  this.runCountdownTimer = setInterval((a)= > {
  / /... After 10 seconds, do something to stop the animation
  }, 1000);
},Copy the code


So that’s the end of the motion, so just to summarize what we’ve done

  • 1. Initialize canvas
  • 2. Cache gold coin pictures and generate gold coin objects, each of which contains its own information
  • 3. Keep generating gold objects and adding them to the array to iterate overthis.coinArr 
  • 4, throughwindow.requestAnimationFrame, each frame is redrawn with canvasthis.coinArrAnd it changes every framethis.coinArrThe size of the y value of each object inside, forming a sense of motion


3, judge the click position, bubble +1 effect

As you can see from the above illustration, when you click on a coin, the corresponding coin will disappear (if there is overlap, only the top coin will disappear), and there will be a +1 effect, and slowly move up and disappear


Think about the logic first

  • 1. Bind click events
  • 2, calculate the position, traverse the current entire gold array, see which gold click on, find the top one, and then delete the gold object
  • 3. Draw a +1 effect at the click position


First of all, Canvas itself is a DOM object, and the gold drawn on it is not a DOM object and cannot be bound to click events, so it can only be bound to canvas, and the click position can be obtained through events, which is a bit like event proxy

    listenClick() {      const canvas = document.getElementById("canvas");      canvas.addEventListener("click", e => {        const pos = {          x: e.clientX,          y: e.clientY        };      });    },Copy the code


Now that we have the click location, the current coin array this.coinarr also knows that each coin object in the array maintains its own information, including the location and coin size

So, as long as you walk through, if the click position is within the size of the gold coin, is it ok to click on the gold coin?

// Determine if the click position is within a coin
isIntersect(point, coin) {
  const distanceX = point.x - coin.x;
  const distanceY = point.y - coin.y;
  const withinX = distanceX > 0 && distanceX < coin.radius;
  // We only count the bottom half of the square, not the tail of the coin
  const withinY =
    distanceY > 0 &&
    distanceY > coin.radius * 0.5 &&
    distanceY < coin.radius * 1.5;
  return withinX && withinY;
},Copy the code


But, at the same time, it’s possible to hit a lot of overlapping gold coins, so let’s go through all of these gold coins, just the top one

listenClick() {
  const canvas = document.getElementById("canvas");
  canvas.addEventListener("click", e => {
    // Click the position
    const pos = {
      x: e.clientX,
      y: e.clientY
    };
    // All gold points are stored here
    const clickedCoins = [];
    this.coinArr.forEach((coin, index) = > {
      // Determine if the click position is within the gold range
      if (this.isIntersect(pos, coin)) {
        clickedCoins.push({
          x: e.clientX,
          y: e.clientY,
	  // The index is important to delete the coin in this.coinarrindex: index }); }});// If you click on an overlapping coin, just take the first coin and delete the first coin and increase the count only once
    if (clickedCoins.length > 0) {
      this.count += 1;
      const bubble = {
        x: clickedCoins[0].x,
        y: clickedCoins[0].y,
        opacity: 1
      };
      // This is related to generating the +1 bubble effect, which will be covered in a moment
      this.bubbleArr.push(bubble);
      // Remove the first gold object in the dot
      this.coinArr.splice(clickedCoins[0].index, 1); }}); },Copy the code


Now that you have the current position, it should not be difficult to draw a bubble effect in the current position, as long as you handle the bubble movement and disappearance, essentially the same as drawing gold above

  • 1. Save onethis.bubbleArrArray, an animation that iterates over and draws its objectsbubble
  • 2,bubbleWith location information, add one more transparencyopacityAs you move, you keep decreasing the transparency until you get to zerobubbleDelete it from the array

drawBubble() {
  this.bubbleArr.forEach((ele, index) = > {
    if (ele.opacity > 0) {
      // Transparency gradient
      this.ctxBubble.globalAlpha = ele.opacity;
      this.ctxBubble.drawImage(
        this.bubbleImage,
        ele.x,
        ele.y,
        this.calculatePos(60),
        this.calculatePos(60));// Update: Reduce opacity by 0.02 each time you finish drawing while moving position
      const newEle = {
        x: ele.x + this.calculatePos(1),
        y: ele.y - this.calculatePos(2),
        opacity: ele.opacity - 0.02
      };
      this.bubbleArr.splice(index, 1, newEle); }}); }, keepDrawBubble() {this.ctxBubble.clearRect(0.0.this.innerWidth, this.innerHeight);
  None: 0; // None: 0
  this.bubbleArr.forEach((ele, index) = > {
    if (ele.opacity < 0) {
      this.bubbleArr.splice(index, 1); }});this.drawBubble();
  this.bubbleAnimation = window.requestAnimationFrame(this.keepDrawBubble);
},Copy the code


4. Performance test

Here, the core principle of the whole movement is finished, let’s test the performance of animation

In chrome’s performance test, the FPS remained steady at 60 frames per second, which is pretty good




The latter

Thank you for your patience to see here, hope to gain!

I like to keep records during the learning process and share my accumulation and thinking on the road of front-end. I hope to communicate and make progress with you. For more articles, please refer to [amandakelake’s Github blog].