html5 图片记忆游戏,30分钟完成JavaScript中的记忆游戏

bVbwv47?w=1263&h=417

通过在30分钟内构建一个记忆游戏来学习JS,CSS和HTML!

本教程介绍了一些基本的关于HTML5,CSS3和JavaScript概念。 我们将讨论数据属性,定位,透视,转换,flexbox,事件处理,超时和三元表达式。 读懂此文章不需要大家有许多编程方面的知识。 如果您已经知道HTML,CSS和JS的用途,那就绰绰有余了!

项目结构

让我们在终端中创建项目文件:

🌹 mkdir memory-game

🌹 cd memory-game

🌹 touch index.html styles.css scripts.js

🌹 mkdir img

HTML

连接css和js文件的初始页面模板。

Memory Game

这个游戏有12张卡片。每个卡片由一个名为.memory-card的容器div组成,其中包含两个img元素。第一个代表卡片的front-face(意为正面),第二个代表卡片的back-face(意为背面)。

1460000020103783?w=300&h=214

React

Memory Card

我们可以在Memory Game Repo下载该项目的资源文件。

这组卡片将被包装在section容器元素中。最终代码结果是这样的:

React

Memory Card

React

Memory Card

Angular

Memory Card

Angular

Memory Card

Ember

Memory Card

Ember

Memory Card

Vue

Memory Card

Vue

Memory Card

Backbone

Memory Card

Backbone

Memory Card

Aurelia

Memory Card

Aurelia

Memory Card

CSS

我们将使用一个简单但非常有用的重置,适用于所有项目:

/* styles.css */

* {

padding: 0;

margin: 0;

box-sizing: border-box;

}

box-sizing: border-box属性使元素充满整个边框,因此我们可以跳过数学计算。

通过设置display: flex到body和margin: auto到.memory-game容器,它将垂直地和水平地居中。

.memory-game也将是一个flex-container。默认情况下,里面的元素会缩小宽度来适应这个容器。通过将flex-wrap设置为wrap,flex-items会根据弹性元素的大小进行自适应。

/* styles.css */

body {

height: 100vh;

display: flex;

background: #060AB2;

}

.memory-game {

width: 640px;

height: 640px;

margin: auto;

display: flex;

flex-wrap: wrap;

}

每个卡片的width(意为宽度)和 height(意为高度)都是用calc() CSS函数计算的。我们将width设置为25%,height设置为33.333% ,并从margin(意为边距)中减去10px,来制作三行四张牌。

对于.memory-card子元素,我们添加position: relative,这样我们就可以相对它进行子元素的绝对定位。

把属性front-face和back-face的属性设置为position: absolute,这样就可以从原始位置移除元素,并使它们堆叠在一起。

/* styles.css */

.memory-card {

width: calc(25% - 10px);

height: calc(33.333% - 10px);

margin: 5px;

position: relative;

box-shadow: 1px 1px 1px rgba(0,0,0,.3);

}

.front-face,

.back-face {

width: 100%;

height: 100%;

padding: 20px;

position: absolute;

border-radius: 5px;

background: #1C7CCC;

}

这时页面模版看上去应该是这样:

1460000020103784?w=600&h=490

让我们也添加一个点击效果。:active伪类将在每次点击元素时触发。它引发一个 0.2秒的过渡:

.memory-card {

width: calc(25% - 10px);

height: calc(33.333% - 10px);

margin: 5px;

position: relative;

transform-style: preserve-3d;

box-shadow: 1px 1px 0 rgba(0, 0, 0, .3);

+ transform: scale(1);

}

+.memory-card:active {

+ transform: scale(0.97);

+ transition: transform .2s;

+}

1460000020103785?w=188&h=247

翻转卡片

要在单击时翻转卡片,我们需要向元素添加类别flip(意为翻转)。 为此,让我们使用document.querySelectorAll选择所有的memory-card元素。 然后使用forEach循环遍历它们并附加一个事件监听器。 每次卡片被点击时,都会触发flipCard(意为翻转卡片)功能。 this变量表示被单击的卡片。 该函数访问元素的classList并切换flip类:

// scripts.js

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

function flipCard() {

this.classList.toggle('flip');

}

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

在CSS里flip类把卡片旋转了180度:

.memory-card.flip {

transform: rotateY(180deg);

}

为了产生3D翻转效果,我们将把perspective属性添加到.memory-game中。这个属性用来设置对象与用户在 z轴上的距离。数值越低,透视效果越大。为了达到最佳的效果,让我们设置为1000px:

.memory-game {

width: 640px;

height: 640px;

margin: auto;

display: flex;

flex-wrap: wrap;

+ perspective: 1000px;

}

对于.memory-card元素,我们添加transform-style: preserve-3d属性,这样就把卡片置于在父节点中创建的3D空间中,而不是将其平铺在z = 0平面上(transform-style)。

.memory-card {

width: calc(25% - 10px);

height: calc(33.333% - 10px);

margin: 5px;

position: relative;

box-shadow: 1px 1px 1px rgba(0,0,0,.3);

transform: scale(1);

+ transform-style: preserve-3d;

}

现在,我们需要把transition属性的值设置为transform就可以生成动态效果了:

.memory-card {

width: calc(25% - 10px);

height: calc(33.333% - 10px);

margin: 5px;

position: relative;

box-shadow: 1px 1px 1px rgba(0,0,0,.3);

transform: scale(1);

transform-style: preserve-3d;

+ transition: transform .5s;

}

