preface

In the last article, we built Super Mario Bros based on the DOM architecture, so in this article we will use canvas to upgrade the entire architecture to improve the visual experience of the game. Students who need to learn can view the source code.

Online Experience Address

Considering that some students are not familiar with Canvas. This article will make some general explanation to canvas foundation.

Canvas Basics

The canvas element

The Canvas tag allows us to use JavaScript to draw various types of graphics on web pages. To access the actual drawing interface, we first need to create a context, which is an object that provides the interface for drawing. There are two popular drawing styles: “2D” for 2d graphics and WebGL for 3D graphics via the OpenGL interface.

For example, we can create a context using the getContext method on the < Canvas /> DOM element.

 <body>
   <canvas width="500" height="500" />
 </body>
 <script>
   let canvas = document.querySelector('canvas');
   let context = canvas.getContext('2d');
   context.fillStyle = "yellow";
   context.fillRect(10.10.400.400);
 </script>
Copy the code

We draw a yellow square 400 pixels wide and high with the coordinates of (10, 10) at the top left vertex. The canvas coordinate system (0, 0) is in the upper left corner.

Border drawing

In the canvas interface, the fillRect method is used to fill the rectangle. FillStyle Method used to control the filling shape. Such as

  • monochromatic
context.fillStyle = "yellow";
Copy the code
  • gradient
let canvas = document.querySelector('canvas');
let context = canvas.getContext('2d');
let grd = context.createLinearGradient(0.0.170.0);
grd.addColorStop(0."black");
grd.addColorStop(1."red");
context.fillStyle = grd;
context.fillRect(10.10.400.400);
Copy the code
  • Pattern object
let canvas = document.querySelector('canvas');
let context = canvas.getContext('2d');
let img = document.createElement('img');
img.src = "Https://dss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3112798566, & FM = 26 & gp = 0. 2640650199 JPG";
img.onload = () = > {
  let pattern = context.createPattern(img, 'no-repeat');
  context.fillStyle = pattern;
  context.fillRect(10.10.400.400)}Copy the code

The strokeStyle property is similar to the fillStyle property, but strokeStyle works with the color of the stroke line. The width of a line is determined by the lineWidth property.

Let’s say I want to draw a yellow square with a border width of 6.

let canvas = document.querySelector('canvas');
let context = canvas.getContext('2d');
context.strokeStyle = "yellow";
context.lineWidth = 6;
context.strokeRect(10.10.400.400);
Copy the code

The path

A path is a combination of lines. The moveTo and lineTo functions are frequently used if we want to draw various shapes.

  let canvas = document.querySelector('canvas');
  let context = canvas.getContext('2d');
  context.beginPath();
  for (let index = 0; index < 400; index+=10) {
    context.moveTo(10, index);
    context.moveTo(index, 0);
    context.lineTo(390, index);
  }
  context.stroke();
Copy the code

MoveTo represents the position where our current brush starts, and lineTo represents the line where our brush starts and ends. After the above code is executed, it looks like this:

Of course we can fill in the graphics drawn by lines.

  let canvas = document.querySelector('canvas');
  let context = canvas.getContext('2d');
  context.beginPath();
  context.moveTo(50.10);
  context.lineTo(10.70);
  context.lineTo(90.70);
  context.fill();
  context.closePath();
Copy the code

Draw pictures

In computer graphics, it is usually necessary to distinguish vector graphics from bitmap graphics. Vector graphics are: by giving shape logic to describe a given picture. Bitmap graphics, on the other hand, use pixel data without specifying the actual shape.

A canvasdrawImageMethod allows us to draw pixel data onto the canvas. Pixel data can come fromElement or another canvas.

DrawImage supports passing 9 parameters. Parameters 2 to 5 indicate which image is copied from the source image (x, y, height, width), and parameters 6 to 9 indicate the position of the image to be copied on the Canvas and its width and height.

Below is a summary of Mary’s various poses. We used the drawImage to get her running properly.

let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
let img = document.createElement('img');
img.src = './player_big.png'
let spriteW = 47, spriteH = 58;
img.onload = () = > {
  let cycle = 0;
  setInterval(() = > {
    ctx.clearRect(0.0, spriteW, spriteH);
    ctx.drawImage(img,
     cycle*spriteW, 0, spriteW, spriteH,
     0.0, spriteW, spriteH,
    );
    cycle = (cycle + 1) % 10;
  }, 120);
}
Copy the code

