This is the sixth day of my participation in the August Text Challenge.More challenges in August

preface

Who are blessed with special blessings,

But also beware of good fortune.

– Seneca

introduce

You are still depressed about the lack of money, you are still suffering from black face, come here to learn this, so that you can get all kinds of cards ~

The theme of the cold rice king, oh no, is king of games, that is the memory of 80,90 generation, small shop to buy cards together to find partners Versailles their own rare card, and then imagine two people with animation in the picture, is also a section of ting in two memories.

The task we did this time was very simple, using pure Canvas instead of CSS3 and without game engine and plug-in, to make the card flat to fit the landscape screen, and then click and reverse to work together.

This time, we will focus on how the cards are arranged horizontally, how the clicks are detected, and how to make the 3D effect flip, which will be roughly expanded from the infrastructure, card horizontal coordinate processing, card class implementation.

PS: The game is based on the canvas implementation, can not use CSS3, in this way to implement it, so this is only to provide an idea of how to deal with it, if doing small activity page, still suggest using CSS3 preserve-3D mode to change the rotateY of the element is the most convenient.

Set out

Chapter 1: Infrastructure

We still write the HTML first, in which we use module mode, after the aspect of the module load

<style>
    * {
        padding: 0;
        margin: 0;
    }
    html.body {
        width: 100%;
        height: 100vh;
        overflow: hidden;
    }
    #canvas{
        width: 100%;
        height: 100%;
    }
</style><body>
    <canvas id="canvas"></canvas>
    <script type="module" src="./app.js"></script>
</body>
Copy the code

Then, we write the structure of the card class first, and the aspects are introduced in the main logic.

/*Card.js*/
class Card {
  constructor(options) {
    this.name = "";
    this.x = 0;
    this.y = 0;
    return this;
  }
  render(ctx) {}
  draw(){}}export default Card;
Copy the code
/*app.js*/
import Card from "./js/Card.js";
Copy the code

Next we need to prepare some card material, as fake data implementation.

/*app.js*/
const cardData = [{
  name: "back".src: "./assets/back.png"
}, {
  name: "Broken 壊 Shin Ho In fact Dipped ゴ".src: "./assets/ broken 壊 God in fact penetrated ゴ. PNG"
}, {
  name: "White Dragon by Blue Eyes".src: "./assets/ White dragon. PNG"
}, {
  name: "Raijin-mode · プ Mode of transportation" "Raijin-mode · プ Mode of transportation".src: "./assets/ raikmode · プ Mode migration. PNG"
}, {
  name: "V · HERO Hiatus ィ · チ · Mode".src: "./ Assets /V · HERO hiatus チ · Fukushima.png"
}, {
  name: "エ レ ベ 'タ' degrees degrees".src: "./ Assets /エ レ ス マ ス chance · Nitd random ー Spoon."
}, {
  name: "A · O · J (I'm playing) デ fair トロ".src: "./assets/A · O · J (I'm doing something) デ fair トロ - PNG"}, {name:"Great God Bird by Rosy Clouds and Valleys".src: "./assets/ Giant orator.png"}, {name:Conceived by Matuchi matuchi for the chemical industry..src: "./assets/ uchi sprung sprung by uchi chi. PNG"}, {name:"Inda チ roundtable · raijin-raid second".src: List_second "./assets/ inda チ roundtable · raid_enjoyable. PNG"}, {name:"チ hikaristic rotational rotational rotational".src: "./assets/チ hikari · Can be rescued. PNG"
}];
Copy the code

Mobile game cards are actually returned to the foreground data through the back-end algorithm, and then displayed, so we directly write dead here.

With the resource, we can write the main logic, let the main logic load the resource, and then do the feedback:

class Application {
  constructor() {
    this.canvas = null;           / / the canvas
    this.ctx = null;              / / environment
    this.w = 0;                   / / canvas width
    this.h = 0;                   / / the canvas
    this.textures = new Map(a);/ / texture set
    this.spriteData = new Map(a);// Sprite data
    this.cardList = [];           // Card array
    this.progress = 0;            // Load progress [0-1]
    this.init();
  }
  init() {
    / / initialization
    this.canvas = document.getElementById("canvas");
    this.ctx = this.canvas.getContext("2d");
    window.addEventListener("resize".this.reset.bind(this));
    this.reset();
    cardData.forEach(card= > {
      this.textures.set(card.name, card.src);
    })
    this.load().then(this.render.bind(this));
  }
  reset() {
    // Screen changes events
    this.w = this.canvas.width = this.ctx.width = window.innerWidth;
    this.h = this.canvas.height = this.ctx.height = window.innerHeight;
    if (this.progress >= 1) this.render();
  }
  render() {
    / / the main rendering
    this.cardList.length = 0;
    this.step();
  }
  load() {
    // Load the texture
    const { textures, spriteData } = this;
    let n = 0;
    return new Promise((resolve, reject) = > {
      if (textures.size == 0) {
        this.progress = 1;
        resolve();
      }
      for (const key of textures.keys()) {
        let _img = new Image();
        spriteData.set(key, _img);
        _img.onload = () = > {
          this.progress += (1 / textures.size);
          if (++n == textures.size) {
            this.progress = 1; resolve(); } } _img.src = textures.get(key); }})}drawCard() {
    // Draw the cards in the card array
    this.cardList.forEach(card= >{ card.draw(); })}drawBackground() {
    // Draw the background
    const { w, h, ctx} = this;
    ctx.fillStyle = "# 000";
    ctx.fillRect(0.0, w, h);
  }
  step(delta) {
    / / redraw
    const { w, h, ctx } = this;
    requestAnimationFrame(this.step.bind(this));
    ctx.clearRect(0.0, w, h);
    this.drawBackground();
    this.drawCard()
  }
}
​
window.onload = new Application();
Copy the code

Just like before, we put the image data into texture set inside, and then load them one by one, added progress variables to see the loading schedule, because of the screen to change the situation, we will empty card set, to map the position of the card, is that the progress is the premise of doing these 1 is full loaded resources will make a change.

After loading, we go to the main render, where we will later deal with the generation of cards into the deck. We first draw a black background, walk through the deck to draw each card, and then redraw the event continuously.

· Card layout

We expect cards to be horizontal and wrap if they exceed the screen width. This works fine if you do elastic or float in CSS, but how does this work in Canvas?

We first add the card generation content to the main logic

render() {
    const { ctx, spriteData, w } = this;
    this.cardList.length = 0;
    let scale = 0.225;
    let n = 1;
    while (n < cardData.length) {
        let item = cardData[n];
        let img = spriteData.get(item.name);
        let size = Math.max(1.Math.floor(w / (img.width * scale + 20)));
        let i = n - 1;
        let x = 20 * (i % size + 1) + (i % size) * img.width * scale;
        let y = 20 * Math.ceil(n / size) + Math.floor(i / size) * img.height * scale;
        let card = new Card({
            name: item["name"],
            scale,
            x,
            y,
            img,
            back: spriteData.get("back")
        }).render(ctx)
        this.cardList.push(card);
        n++;
    }
    this.step();
}
Copy the code

The material image that we’ve got is actually quite large, so we’re going to have to scale it, and we’re going to scale it in card later. But the implementation has to define the scaling factor. Next, the variable n as image resources use count, because starting from 1 0 is card back pictures, each all have, so we traverse is he different positive, we expect that the incoming his name x y coordinate positive figure on the back of the zoom level, this also is we can think of is at present normal logic, let his rendering. Instantiated.

Now the key question is how do we get his x and y coordinates?

Size = (width of canvas)/(width of image after scaling + white space). At the same time, ensure that there is at least one number.

Then we can use it to calculate his abscissa in turn, that is, the sum of the white space and the sum of the width of the picture after scaling. Then we can use the size just calculated to take the modulus, let it break the line and calculate from the beginning. Similarly, the y-coordinate does the same thing, but we just divide to get the current number of rows.

Chapter 3 · Card category

