Day 07 卡牌垂直重疊

今日完成目標 多張卡牌實現垂直重疊,但露出非交疊的部分 垂直重疊 為了產生重疊的效果,個人覺得最酷的方式應該是使用CSS的grid排版, 所以在這之前我利用一個GRID GARDEN的網站練習了一下。 原本問ChatGPT是得知用每一張卡堆疊都還要套用不同的CSS,如果要多一張牌就要多一個class,或是用sass的寫法達成,但以上這些我都不想要,一來太麻煩、二來sass還要額外引入新依賴,畢竟我只是想堆疊卡牌而已。 接著說明實際我達成的方式是靠display: grid;要求格狀排列,然後設定grid-template-rows為 repeat(13, 3rem);,這樣就可以讓每一張牌所在的格子都只有3rem的高度,設定13是因為我認為這樣垂直重疊排列卡牌最多只有13張,畢竟紙牌接龍不同花色暫時串再一起也就13張,在現實還是虛擬我的認知都是這樣,當然在設定比13高一點也不會影響,但如果出現第14、15張就會有卡牌不重疊,這點還請注意。 那為什麼達成重疊呢?我在容器內裝13個元件,每一個元件都只裝一張牌,且元件高度限制都在3rem,但實際元件 牌的高度是超過3rem,所以當我裝入第二張牌就會擋住第一張牌,讓第一張牌只露出3rem的高度,以此類推,最後一張牌則會露出全部。 // CardColumn.vue <template > <div class="card-box" :class="{ 'empty-card-box': isEmpty }"> <div class="card" style="visibility: hidden;"></div> <div style="visibility: visible; position: absolute; z-index: 1;"> <div v-if="isEmpty">沒牌</div> <div v-else style="; display: grid; grid-template-rows: repeat(13, 3rem);"> <Card v-for="(card, index) in cards" @click="onClick" :key="card.value" :value="card.value" :isOpen="card.isOpen" /> </div> </div> </div> </template> 額外補充可以注意到card-box內第一個元素是用來稱基本的寬高, 所以第二個元素我則讓他設定position: absolute這樣可以讓其中的格狀排列不會受到第一個元素的影響也不會影響到外部元素。 若少掉position: absolute的話,會變成底下這樣: ...

September 15, 2023 · 1 min · 宗嘉

Day 06 實現自訂義牌堆

今日目標 研究如何製作牌堆 製作牌堆 製作CardBox元件負責用來擺放卡片的容器,所以結構上就只是比原本卡片元件的稍寬,所以設計成可放子元件進去的樣板,然後當沒放牌時會產生跟牌一個寬高的隱藏區塊到slot裡面。 // CardBox.vue <template > <div class="card-box"> <slot> <div class="card" style="visibility: hidden;"></div> </slot> </div> </template> 所以目前首頁是長這樣,放了兩個空的CardBox佔位置。 // HomeView.vue <div style="display: grid; grid-template-columns: 1fr 1fr;"> <CardBox></CardBox> <CardBox></CardBox> </div> 接著思考如何實作卡牌從A點發到B點,看著畫面思考發現, 不如今天就來實作兩邊卡堆點擊後會移動到對方卡堆的功能。 這邊為了簡單驗證想法,定義函數geneateDeck(5, true)生成五張卡牌依序是梅花A~5(設為開牌), 我預計實驗兩個卡堆,所以函數也就設計以下兩個非常相似的函數, 功用正是將卡堆的最後一張卡彈出,並推到另一個牌堆。 // HomeView.vue const fstCards = ref(geneateDeck(5, true)); const secondCards = ref([]); const moveCardFromAToB = () => { const card = fstCards.value.pop(); if (card === undefined) return; secondCards.value.push(card); }; const moveCardFromBToA = () => { const card = secondCards.value.pop(); if (card === undefined) return; fstCards.value.push(card); }; 在經過一番折騰,發現CardBox.vue應該要內部自帶Card元件, 由外面用slot的方式注入進去畫面反而不好寫版面,當然也有可能是我排版功力不夠,改由props傳入cards陣列,由外部HomeView.vue處理資料傳遞,但由CardBox.vue負責卡片的顯示。 所以可以看到以下CardBox.vue會判斷當前有牌沒牌渲染不同的樣式。 // CardBox.vue <script setup> import { computed } from 'vue'; import Card from '../components/Card.vue'; const { cards } = defineProps(['cards']); const isEmpty = computed(() => { return cards.length == 0; }); </script> <template > <div class="card card-box" :class="{ 'empty-card-box': isEmpty }"> <div v-if="isEmpty">沒牌啦</div> <div v-else class="card"> <Card v-for="(card, index) in cards" :key="card.value" :value="card.value" :isOpen="card.isOpen" /> </div> </div> </template> 遊戲大廳HomeView.vue負責處理資料的部分依舊如上,但樣板改成傳簡單的卡片陣列進到<CardBox /> ...

