Author: Crazy tech geek


The original:
https://medium.freecodecamp.o…


This article first send WeChat messages public number: jingchengyideng welcome attention, every day to push you fresh front-end technology articles


This tutorial uses the basic technologies of HTML5, CSS3 and JavaScript. We’ll discuss data properties, positioning, perspective, transformations, flexbox, event handling, timeouts, and triples. You don’t have to have a lot of programming knowledge or experience to understand this, but you do need to know what HTML, CSS, and JS are.

  • 🕹 Demo: the Memory Game Project

The project structure

First create the project file in the terminal:

🌹 mkdir memory-game interface CD memory-game interface touch index.html styles.css scripts. Js file mkdir img

HTML

Initialize the page template and link the CSS file to the JS file.

<! -- index.html --> <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Memory Game</title> <link rel="stylesheet" href="./styles.css"> </head> <body> <script src="./scripts.js"></script> </body> </html>

There are 12 cards in this game. Each card contains a container div named.memory-card, which contains two img elements. One represents the front-face of the card, and the other represents the back-face.

<div class="memory-card">
  <img class="front-face" src="img/react.svg" alt="React">
  <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
</div>

You can download the resource file for this project here: Memory Game Repo.

This set of cards will be wrapped in a section container element. The final code is as follows:

<! -- index.html --> <section class="memory-game"> <div class="memory-card"> <img class="front-face" src="img/react.svg" alt="React"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/react.svg" alt="React"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/angular.svg" alt="Angular"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/angular.svg"  alt="Angular"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/ember.svg" alt="Ember"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/ember.svg" alt="Ember"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/vue.svg" alt="Vue"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/vue.svg" alt="Vue"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/backbone.svg" alt="Backbone"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/backbone.svg" alt="Backbone"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/aurelia.svg" alt="Aurelia"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/aurelia.svg"  alt="Aurelia"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> </section>

CSS

We will use a simple but very useful configuration and apply it to all projects:

/* styles.css */

* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

Box-sizing: border-box makes the element fill the entire border, so we don’t have to do some math.

Set display: flex to the body and apply margin:auto to the.memory-game container so that it is vertically and horizontally centered.

.memory-game is an elastic container. By default, the elements inside the container are reduced in width to fit the container. By setting the flex-wrap value to wrap, it ADAPTS to the size of the elastic element.

/* styles.css */

body {
  height: 100vh;
  display: flex;
  background: #060AB2;
}

.memory-game {
  width: 640px;
  height: 640px;
  margin: auto;
  display: flex;
  flex-wrap: wrap;
}

The width and height of each card are calculated using the CSS calc() function. Next we need to create a screen with three rows and four columns, set the width to 25%, the height to 33.333%, and subtract 10px to leave a full margin.

To position the.memory-card child, add the position: relative attribute so that we can position the child relative to it.

Setting the position attribute of both front-face and back-face to absolute will remove elements from their original positions and stack them together.

The page template should look like this:

We also need to add a click effect. The active pseudoclass fires each time an element is clicked, which causes a 0.2 second transition:

Flip CARDS

To flip the card on click, you need to add a Flip class to the element. . Therefore, let us use the document querySelectorAll select all the memory card – element, then use the forEach traverse them and attach an event listener. Each time a card is clicked, the flipCard function is triggered, where this represents the card that was clicked. This function accesses the element’s ClassList and switches to the Flip class:

// scripts.js
const cards = document.querySelectorAll('.memory-card');

function flipCard() {
  this.classList.toggle('flip');
}

cards.forEach(card => card.addEventListener('click', flipCard));

The CSS Flip class rotates the card by 180deg:

.memory-card.flip {
  transform: rotateY(180deg);
}

To produce the 3D rollover effect, you also need to add the Perspective property to.memory-game. This property is used to set the distance between the object and the user on the z-axis. The smaller the value, the stronger the perspective effect. For best results, set it to 1000px:

.memory-game {
  width: 640px;
  height: 640px;
  margin: auto;
  display: flex;
  flex-wrap: wrap;
+ perspective: 1000px;
}

Next, add the transform-style: preserve-3D attribute to the.memory-card element, which places the card in the 3D space created in the parent node, rather than tiling it on the z = 0 plane (transform-style).

.memory-card { width: calc(25% - 10px); Height: calc (33.333% - 10 px); margin: 5px; position: relative; Box-shadow: 1px 1px 1px rgba(0,0,.3); box-shadow: 1px 1px 1px rgba(0,0,.3); transform: scale(1); + transform-style: preserve-3d; }

The Transition property is set to transform to create a dynamic effect:

.memory-card { width: calc(25% - 10px); Height: calc (33.333% - 10 px); margin: 5px; position: relative; Box-shadow: 1px 1px 1px rgba(0,0,.3); box-shadow: 1px 1px 1px rgba(0,0,.3); transform: scale(1); transform-style: preserve-3d; + transition: transform .5s; }

Yeah! Now we have a card with a 3D rollover effect, but why isn’t the other side of the card shown? Due to absolute positioning,.front-face and.back-face are now stacked on top of each other. The back face of each element is the mirror image of its front face. The backface-visibility property defaults to visible, so when we flip the card, we get the JS badge on the back.

