做者:瘋狂的技術宅
原文: 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歡迎關注,天天都給你推送新鮮的前端技術文章