September 15, 2023 · 1 min · 宗嘉

Day 05 引入Vue Router切換頁面

因為想留著昨天完成的撲克牌連連看,預計將不同遊戲的頁面可以做保留並切換, 所以我打算在做牌堆之前,首先應該要了解Vue3在路由頁面的實作是如何切換頁面。 引入 Vue Router 接下來步驟我是參考『直接使用npm create vue@latest指令產生預設攜帶有用Vue Router專案的檔案架構』 下去調整的,所以步驟草率請敬請見諒。 安裝依賴Vue Router npm install vue-router@4 在src底下新增兩個資料夾views和router 在views資料夾底下新增兩個頁面檔Game1View.vue和HomeView.vue src ├─App.vue ├─main.js ├─views | ├─Game1View.vue # 撲克連連看 | └─HomeView.vue # 首頁 ├─utils | ├─constants.js | └─poker-helper.js ├─router | └─index.js ├─components | ├─Card.vue | ├─FoxyHeader.vue | ├─GameBoard.vue | 略... 如果想知道是怎麼產生樹狀目錄,我是在src目錄下執行npx treer -e ./result.txt 便會 將樹狀結構寫到當前的result.txt文件中 定義路由應對應的元件,routes的部分可以有預先載入的功能 // router/index.js import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: HomeView }, { path: '/game1', name: 'game1', // Lazy Loading component: () => import('../views/Game1View.vue') } ] }) export default router 接著App.vue修改成以下結構,RouterView就是渲染對應路由的元件,RouterLink則是像<a>的連結會修改網址的對應路由,只是不會重新整理整個網頁,而是動態載入去切換部分畫面、網址。 // App.vue <script setup> import { RouterView, RouterLink } from 'vue-router'; </script> <template> <header> <nav> <RouterLink to="/">Home</RouterLink> <RouterLink to="/game1">撲克牌連連看</RouterLink> </nav> </header> <RouterView /> </template> 如果需要當前路由對應到對應連結時自動套用特定class的話,不一定要在每個<RouterLink>寫:class=""去做判斷,只需要在<style>...</style>中定義class .router-link-active即可 ...

September 14, 2023 · 2 min · 宗嘉

Day 04 調整翻牌效果&實作洗牌&外加撲克牌連連看