/*Card.js*/
class Card {
  constructor(options) {
    this.name = "";                     / / name
    this.x = 0;                         // X coordinates
    this.y = 0;                         // Y coordinates
    this.back = null;                   // The back of the card
    this.img = null;                    // Card front
    this.speed = 0.02;                  // Flip speed
    this.scale = 1;                     // Scale
    Object.assign(this, options);
    this.ctx = null;                    / / environment
    this.timer = null;                  / / timer
    this.scaleX = this.scale;           // x scale
    this.scaleY = this.scale;           // y-scale
    this.isActive = false               // Whether to activate
    return this;
  }
  render(ctx) {
    / / the main rendering
    if(! ctx)return;
    this.ctx = ctx;
    this.draw();
     return this;
  }
  show() {
    / / show
    if(this.isActive) return;
    this.isActive = true;
    this.timer = setInterval(() = > {
      this.scaleX -= this.speed;
      if (this.scaleX <= 0) {
        this.isShow = true;
      }
      if (this.scaleX <= -this.scale ) {
        this.scaleX = -this.scale;
        clearInterval(this.timer);
        this.timer = null; }},1000 / 60)}draw() {
    / / to draw
    if(this.isShow)
      this.drawCard();
    else  
      this.drawBack();   
  }
  drawCard() {
    // Draw the front side
    const {ctx, img, x, y, scaleX, scaleY,scale} = this;
    ctx.save();
    ctx.translate(x+img.width*(scale-scaleX)/2, y);
    ctx.scale(scaleX, scaleY);
    ctx.drawImage(img, 0.0);
    ctx.restore();
  }
  drawBack() {
    // Draw the back side
    const {ctx, x, y, scaleX, scaleY, back, img,scale} = this;
    ctx.save();
    ctx.translate(x+img.width*(scale-scaleX)/2, y);
    ctx.scale(scaleX, scaleY);
    ctx.drawImage(back, 0.0); ctx.restore(); }}Copy the code

So what we do is we use isShow to determine whether to draw heads or tails, and isActive is to determine whether it’s already active, and once it’s active, it can’t be changed, it’s a one-way state.

We’re going to talk a lot about how to draw it, and we probably started with the rotate of the canvas. However, this form only works for 2d horizontal rotation. And we do the card flip in 3D. You can’t do that, and at this point, we’re reminded of another algorithm, which is the perspective algorithm, where you zoom in and out to simulate what it looks like.

So the following operations are written:

/*Card.js*/
show() {
    if(this.isActive) return;
    this.isActive = true;
    this.timer = setInterval(() = > {
        this.scaleX -= this.speed;
        if (this.scaleX <= 0) {
            this.isShow = true;
        }
        if (this.scaleX <= -this.scale ) {
            this.scaleX = -this.scale;
            clearInterval(this.timer);
            this.timer = null; }},1000 / 60)}Copy the code

Since we are rotating along the y axis, we need to change the scale of the x axis appropriately so that the back side is scaled to 0 and the front side is restored to the original opposite value.

But, it’s not enough just to change this, because it’s just going to flip around the zero point, and what we want is the center point, but the most important thing for the center point of the x axis is to keep calculating the difference between the current size and the original size, and keep shifting it by that distance, and trick the naked eye into thinking that it’s not moving, it’s flipped around in its original position.

/*Card.js*/
ctx.translate(x+img.width*(scale-scaleX)/2, y);
Copy the code

Final chapter · Capture cards

We have written, now only how can let the mouse trigger his flip effect ~

/*app.js*/
init() {
  + canvas.addEventListener("click".this.clickHandle.bind(this), false)}clickHandle(e) {
    let offsetX = e.offsetX;
    let offsetY = e.offsetY;
    let card = this.cardList.find(card= > {
        const { img, x, y, scale } = card;
        return (offsetX > x && offsetX < x + img.width * scale) && (offsetY > y && offsetY < y + img.height * scale)
    })
    card && card.show();
}
Copy the code

First, we need to monitor his click event, bind should method, and then judge the current coordinate after each mouse click. Here, we use the enclosing rectangle determination method.

Peripheral rectangle determination method: refers to that if the detected object is a rectangle or approximate rectangle, we can abstract the object into a rectangle, and then use the method of judging whether the two rectangles collide to detect. In simple terms, we treat the object as a rectangle.

Our card is a rectangle and we match its size according to its horizontal and vertical coordinates, whether the mouse point is in the range or not, and then we sift through the clicked card and flip it.


So we’re done, isn’t it that easy? You’re done, online demo

Expansion and extension

The flip that we do in pseudo-3D is actually an extension of perspective, if you want to do, say, a 3D placement game, and you want to simulate it with a 2D canvas. Of course, if there is a lot of demand, or obediently use some game engine to develop, we do not need to calculate coordinates and anchor points frequently.

Here only to provide ideas, more creative and ideas need to use their own hands to achieve ~


Finally, to the previous students in Versailles, what you bought is pirated ~ haha ~~