所以,我们使得卡片可以3D翻转了,耶! 但为什么卡片的另一面不出现呢? 现在,.front-face和.back-face都堆叠在一起,因为它们被绝对定位了。 每个元素都有一个back face(意为背面),这是它front face(意为正面)的镜像。 属性backface-visibility默认为visible(意为可见的),因此当我们翻转卡片时,我们得到的是背面的JS徽章。

1460000020103786?w=187&h=255

为了显示它背面的图像,让我们把backface-visibility: hidden应用到.front-face和.back-face。

.front-face,

.back-face {

width: 100%;

height: 100%;

padding: 20px;

position: absolute;

border-radius: 5px;

background: #1C7CCC;

+ backface-visibility: hidden;

}

如果我们刷新页面并翻转一张卡片,它就消失了!

1460000020103787?w=187&h=255

因为我们把两个图像都隐藏在了背面,所以另一面什么也没有。接下来我们需要把.front-face旋转180度:

.front-face {

transform: rotateY(180deg);

}

现在,我们有了想要的翻转效果!

1460000020103788?w=187&h=255

匹配卡片

现在我们已经完成翻转卡片的功能之后,接下来我们来处理匹配的逻辑。

当我们点击第一张牌时,它需要等待另一张牌被翻转。变量hasFlippedCard和flippedCard将管理翻转状态。如果没有翻转的卡片,hasFlippedCard被设置为true, flippedCard被设置为已点击的卡片。让我们切换toggle方法来add(意为添加):

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));

现在,当用户点击第二张牌时,我们将进入else块。我们会检查一下它们是否匹配。为了做到这一点,我们需要做到能够识别每一张卡片。

每当我们想向HTML元素添加额外的信息时,我们就可以使用数据属性。通过使用以下语法:data-,其中,可以是任何单词,该属性将插入元素的dataset属性中。所以,让我们为每张卡片添加一个data-framework:

+

React

Memory Card

+

React

Memory Card

+

Angular

Memory Card

+

Angular

Memory Card

+

Ember

Memory Card

+

Ember

Memory Card

+

Vue

Memory Card

+

Vue

Memory Card

+

Backbone

Memory Card

+

Backbone

Memory Card

+

Aurelia

Memory Card

+

Aurelia

Memory Card

所以现在我们可以通过访问两个卡片数据集来检查匹配。 让我们将匹配逻辑提取到它自己的方法checkForMatch(),并将hasFlippedCard设置为false。 如果匹配,则调用disableCards()并分离两个卡片上的事件侦听器,以防止再一次翻转。 否则,unflipCards()会将两张卡都恢复成超过1500毫秒的超时,从而删除.flip类:

把全部代码组合在一起:

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));

编写匹配条件的更简练的方法是使用三元运算符。 它由三个块组成。 第一个块是要判断的条件。 如果条件符合就执行第二个块,否则执行的块是第三个:

- if (firstCard.dataset.name === secondCard.dataset.name) {

- disableCards();

- return;

- }

-

- unflipCards();

+ let isMatch = firstCard.dataset.name === secondCard.dataset.name;

+ isMatch ? disableCards() : unflipCards();

锁定

现在我们已经完成了匹配逻辑,接着为了避免同时转动两组卡片,我们还需要锁定它们,否则翻转将会失败。

我们先声明一个lockBoard变量。 当玩家点击第二张卡片时,lockBoard将设置为true,条件 if (lockBoard) return;在卡片被隐藏或匹配之前会阻止其他卡片翻转:

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));

点击同一卡片

可能会出现玩家在同一张卡上点击两次的情况。 如果匹配条件判断为true,从该卡片上删除事件侦听器。

1460000020103789?w=600&h=391

为了防止这种情况,需要检查当前点击的卡片是否等于firstCard,如果是肯定的则返回。

if (this === firstCard) return;

变量 firstCard和 secondCard需要在每一轮之后被重置,所以让我们将它提取到一个新方法 resetBoard()中, 再其中写上 hasFlippedCard = false;和 lockBoard = false。ES6的解构赋值功能 [var1, var2] = ['value1', 'value2']允许我们把代码写得超短:

function resetBoard() {

[hasFlippedCard, lockBoard] = [false, false];

[firstCard, secondCard] = [null, null];

}

接着我们调用新方法disableCards()和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));

洗牌

我们的游戏现在看起来相当不错,但是如果不能洗牌就失去了乐趣,所以我们现在来处理这个功能。

当 display: flex在容器上被声明时,flex-items会按照组和源的顺序进行排序。 每个组由order属性定义,该属性包含正整数或负整数。 默认情况下,每个flex-item都将其order属性设置为0,这意味着它们都属于同一个组,并将按源的顺序排列。 如果有多个组,则首先按组升序顺序排列。

游戏中有12张牌,因此我们将迭代它们,生成0到12之间的随机数并将其分配给flex-item order属性:

function shuffle() {

cards.forEach(card => {

let ramdomPos = Math.floor(Math.random() * 12);

card.style.order = ramdomPos;

});

}

view raw

为了调用shuffle函数,让它成为一个立即调用函数表达式(IIFE),这意味着它将在声明后立即执行。 脚本应如下所示:

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));

看之后

点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓-_-)

关注公众号「新前端社区」,享受文章首发体验!

每周重点攻克一个前端技术难点。

1460000020039302?w=1080&h=732

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值