改變翻牌效果 研究Transition的文件後發現跟我想像的動畫變換不同,官方用法<Transition></Transition> 包裹內的新舊元件其中一個在動畫過程會先被移除掉或新增,但我先前設計好的CSS會是兩個元素都存在只是一個會被轉到CSS效果轉到背後,因此我的水平翻轉動畫需要兩個元素都存在HTML上。 為了在時間內完成,我打消原本水平翻轉的作法,我改採Transition結合v-if的方式去顯示卡牌正反兩面, 發現用漸進式出現消失也是不錯的效果,以下是樣板的改變: // Card.vue <template> <div class="card " @click="emit('poker-flip', value)"> <Transition name="card-flip"> <div v-if="isOpen" class="card-front" :class="numberClass">{{ content }}</div> <div v-else class="card-back"></div> </Transition> </div> </template> 參考Vue官方的範例, 在style添加以下後綴是-enter-active/leave-active/-enter-from/-leave-to的class,是為了讓Transition知道動畫要如何改變。 // Card.vue <style scoped> .card-flip-enter-active, .card-flip-leave-active { transition: all 0.5s ease-out; } .card-flip-enter-from, .card-flip-leave-to { opacity: 0; } {/* 略 */} 實作洗牌 實作兩個函數存於utils/poker-helper.js 洗牌的函數shuffle,洗牌採用的演算法參考看別人文章實作 Fisher-Yates 演算法,雖然洗牌用其他方法也可以,但看參考的文章說這個時間複雜度最低。 function shuffle(deck) { let length = deck.length; for (let i = 0; i < length; i++) { let rand_to_swap = Math.floor(Math.random()*(length-i)); let tmp = deck[length-1-i]; deck[length-1-i] = deck[rand_to_swap]; deck[rand_to_swap] = tmp; } return deck; } 產生52張洗亂的牌geneateShuffleDeck,這邊的isDone是為了儲存比對成功, 原本在實作上是會消失,但後來發現連連看的版面會跑掉,所以設計isDone為true時卡片會變成顯示不可點擊的小綠卡。 function geneateShuffleDeck() { let deck = []; for (let i = 0; i < 52; i++) { deck.push({ value: i, isOpen: false, isDone: false }); } return shuffle(deck); } 製作撲克牌連連看 簡單就是52張牌洗亂擺在桌上,翻出兩張牌打開有相同的就等於連上, 因為我記憶力沒多好,所以遊戲規則調整成一次最多可以翻六張,超過張數的話牌就會蓋起來! ...

September 13, 2023 · 1 min · 宗嘉

Day 03 完成卡牌自動翻面的效果,但還不完整

原本今天預計製作發牌和卡牌自動翻面的效果,希望兩個都能有動畫效果, 但現實的能力讓我想先說說今天的開發方式 實際思考/開發的過程 卡牌翻面 想像的實作過程 沿用原本的Card.vue內的樣板,在裡面分成兩個div區塊, 一個div區塊放正面有花色數字的牌面,另一個則放一張背圖。 在Card.vue內撰寫 @click 去改變卡片的狀態 撰寫CSS的動畫去實現翻面的過程 現實開發 先調整撲克牌背面的樣板,花了些時間讓圖片不會超出卡牌寬高 background-size: cover; 上網查詢是怎麼做翻轉撲克的效果,甚至查到有人做出6個div欄位做出3D方塊,但嘗試改成只有前後的div欄位但發現景深會產生兩張卡片交疊的一點位移就放棄這個寫法 意外查到IT邦上有個純CSS挑戰撲克翻轉,所以就先照著原作的CSS翻轉寫法調整Card.vue樣板 並且添加@click 會觸發emit將點到的卡牌數字回傳至上層進行更新 // Card.vue <template> <div class="card " @click="emit('poker-flip', value)" :class="backCardClass"> <div class="card-front" :class="numberClass">{{ content }}</div> <div class="card-back"></div> </div> </template> 因為我Vue3是採用SFC(Single-File Components)的寫法,所以定義emit的方式如下所示: // Card.vue const emit = defineEmits(["poker-flip"]); 在上層的元件要接會使用剛剛定義在defineEmits的名稱 @poker-flip // GameBoard.vue 其他都略 <Card v-for="card in boardCards" key="card.value" :value="card.value" :isOpen="card.isOpen" @poker-flip="toggleFlip" /> 可能因為練習過ReactJS官網的的關係,知道狀態提升(Lifting State Up)的概念,所以目前所有卡牌資料依然是先放在上層的GameBoard.vue裡面,toggleFlip是我撰寫 // GameBoard.vue function toggleFlip(num) { // 找到對應的開牌狀態且翻轉 const targetIdx = boardCards.value.findIndex((item) => item.value === num) boardCards.value[targetIdx].isOpen = !boardCards.value[targetIdx].isOpen; } 今天也有犯一點蠢,發現開牌的狀態一直傳不下去,但toggleFlip函數又有拿到對應的數字和改變的狀態, 原來是GameBoard.vue中寫的 <Card /> 元件沒寫屬性 :isOpen 導致程式沒報錯,但有執行異常的情況… ...

September 12, 2023 · 1 min · 宗嘉