做者:疯狂的技术宅
原文: https://medium.freecodecamp.o...
本文首发微信公众号:jingchengyideng
欢迎关注,天天都给你推送新鲜的前端技术文章javascript
本教程使用了HTML5,CSS3和JavaScript的基本的技术。 咱们将讨论数据属性、定位、透视、转换、flexbox、事件处理、超时和三元组。 你不须要在编程方面有太多的知识和经验就能看懂,不过仍是须要知道HTML,CSS和JS都是什么。css
先在终端中建立项目文件:html
🌹 mkdir memory-game 🌹 cd memory-game 🌹 touch index.html styles.css scripts.js 🌹 mkdir img
初始化页面模版并连接 css
文件 js
文件.前端
<!-- 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>
这个游戏有 12 张卡片。 每张卡片中都包含一个名为 .memory-card
的容器 div
,它包含两个img
元素。 一个表明卡片的正面 front-face
,另外一个个表明背面 back-face
。vue
<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>
您能够在这里下载本项目的资源文件: Memory Game Repo。java
这组卡片将被包装在一个 section
容器元素中。 最终代码以下:react
<!-- 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>
咱们将使用一个简单但很是有用的配置,把它应用于全部项目:git
/* styles.css */ * { padding: 0; margin: 0; box-sizing: border-box; }
box-sizing: border-box
属性能使元素充满整个边框,因此咱们就能够不用作一些数学计算了。es6
把 display:flex
设置给 body
,而且把 margin:auto
应用到到 .memory-game
容器,这样可使它将垂直水平居中。github
.memory-game
是一个弹性容器,在默认状况下,里面的元素会缩小宽度来适应这个容器。经过把 flex-wrap
的值设置为 wrap
,会根据弹性元素的大小进行自适应。
/* styles.css */ body { height: 100vh; display: flex; background: #060AB2; } .memory-game { width: 640px; height: 640px; margin: auto; display: flex; flex-wrap: wrap; }
每一个卡片的 width
和 height
都是用 CSS 的 calc()
函数进行计算的。 下面咱们须要制做一个三行四列的界面,而且把 width
设置为 25%
, height
设置为 33.333%
,还要再减去 10px
留足边距.
为了定位 .memory-card
子元素,还要添加属性 position: relative
,这样咱们就能够相对它进行子元素的绝对定位。
把 front-face
and back-face
的position
属性都设置为 absolute
,这样就能够从原始位置移除元素,并使它们堆叠在一块儿。
这时页面模版看上去应该是这样:
咱们还须要添加一个点击效果。 每次元素被点击时都会触发 :active
伪类,它引起一个 0.2秒的过渡:
要在单击时翻转卡片,须要把一个 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
类会把卡片旋转 180deg
:
.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 徽章。
为了显示它背面的图像,让咱们在 .front-face
和 .back-face
中添加 backface-visibility:hidden
。
.front-face, .back-face { width: 100%; height: 100%; padding: 20px; position: absolute; border-radius: 5px; background: #1C7CCC; + backface-visibility: hidden; }
若是咱们刷新页面并翻转一张卡片,它就消失了!
因为咱们将两个图像都藏在了背面,因此另外一面没有任何东西。 因此接下来须要再把 .front-face
翻转180度:
.front-face { transform: rotateY(180deg); }
效果终于出来了!
完成翻转卡片的功能以后,接下来处理匹配的逻辑。
当点击第一张卡片时,须要等待另外一张被翻转。 变量 hasFlippedCard
和 flippedCard
用来管理翻转状态。 若是没有卡片翻转,hasFlippedCard
的值为 true
,flippedCard
被设置为点击的卡片。 让咱们切换到 toggle
方法:
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
:
<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>
这下就能够经过访问两个卡片的数据集来检查匹配了。 下面将匹配逻辑提取到它本身的方法 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,从该卡上删除事件侦听器。
为了防止这种状况,须要检查当前点击的卡片是否等于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; }); }
为了调用 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));
终于完成了!
您还能够在油管找到视频演示:🎬 Code Sketch Channel.
本文首发微信公众号:jingchengyideng欢迎关注,天天都给你推送新鲜的前端技术文章