We need to roughly capture Mary’s size and use cycle to lock in Mary’s position in the animation. In composition, we just need to loop the first 8 movements to realize one of Mary’s running movements.

Control switch

Now we can make Mary run to the right, but in the actual game Mary can run left and right. Here there are two solutions: 1. Let’s draw another combination of running to the left in Figure 2. Control the canvas to draw the picture in reverse. The first option is simpler, so we’ll go for the second, more complicated option.

The canvas can call the scale method to adjust the scale and draw. This method takes two parameters, the first to set the horizontal scale and the other to set the vertical scale.

let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
ctx.scale(3.. 5);
ctx.beginPath();
ctx.arc(50.50.40.0.7);
ctx.lineWidth = 3;
ctx.stroke();
Copy the code

Above is a simple application of Scale. We called scale, which stretched the circle horizontally by a factor of 3 and shrank the circle vertically by a factor of 0.5.

If the parameter in scale is negative -1, the shape drawn at x 100 will eventually be drawn at -100. Therefore, to convert the image, we cannot call ctx.scale(-1, 1) just before the drawImage, because the converted image is not visible on the current canvas. There are two solutions: 1. DrawImage 2 when x is set to -50 when we call drawImage. By adjusting the axes, the advantage of this is that we write plots that don’t care about scale changes.

We use rotate to render the drawn graphics and move them around with the Translate method.

  function flip(context, around) {
    context.translate(around, 0);
    context.scale(-1.1);
    context.translate(-around, 0);
  }
Copy the code

The idea is something like this:

If we draw a triangle at positive x, it will be at position 1 by default. Flip to the right to get triangle 2. Flip to the right to get triangle 3 by calling scale. Finally, translate triangle 3 again to get triangle 4, the final shape we want.

  let canvas = document.querySelector('canvas');
  let ctx = canvas.getContext('2d');
  let img = document.createElement('img');
  img.src = './player_big.png'
  let spriteW = 47, spriteH = 58;
  img.onload = () = > {
      ctx.clearRect(100.0, spriteW, spriteH);
      flip(ctx, 100 + spriteW / 2);
      ctx.drawImage(img,
      0.0, spriteW, spriteH,
      100.0, spriteW, spriteH,
      );
  }
Copy the code

Look, we’ve turned him around!

Upgrade super Mario Bros

In the last article, all of our elements were displayed directly through the DOM, so after we’ve learned about Canvas, we can use drawImage to draw elements.

We defined CanvasDisplay instead of DOMDisplay, and in addition to that, we added a track yourself view window, which tells us what level we’re currently in, and I added a flipPlayer property so that even if Mary doesn’t move, It’s still facing the direction of its last movement.

var CanvasDisplay = class CanvasDisplay {
  constructor(parent, level) {
    this.canvas = document.createElement("canvas");
    this.canvas.width = Math.min(600, level.width * scale);
    this.canvas.height = Math.min(450, level.height * scale);
    parent.appendChild(this.canvas);
    this.cx = this.canvas.getContext("2d");

    this.flipPlayer = false;

    this.viewport = {
      left: 0.top: 0.width: this.canvas.width / scale,
      height: this.canvas.height / scale
    };
  }

  clear() {
    this.canvas.remove(); }}Copy the code

The syncState method first evaluates the new view window and then draws it in place.

CanvasDisplay.prototype.syncState = function(state) {
  this.updateViewport(state);
  this.clearDisplay(state.status);
  this.drawBackground(state.level);
  this.drawActors(state.actors);
};
Copy the code
DOMDisplay.prototype.syncState = function(state) {
  if (this.actorLayer) this.actorLayer.remove();
  this.actorLayer = drawActors(state.actors);
  this.dom.appendChild(this.actorLayer);
  this.dom.className = `game ${state.status}`;
  this.scrollPlayerIntoView(state);
};
Copy the code

In contrast to the previous update, we now have to redraw the background with each update. Since the shapes on the canvas are just pixels, there is no good way to move or delete them after drawing. So the only way to update the canvas is to clean it and redraw it.

The updateViewport method is the same as the scrollPlayerIntoView method. It checks if the player is too close to the edge of the view.