To show the image on the back of it, let’s add backface-visibility:hidden in.front-face and.back-face.

.front-face,
.back-face {
  width: 100%;
  height: 100%;
  padding: 20px;
  position: absolute;
  border-radius: 5px;
  background: #1C7CCC;
+ backface-visibility: hidden;
}

If we refresh the page and flip a card, it’s gone!

Since we hid both images on the back, there was nothing on the other side. So the next step is to flip the.front-face 180 degrees:

.front-face {
  transform: rotateY(180deg);
}

The effect is finally coming!

Match the CARDS

Once the function of flipping the cards is complete, the matching logic is processed.

When the first card is clicked, wait for the other card to be flipped. The variables hasFlippedCard and flippedCard are used to manage the flip state. If there is no card flip, the hasFlippedCard value is true, and the flippedCard is set to the card that was clicked. Let’s switch to the toggle method:

const cards = document.querySelectorAll('.memory-card'); + let hasFlippedCard = false; + let firstCard, secondCard; function flipCard() { - this.classList.toggle('flip'); + this.classList.add('flip'); + if (! hasFlippedCard) { + hasFlippedCard = true; + firstCard = this; + } } cards.forEach(card => card.addEventListener('click', flipCard));

Now, when the user clicks on the second card, the code will enter the else block, and we’ll check to see if they match. In order to do this, you need to be able to recognize each card.

We use data attributes whenever we want to add additional information to an HTML element. By using the following syntax: data-*, where the * can be any word, it will be inserted into the element’s dataset attribute. So add a data-framework for each card:

<section class="memory-game"> + <div class="memory-card" data-framework="react"> <img class="front-face" src="img/react.svg" alt="React"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> + <div class="memory-card" data-framework="react"> <img class="front-face" src="img/react.svg" alt="React"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> + <div class="memory-card" data-framework="angular"> <img class="front-face" src="img/angular.svg" alt="Angular"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> + <div class="memory-card" data-framework="angular"> <img class="front-face" src="img/angular.svg" alt="Angular"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> + <div class="memory-card" data-framework="ember"> <img class="front-face" src="img/ember.svg" alt="Ember"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> + <div class="memory-card" data-framework="ember"> <img class="front-face" src="img/ember.svg" alt="Ember"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> + <div class="memory-card" data-framework="vue"> <img class="front-face" src="img/vue.svg" alt="Vue"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> + <div class="memory-card" data-framework="vue"> <img  class="front-face" src="img/vue.svg" alt="Vue"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>  + <div class="memory-card" data-framework="backbone"> <img class="front-face" src="img/backbone.svg" alt="Backbone"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> + <div class="memory-card" data-framework="backbone"> <img class="front-face" src="img/backbone.svg" alt="Backbone"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> + <div class="memory-card" data-framework="aurelia"> <img class="front-face" src="img/aurelia.svg" alt="Aurelia"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card">  </div> + <div class="memory-card" data-framework="aurelia"> <img class="front-face" src="img/aurelia.svg" alt="Aurelia"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> </section>

This allows you to check the match by accessing the data sets of the two cards. The matching logic is extracted into its own method checkFormatch () and hasFlippedCard is set to false. If a match is made, disableCards() is called and the event listeners on both cards are separated to prevent another flip. Otherwise, unflipCards() will restore both cards to a timeout of more than 1500 milliseconds, thereby removing the.flip class:

Put the code together:

const cards = document.querySelectorAll('.memory-card');

  let hasFlippedCard = false;
  let firstCard, secondCard;

  function flipCard() {
    this.classList.add('flip');

    if (!hasFlippedCard) {
      hasFlippedCard = true;
      firstCard = this;
+     return;
+   }
+
+   secondCard = this;
+   hasFlippedCard = false;
+
+   checkForMatch();
+ }
+
+ function checkForMatch() {
+   if (firstCard.dataset.framework === secondCard.dataset.framework) {
+     disableCards();
+     return;
+   }
+
+   unflipCards();
+ }
+
+ function disableCards() {
+   firstCard.removeEventListener('click', flipCard);
+   secondCard.removeEventListener('click', flipCard);
+ }
+
+ function unflipCards() {
+   setTimeout(() => {
+     firstCard.classList.remove('flip');
+     secondCard.classList.remove('flip');
+   }, 1500);
+ }

  cards.forEach(card => card.addEventListener('click', flipCard));

A more elegant way of doing conditional matching is to use the ternary operator, which consists of three parts: the first part is the condition to be determined, and if the condition is met, the code in the second part is executed, otherwise the third part is executed:

- if (firstCard.dataset.name === secondCard.dataset.name) {
-   disableCards();
-   return;
- }
-
- unflipCards();

+ let isMatch = firstCard.dataset.name === secondCard.dataset.name;
+ isMatch ? disableCards() : unflipCards();

lock

Now that the matching logic is complete, you need to lock both sets of cards in order to avoid rotating them at the same time, otherwise the roll-over will fail.