CanvasDisplay.prototype.updateViewport = function(state) {
  let view = this.viewport, margin = view.width / 3;
  let player = state.player;
  let center = player.pos.plus(player.size.times(0.5));

  if (center.x < view.left + margin) {
    view.left = Math.max(center.x - margin, 0);
  } else if (center.x > view.left + view.width - margin) {
    view.left = Math.min(center.x + margin - view.width,
                        state.level.width - view.width);
  }
  if (center.y < view.top + margin) {
    view.top = Math.max(center.y - margin, 0);
  } else if (center.y > view.top + view.height - margin) {
    view.top = Math.min(center.y + margin - view.height, state.level.height - view.height); }};Copy the code

When we succeed or fail, we need to clear the current scene, because if we fail, we need to start over, if we succeed, we need to delete the current scene, and draw a new scene.

CanvasDisplay.prototype.clearDisplay = function(status) {
  if (status == "won") {
    this.cx.fillStyle = "rgb(68, 191, 255)";
  } else if (status == "lost") {
    this.cx.fillStyle = "rgb(44, 136, 214)";
  } else {
    this.cx.fillStyle = "rgb(52, 166, 251)";
  }
  this.cx.fillRect(0.0.this.canvas.width, this.canvas.height);
};
Copy the code

Next, we need to paint the walls and lava. First, we iterate over all the walls and bricks in the current view. We used Sprites.png to draw all non-empty wall tiles (walls, lava, coins). In the supplied footage, we have the wall 20px by 20px with an offset of 0 and the lava 20px by 20px with an offset of 20px.

let otherSprites = document.createElement("img");
otherSprites.src = "img/sprites.png";

CanvasDisplay.prototype.drawBackground = function(level) {
  let {left, top, width, height} = this.viewport;
  let xStart = Math.floor(left);
  let xEnd = Math.ceil(left + width);
  let yStart = Math.floor(top);
  let yEnd = Math.ceil(top + height);

  for (let y = yStart; y < yEnd; y++) {
    for (let x = xStart; x < xEnd; x++) {
      let tile = level.rows[y][x];
      if (tile == "empty") continue;
      let screenX = (x - left) * scale;
      let screenY = (y - top) * scale;
      let tileX = tile == "lava" ? scale : 0;
      this.cx.drawImage(otherSprites,
                        tileX,         0, scale, scale, screenX, screenY, scale, scale); }}};Copy the code

Finally, we need to draw the player’s model.

In the previous 8 images, there is a complete movement process. The ninth picture is of the player standing still, and the tenth picture is of the player lifting off the ground. So when the player moves, we need to switch frames every 60ms. The ninth screen is drawn when the player is not moving and the tenth screen is drawn when the player jumps.

  CanvasDisplay.prototype.drawPlayer = function(player, x, y, width, height){
  width += playerXOverlap * 2;
  x -= playerXOverlap;
  if(player.speed.x ! =0) {
    this.flipPlayer = player.speed.x < 0;
  }

  let tile = 8;
  if(player.speed.y ! =0) {
    tile = 9;
  } else if(player.speed.x ! =0) {
    tile = Math.floor(Date.now() / 60) % 8;
  }

  this.cx.save();
  if (this.flipPlayer) {
    flipHorizontally(this.cx, x + width / 2);
  }
  let tileX = tile * width;
  this.cx.drawImage(playerSprites, tileX, 0, width, height,
                                  x,     y, width, height);
  this.cx.restore();
};
Copy the code

For models that are not players, we find the corresponding image based on the offset of the corresponding model.

  CanvasDisplay.prototype.drawActors = function(actors) {
  for (let actor of actors) {
    let width = actor.size.x * scale;
    let height = actor.size.y * scale;
    let x = (actor.pos.x - this.viewport.left) * scale;
    let y = (actor.pos.y - this.viewport.top) * scale;
    if (actor.type === "player") {
      this.drawPlayer(actor, x, y, width, height);
    } else {
      let tileX = (actor.type === "coin" ? 2 : 1) * scale;
      this.cx.drawImage(otherSprites,
                        tileX, 0, width, height, x, y, width, height); }}};Copy the code

The last

ok! At this point, our Super Mario is complete, will be added to some other map elements ~ interested partners can pay attention to oh ~