Start by declaring a lockBoard variable. When the player clicks the second card, the lockBoard is set to true, with the condition if (lockBoard) return; Prevent other cards from flipping until they are hidden or matched:

const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false; + let lockBoard = false; let firstCard, secondCard; function flipCard() { + if (lockBoard) return; this.classList.add('flip'); if (! hasFlippedCard) { hasFlippedCard = true; firstCard = this; return; } secondCard = this; hasFlippedCard = false; checkForMatch(); } function checkForMatch() { let isMatch = firstCard.dataset.name === secondCard.dataset.name; isMatch ? disableCards() : unflipCards(); } function disableCards() { firstCard.removeEventListener('click', flipCard); secondCard.removeEventListener('click', flipCard); } function unflipCards() { + lockBoard = true; setTimeout(() => { firstCard.classList.remove('flip'); secondCard.classList.remove('flip'); + lockBoard = false; }, 1500); } cards.forEach(card => card.addEventListener('click', flipCard));

Click on the same card

Again, the player can click twice on the same card. If the match condition evaluates to true, the event listener is removed from the card.

To prevent this, you need to check whether the currently clicked card is equal to firstCard, and return if it is.

if (this === firstCard) return;

The variables firstCard and secondCard need to be reset after each round, so let’s extract it into a new method, resetBoard(), and say hasFlipPedCard = false; And lockBoard = false. ES6’s destructuring assignment [var1, var2] = [‘value1’, ‘value2’] allows us to write code that is very short:

function resetBoard() {
  [hasFlippedCard, lockBoard] = [false, false];
  [firstCard, secondCard] = [null, null];
}

Then call the new methods disableCards() and unflipCards() :

const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false; let lockBoard = false; let firstCard, secondCard; function flipCard() { if (lockBoard) return; + if (this === firstCard) return; this.classList.add('flip'); if (! hasFlippedCard) { hasFlippedCard = true; firstCard = this; return; } secondCard = this; - hasFlippedCard = false; checkForMatch(); } function checkForMatch() { let isMatch = firstCard.dataset.name === secondCard.dataset.name; isMatch ? disableCards() : unflipCards(); } function disableCards() { firstCard.removeEventListener('click', flipCard); secondCard.removeEventListener('click', flipCard); + resetBoard(); } function unflipCards() { lockBoard = true; setTimeout(() => { firstCard.classList.remove('flip'); secondCard.classList.remove('flip'); - lockBoard = false; + resetBoard(); }, 1500); } + function resetBoard() { + [hasFlippedCard, lockBoard] = [false, false]; + [firstCard, secondCard] = [null, null]; + } cards.forEach(card => card.addEventListener('click', flipCard));

Shuffle the deck

Our game looks pretty good, but it’s not fun if you can’t shuffle the cards, so deal with this feature now.

When display: flex is declared on a container, flex-items are sorted by group and source order. Each group is defined by the order attribute, which contains positive or negative integers. By default, each flex-item has its order attribute set to 0, which means that they all belong to the same group and will be sorted in order of source. If there are multiple groups, they are sorted first in ascending order.

There are 12 cards in the game, so we’ll iterate over them, generating random numbers between 0 and 12 and assigning them to the flex-item order property:

function shuffle() {
  cards.forEach(card => {
    let ramdomPos = Math.floor(Math.random() * 12);
    card.style.order = ramdomPos;
  });
}

To call the shuffle function, make it an immediate-call function expression (IIFE), which means it will be executed immediately after the declaration. The script should look like this:

const cards = document.querySelectorAll('.memory-card');

  let hasFlippedCard = false;
  let lockBoard = false;
  let firstCard, secondCard;

  function flipCard() {
    if (lockBoard) return;
    if (this === firstCard) return;

    this.classList.add('flip');

    if (!hasFlippedCard) {
      hasFlippedCard = true;
      firstCard = this;
      return;
    }

    secondCard = this;
    lockBoard = true;

    checkForMatch();
  }

  function checkForMatch() {
    let isMatch = firstCard.dataset.name === secondCard.dataset.name;
    isMatch ? disableCards() : unflipCards();
  }

  function disableCards() {
    firstCard.removeEventListener('click', flipCard);
    secondCard.removeEventListener('click', flipCard);

    resetBoard();
  }

  function unflipCards() {
    setTimeout(() => {
      firstCard.classList.remove('flip');
      secondCard.classList.remove('flip');

      resetBoard();
    }, 1500);
  }

  function resetBoard() {
    [hasFlippedCard, lockBoard] = [false, false];
    [firstCard, secondCard] = [null, null];
  }

+ (function shuffle() {
+   cards.forEach(card => {
+     let ramdomPos = Math.floor(Math.random() * 12);
+     card.style.order = ramdomPos;
+   });
+ })();

  cards.forEach(card => card.addEventListener('click', flipCard));

It’s finally done!

You can also find a video demo on YouTube: 🎬 Code Sketch Channel.


This article first send WeChat messages public number: jingchengyideng welcome attention, every day to push you fresh front-end technology articles