Передмова
У статті створимо UI, що дозволяє перетасовувати карти у випадковому порядку.
Повна версія застосунку:
Контекст
Оскільки стаття розрахована на розробників, які не знайомі з Vue, ми детально оглянемо такі пункти:
- Створення елементів карт.
- Екземпляр (instance) Vue.
- Алгоритм тасування Фішера-Єтса.
- Переходи (transitions) у Vue.
Підготовка застосунку
Відправним пунктом нашого застосунку будуть два файли — index.html
та styles.css
.
index.html
index.html
представлятиме розмітку основної сторінки нашого застосунку і починатиметься так:
<html>
<head>
<title>Vue Transitions - Shuffle a Deck of Cards</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel='stylesheet' href='https://web.archive.org/web/20230325135840/https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css'>
<link rel='stylesheet' href='https://web.archive.org/web/20230325135840/https://use.fontawesome.com/releases/v5.0.6/css/all.css'>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="app">
<h1 class="title">
<img class="vue-logo" src="https://vuejs.org/images/logo.png" />
Card Shuffling
</h1>
<div class="deck">
<div class="card">
<span class="card__suit card__suit--top">♣</span>
<span class="card__number">A</span>
<span class="card__suit card__suit--bottom">♣</span>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</body>
</html>
У тегу <head>
бачимо три посилання: Bulma застосовується у ролі CSS-фреймворку нашого застосунку, Font Awesome, що потрібний для іконок, та файл стилів styles.css
, розміщений у корені проєкту.
Уся розмітка застосунку розташовується всередині елемента <div id="app"></div>
, що у тегу <body>
. У кореневому елементі id="app"
на цей момент існує два дочірні елементи, які мають початкову розмітку: <h1 class="title"></h1>
та <div class="deck"></div>
.
У кінці тега <body>
створено єдиний елемент <script>
, який підвантажує Vue з Content Delivery Network (CDN).
Використання CDN для завантаження залежностей Vue — один із найпростіших та найшвидших способів використати його у застосунку.
styles.css
Файл styles.css
вміщує увесь користувацький CSS, який є необхідним для застосунку. Коли ми створюємо розмітку, просто оголошуємо кожен елемент з відповідними атрибутами класу. Так ми зосереджуємося на самому Vue.
На цьому етапі наш застосунок складається лише із заголовка та одного статичного елемента карти.
HTML-секція нашого Codepen представляє користувацький інтерфейс застосунку (наприклад, елемент <div id="app"></div>
) оскільки всі залежності (Bulma/Vue/JS та ін.) вводяться ззовні через редактор Codepen.
Створюємо елементи карти
Спершу нам необхідно представити список елементів для усієї колоди карт. Кожен такий елемент буде показуватись у простий спосіб, з невеликою кількістю розмітки:
<div class="card">
<span class="card__suit card__suit--top">♣</span>
<span class="card__number">A</span>
<span class="card__suit card__suit--bottom">♣</span>
</div>
Враховуючи вже встановлені стилі, наведена розмітка буде мати приблизно такий вигляд:
Стандартна колода карт містить чотири масті (♣, ♦, ♥, ♠), кожна з них складається з тринадцяти різних рангів: туз, числа від одного до десяти, валет, дама і король. Тобто 52 унікальні карти в колоді.
Такий стандартний тип колоди найбільш поширений і відомий як «Стандартна французька колода». Існують також інші варіації з різними розмірами колоди та унікальними картами (наприклад, з Джокером).
Для початкового стану ми розміщуємо карти в колоді, розмежовуючи між чотирма мастями. Кожна секція масті буде показувати карти в порядку зростання (наприклад, від туза до короля).
Оголошуємо статично 52 елементи різних карт у файлі index.html
. Однак такий спосіб призведе до великої кількості повторюваного коду, який буже важко підтримувати, тому будемо намагатись показати колоду карт динамічно.
Екземпляр Vue
Насамперед створимо основу нашого застосунку — екземпляр Vue. Він приймає параметри об'єкта, які вміщують деталі екземпляру: шаблон, дані, методи тощо.
У новому файлі main.js
ми створимо екземпляр Vue кореневого рівня та оголосимо DOM-елемент з id="app"
, на якому базуватиметься наш застосунок.
new Vue({
el: '#app',
});
Для динамічного рендеру неперетасованої колоди карт у нашому застосунку ми визначимо деякі дані в екземплярі. Проініціалізуємо три різні властивості — rank
, suits
та cards
. Властивості rank
та suits
призначені для показування усіх можливих рангів та мастей, які містяться в колоді. Властивість cards
ініціалізується порожнім масивом та буде заповнюватись динамічно при запуску застосунку.
Код буде таким:
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
},
});
Нам необхідно, щоб елементами масиву cards
була повна колода карт у неперемішаному стані. Створимо метод під назвою displayInitialDeck()
, що використає з цією метою масиви ranks
та suits
. Метод буде всередині властивості methods
екземпляру.
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
},
methods: {
displayInitialDeck() {},
}
});
Щоб заповнити масив cards
, необхідно оголосити елемент карти для кожного рангу в кожній масті. Усі представлені елементи будуть об'єктами, що складатимуться з властивостей id
, rank
та suit
. Ці властивості будуть динамічно прив'язані до шаблону, який ми побачимо незабаром.
У методі displayInitialDeck()
:
- По-перше, оголошуємо змінну
id
, що застосовуватиметься для кожного об'єктуcard
, який ми створимо. Проініціалізуємо зміннуid
значенням 1. - Встановимо значення
cards
як пустий масив, щоб переконатися, що ми починаємо з чистого аркуша. (Зауважте: це знадобиться нам пізніше, коли викличемо методdisplayInitialDeck()
після створення колоди). - Далі створюємо вкладений цикл
for
, щоб пробігтися кожним елементом уranks
для кожного елемента зsuits
. На кожній ітерації будемо створювати об'єкт карти та встановлювати необхідні властивості. - Усередині вкладеного циклу додамо оброблений об'єкт картки до
cards
та збільшимо на 1 властивістьid
.
У результаті метод displayInitialDeck()
буде таким:
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
},
methods: {
displayInitialDeck() {
let id = 1;
this.cards = [];
for( let s = 0; s < this.suits.length; s++ ) {
for( let r = 0; r < this.ranks.length; r++ ) {
let card = {
id: id,
rank: this.ranks[r],
suit: this.suits[s]
}
this.cards.push(card);
id++;
}
}
},
},
});
Наш метод не повинен повертати this.cards
, оскільки змінні реактивні автоматично (коли this.cards
змінюється — зображення перезавантажується).
Однак displayInitialDeck()
необхідно викликати на самому початку, коли наш застосунок створюється. Тож викличемо метод у нескінченному циклі екземпляру [created()
, який є методом, що запускається, коли компонент або екземпляр створюється у перший раз.
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
},
created() {
this.displayInitialDeck();
},
methods: {
displayInitialDeck() {
// ...
},
},
});
Тепер, коли наш застосунок створено, властивість cards
буде заповнено 52 картами у впорядкованому форматі. Отримуємо HTML-шаблон, який може показати ці карти. Оскільки ми прагнемо показати список елементів, що базується на основі джерела даних, використаємо директиву v-for з Vue native.
Візуалізація списку карт
У файлі index.html
оголошуємо директиву v-for
у розмітці, пов'язаній зі створенням елемента карти (наприклад, елемент <div class="card"></div>
).
Директива v-for
вимагає синтаксис item
в масиві items
. Оголосимо оператор з аліасом card
як item
, який циклічно обробляється. Для кожного елемента card
ми пов'язуємо значення рангу та масті card
до елемента. Тобто наш index.html
доповнюється так:
<html>
<head>
// ...
</head>
<body>
<div id="app">
// ...
<div class="deck">
<div v-for="card in cards" :key="card.id"
class="card"
:class="{ 'black': card.suit === '♠' || card.suit === '♣',
'red': card.suit === '♥' || card.suit === '♦' }">
<span class="card__suit card__suit--top">{{ card.suit }}</span>
<span class="card__number">{{ card.rank }} </span>
<span class="card__suit card__suit--bottom">{{ card.suit }}</span>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script src="./main.js"></script>
</body>
</html>
Тепер ми посилаємось на нещодавно створений файл main.js у тегу <script>
.
Треба звернути увагу на такі пункти:
- Ми використовуємо синтаксис Mustache, щоб прив'язати значення
card.suit
таcard.rank
до шаблону. - Щоб визначити унікальність кожного зображуваного елементу карти, прив'язуємо атрибут
key
до значенняcard.id
для кожного елемента карти. Оскільки використовуються динамічні значення, застосовується скорочений синтаксис директивиv-bind
для зв'язування нашогоkey
зcard.id
. - Ми також застосовуємо умовну прив'язку класу до елемента
card
за допомогою директивиv-bind
. Наш умовний клас стверджує, що ми додамо клас.black
до елемента зі значеннямcard.suit
♠ або ♣ Якщо жcard.suit
має значення ♦ або ♥, додаємо клас.red
.
Хоча умовна прив'язка класу, яку ми встановили вище, працює добре, ми можемо зробити код трохи читабельнішим. Замість того щоб логіка класу card
була вказана в шаблоні самостійно, ми можемо ввести властивість data
, яка допоможе нам. Представляємо об'єкт suitColor
у полі data
нашого екземпляру:
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
suitColor: {
'♠': 'black',
'♣': 'black',
'♦': 'red',
'♥': 'red',
},
},
created() {
this.displayInitialDeck();
},
methods: {
displayInitialDeck() {
// ...
},
},
});
У показаному списку елементів тепер можемо визначити умовну прив'язку класу виразом :class="suitColor[card.suit]"
:
<html>
<head>
// ...
</head>
<body>
<div id="app">
// ...
<div class="deck">
<div v-for="card in cards" :key="card.id"
class="card"
:class="suitColor[card.suit]">
<span class="card__suit card__suit--top">{{ card.suit }}</span>
<span class="card__number">{{ card.rank }} </span>
<span class="card__suit card__suit--bottom">{{ card.suit }}</span>
</div>
</div>
</div>
// ...
</body>
</html>
Тепер, запустивши наш застосунок, побачимо усю колоду у початковому стані.
Застосунок на цьому етапі буде таким:
Тасування Фішера-Єтса
shuffleDeck( )
Наші карти візуалізуються як елементи масиву cards
. Правильне перемішування спричиняє зміну елементів у масиві у випадковому порядку. Перестановка елементів у масиві — це просто, однак перестановка у випадковому порядку вимагає деяких зусиль.
Хоча існує багато способів перетасовування, не всі вони є ефективними, якщо необхідна випадковість. Надійним та стандартним методом є алгоритм Фішера-Єтса (також відомий як алгоритм Кнута).
Для ліпшого ознайомлення з тасуванням Фішера-Єтса, перегляньте цю статтю.
Ми не будемо вдаватися у подробиці, а лише оглянемо як організувати тасування Фішера-Єтса. По-перше, створимо метод, що буде відповідати за це і матиме назву shuffleDeck()
:
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
suitColor: {
'♠': 'black',
'♣': 'black',
'♦': 'red',
'♥': 'red',
},
},
created() {
this.displayInitialDeck();
},
methods: {
displayInitialDeck() {
// ...
},
shuffleDeck() {},
},
});
Будемо описувати метод shuffleDeck()
крок за кроком. Алгоритм Фішера-Єтса полягає у циклічному переборі та перетасуванні кожного елемента у масиві даних. Для початку створимо цикл for
, що пробіжиться кожним елементом масиву cards
. Проініціалізуємо лічильник циклу значенням this.cards.length — 1
, а далі на кожному кроці будемо виконувати його декремент, доки не буде досягнуто 0:
shuffleDeck() {
for(let i = this.cards.length - 1; i > 0; i--) {
}
}
Спочатку у перетасуванні буде залучено випадкові елементи. Цього можна досягнути, генеруючи випадкове число між 0 та довжиною ітерованого масиву за допомогою Math.floor(Math.random() * i)
. Ми отримаємо випадковий індекс у діапазоні між 0 та довжиною масиву, що залишилася для тасування. Присвоїмо це випадкове число змінній randomIndex
:
shuffleDeck() {
for(let i = this.cards.length - 1; i > 0; i--) {
let randomIndex = Math.floor(Math.random() * i);
}
}
Далі звертаємось до елемента в масиві, який обробляємо у циклі (тобто поточний елемент cards
), та обміняємо значення цього елемента зі значенням елемента, отриманим за індексом randomIndex
. Щоб реалізувати такий обмін:
- Присвоюємо змінній
temp
значення поточного елементаcards
. - Присвоюємо елементу масиву з поточним індексом значення елемента з
randomIndex
. - Присвоюємо елементу з
randomIndex
значення змінноїtemp
, яка насправді містить значення елемента масиву з поточним індексом.
Спробуємо реалізувати викладене у коді:
shuffleDeck() {
for(let i = this.cards.length - 1; i > 0; i--) {
let randomIndex = Math.floor(Math.random() * i);
let temp = this.cards[i];
this.cards[i] = this.cards[randomIndex];
this.cards[randomIndex] = temp;
}
}
Коли справа доходить до оновлення користувацького інтерфейсу з впровадженими змінами, такий спосіб не спрацює. Причиною є те, що Vue не в змозі підхопити зміни, які ми зробили, коли прямо встановлювали значення елементу масиву або змінювали його довжину. Аби мати можливість прямо оновлювати значення елементу даних масиву, нам необхідно застосувати метод Vue.set()
.
Vue.set()
приймає три аргументи:
- масив, що оновлюється;
- значення індексу елементу, що оновлюватиметься;
- нове значення.
Наприклад, можна замінити перше значення в масиві suits
на 🦆 ось так:
Vue.set(this.suits, 0, 🦆);
У нашому методі shuffleDeck()
ми використаємо Vue.set()
два рази для того, щоб здійснити обмін необхідних елементів. Доповнений метод shuffleDeck()
буде таким:
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
suitColor: {
'♠': 'black',
'♣': 'black',
'♦': 'red',
'♥': 'red',
},
},
created() {
this.displayInitialDeck();
},
methods: {
displayInitialDeck() {
// ...
},
shuffleDeck() {
for(let i = this.cards.length - 1; i > 0; i--) {
let randomIndex = Math.floor(Math.random() * i);
let temp = this.cards[i];
Vue.set(this.cards, i, this.cards[randomIndex]);
Vue.set(this.cards, randomIndex, temp);
}
},
},
* });
Перший метод Vue.set()
змінює значення елемента cards
поточного індексу на значення елемента з randomIndex
. Наступний Vue.set()
змінює значення елемента з індексом randomIndex
на значення змінної temp
.
На цьому все! Тепер можемо створити заклик до дії, що запустить метод shuffleDeck()
.
Однією з важливих нововведень у Vue 3.0 буде можливість прямого оновлення значень елементів масиву без використання Vue.set()
.
Метод shuffle з бібліотеки Lodash реалізовує версію алгоритму тасування Фішера-Єтса.
Кнопка тасування
Створимо кнопку для виклику тасування безпосередньо над колодою карт. У нашій HTML-розмітці, ми розташуємо цей елемент у тегу <div class="main-buttons"></div>
:
<html>
<head>
// ...
</head>
<body>
<div id="app">
<h1 class="title">
// ...
</h1>
<div class="main-buttons">
<button @click="shuffleDeck" class="button is-primary">
Shuffle <i class="fas fa-random"></i>
</button>
</div>
<div class="deck">
// ...
</div>
</div>
// ...
</body>
</html>
Оголосимо слухача подій на цю кнопку за допомогою @click="shuffleDeck"
. Тобто встановлюється обробник події натискання, що викликає метод shuffleDeck()
. Натиснувши на кнопку Shuffle, запускаємо застосунок.
@click
— скорочена версія директивиv-on:click
.
Чудово! З кожним кліком можна помітити, як карти з колоди перемішуються. Хоч карти змінюють порядок, перезавантаження під час кожного тасування відбувається миттєво. Щоб спостерігати за процесом зміни місць, можна застосувати переходи Vue.
Застосунок на цьому етапі:
Переходи
transition-group
Vue пропонує нам декілька різних способів для введення переходів, як от: переходи єдиного вузла, переходи декількох вузлів, де кожної миті показується лише один, та переходи списку елементів. Наша колода карт використовує директиву v-for
для візуалізації списку елементів, тому ми будемо застосовувати List Move Transition, щоб досягти анімації при зміні позиції кожної карти.
Для досягнення цього ми замінимо елемент <div class="decks"></div>
елементом transition-group
, що виступає у якості обгортки для списку v-for
(тобто колоди карт). Оголосивши transition-group
, ми зв'язуємо назву переходу shuffleSpeed
(тобто :name="shuffleSpeed"
) та структуру, яку група переходу повинна показати як елемент div
(тобто tag="div"
):
<html>
<head>
// ...
</head>
<body>
<div id="app">
// ...
<transition-group :name="shuffleSpeed" tag="div" class="deck">
<div v-for="card in cards" :key="card.id"
class="card"
:class="suitColor[card.suit]">
<span class="card__suit card__suit--top">{{ card.suit }}</span>
<span class="card__number">{{ card.rank }} </span>
<span class="card__suit card__suit--bottom">{{ card.suit }}</span>
</div>
</transition-group>
</div>
// ...
</body>
</html>
У нашому екземплярі Vue ми оголосили властивість shuffleSpeed
та встановили за значення рядок shuffleMedium
. Так ми можемо контролювати швидкість переходів, що побачимо незабаром.
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
suitColor: {
'♠': 'black',
'♣': 'black',
'♦': 'red',
'♥': 'red',
},
shuffleSpeed: 'shuffleMedium',
},
created() {
this.displayInitialDeck();
},
methods: {
displayInitialDeck() {
// ...
},
shuffleDeck() {
// ...
},
},
});
Коли застосунок завантажується, transition-group
, що огортає колоду карт, матиме атрибут зі значенням shuffleMedium
. На основі назви переходу Vue автоматично розпізнає, чи були задані певні CSS-переходи/анімації. Оскільки ми прагнемо викликати ефект переходу при переміщенні елементів списку, Vue буде шукати зазначений CSS-перехід на рядку shuffleMedium-move
(де shuffleMedium
— ім'я нашої групи переходу).
У файлі styles.css
нашого застосунку, зараз вже існують три класи: shuffleSlow-move
, shuffleMedium-move
та shuffleFast-move
, кожен з яких відповідальний у впровадженні CSS-переходу. Усі класи мають однаковий тип, але різну тривалість переходу.
// Переходи
.shuffleSlow-move {
transition: transform 2s;
}
.shuffleMedium-move {
transition: transform 1s;
}
.shuffleFast-move {
transition: transform 0.5s;
З уже доданими CSS-переходами наші карти тепер будуть виконувати переміщення, коли запущено тасування.
Коли ми натискаємо кнопку Shuffle, вірогідність отримання певного порядку карт унікальна, адже число можливих перестановок становить 52 факторіал, неймовірно велике число. У відео Vsauce усе пояснюється докладніше.
Тепер нам залишається дозволити користувачеві вказувати, з якою швидкістю виконувати тасування.
Швидкість перестановок
Елемент transition-group
спочатку отримує значення для shuffleSpeed
(ініціалізовано як shuffleMedium
). Аби користувач зміг змінювати швидкість переходу, ми, по суті, повинні дозволити зміну значення shuffleSpeed
.
По-перше, створимо три кнопки з назвами Slow
, Medium
, та Fast
. Розташуємо їх всередині елементу <div class="speed-buttons"></div>
, що одразу над кнопкою Shuffle.
<html>
<head>
// ...
</head>
<body>
<div id="app">
<h1 class="title">
// ...
</h1>
<div class="speed-buttons">
<button class="button is-small"
:class="{ 'is-light': shuffleSpeed != 'shuffleSlow' }"
@click="shuffleSpeed = 'shuffleSlow'">
Slow
</button>
<button class="button is-small"
:class="{ 'is-light': shuffleSpeed != 'shuffleMedium' }"
@click="shuffleSpeed = 'shuffleMedium'">
Medium
</button>
<button class="button is-small"
:class="{ 'is-light': shuffleSpeed != 'shuffleFast' }"
@click="shuffleSpeed = 'shuffleFast'">
Fast
</button>
</div>
<div class="main-buttons">
// ...
</div>
<div class="deck">
// ...
</div>
</div>
// ...
</body>
</html>
У кожній з представлених кнопок ми визначили умовну прив'язку класу для того, щоб умовно додати клас .is-light
, зважаючи на значення shuffleSpeed
. .is-light
є класом Bulma, що додає сірий фон до кнопки. Ми просто встановили наші умови, аби вказати, що клас .is-light
треба додати до неактивних кнопок.
Кожен елемент кнопки також має слухача подій, який встановлює значення shuffleSpeed
коректно (наприклад, натиснувши Slow, ви встановлюєте shuffleSpeed
як shuffleSlow
). З різними підготовленими CSS-переходами ми маємо змогу контролювати швидкість тасування:
Оскільки усі елементи кнопок дуже подібні, ми можемо зменшити повторення у нашій HTML-розмітці. Для цього створимо масив shuffleTypes
в екземплярі Vue, який містить усі можливі типи тасування:
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
suitColor: {
'♠': 'black',
'♣': 'black',
'♦': 'red',
'♥': 'red',
},
shuffleSpeed: 'shuffleMedium',
shuffleTypes: ['Slow', 'Medium', 'Fast'],
},
created() {
this.displayInitialDeck();
},
methods: {
displayInitialDeck() {
// ...
},
shuffleDeck() {
// ...
},
},
});
У нашому HTML ми можемо використовувати директиву v-for
для перебору кожного типу тасування та прив'язки відповідної інформації:
<html>
<head>
// ...
</head>
<body>
<div id="app">
<h1 class="title">
// ...
</h1>
<div class="speed-buttons">
<button v-for="type in shuffleTypes"
class="button is-small"
:class="{ 'is-light': shuffleSpeed != `shuffle${type}` }"
@click="shuffleSpeed = `shuffle${type}`">
{{ type }}
</button>
</div>
<div class="main-buttons">
// ...
</div>
<div class="deck">
// ...
</div>
</div>
// ...
</body>
</html>
Так наша розмітка стає чистішою при збереженні усієї інформації.
Ми майже завершили застосунок! Наостанок додамо кнопку Reset, аби користувач мав змогу скидати перетасовану колоду до первинного стану, та лічильник для підрахунку кількості здійснених тасувань.
Reset & Лічильник
Кнопка Reset
Щоб мати можливість повернути колоду карт до первинного стану, реалізуємо кнопку Reset поруч з кнопкою Shuffle.
<html>
<head>
// ...
</head>
<body>
<div id="app">
<h1 class="title">
// ...
</h1>
<div class="main-buttons">
<button v-if="isDeckShuffled" @click="displayInitialDeck" class="button is-primary is-outlined">
Reset <i class="fas fa-undo"></i>
</button>
<button @click="shuffleDeck" class="button is-primary">
Shuffle <i class="fas fa-random"></i>
</button>
</div>
<div class="deck">
// ...
</div>
</div>
// ...
</body>
</html>
У нещодавно доданій кнопці Reset ми оголосили v-if="isDeckShuffled"
для елемента. v-if
є директивою, яка умовно показує елемент на основі істинності зазначеного виразу. Так ми стверджуємо, що кнопка Reset повинна показуватись, коли властивість isDeckShuffled
правдива. Ми використовуватимемо цю властивість лише для того, щоб кнопка Reset з'являлася лише тоді, коли колоду було перетасовано (тобто коли колода не в початковому стані).
Оголосимо властивість isDeckShuffled
для нашого екземпляра та проініціалізуємо її значенням false
. У кінці методу shuffleDeck()
, значення isDeckShuffled
зміниться на true
, щоб позначити, що наша колода була перетасована. У методі displayInitialDeck()
ми скинемо властивість isDeckShuffled
до значення false
, оскільки в цьому стані колода більше не перемішується.
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
suitColor: {
'♠': 'black',
'♣': 'black',
'♦': 'red',
'♥': 'red',
},
shuffleSpeed: 'shuffleMedium',
shuffleTypes: ['Slow', 'Medium', 'Fast'],
isDeckShuffled: false,
},
created() {
this.displayInitialDeck();
},
methods: {
displayInitialDeck() {
let id = 1;
this.cards = [];
for( let s = 0; s < this.suits.length; s++ ) {
for( let r = 0; r < this.ranks.length; r++ ) {
let card = {
id: id,
rank: this.ranks[r],
suit: this.suits[s]
}
this.cards.push(card);
id++;
}
}
this.isDeckShuffled = false;
},
shuffleDeck() {
for(let i = this.cards.length - 1; i > 0; i--) {
let randomIndex = Math.floor(Math.random() * i);
let temp = this.cards[i];
Vue.set(this.cards, i, this.cards[randomIndex]);
Vue.set(this.cards, randomIndex, temp);
}
this.isDeckShuffled = true;
}
},
});
Тож тепер маємо змогу скинути перетасовану колоду до первинного стану. Нам залишилось додати лічильник для підрахунку здійснених тасувань.
Послідовні тасування
Введемо властивість shuffleCount
, відповідальну за відстеження кількості послідовних тасувань. Проініціалізуємо її значенням 0. Коли тасування проводиться (тобто викликається shuffleDeck()
), ми інкрементуємо shuffleCount
. Коли ж колода повертається у первинний стан (викликається displayInitialDeck()
), повертаємо значення shuffleCount
до 0.
Тепер наш екземпляр Vue буде таким:
new Vue({
el: '#app',
data: {
ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suits: ['♥','♦','♠','♣'],
cards: [],
suitColor: {
'♠': 'black',
'♣': 'black',
'♦': 'red',
'♥': 'red',
},
shuffleSpeed: 'shuffleMedium',
shuffleTypes: ['Slow', 'Medium', 'Fast'],
isDeckShuffled: false,
shuffleCount: 0,
},
created() {
this.displayInitialDeck();
},
methods: {
displayInitialDeck() {
let id = 1;
this.cards = [];
for( let s = 0; s < this.suits.length; s++ ) {
for( let r = 0; r < this.ranks.length; r++ ) {
let card = {
id: id,
rank: this.ranks[r],
suit: this.suits[s]
}
this.cards.push(card);
id++;
}
}
this.isDeckShuffled = false;
this.shuffleCount = 0;
},
shuffleDeck() {
for(let i = this.cards.length - 1; i > 0; i--) {
let randomIndex = Math.floor(Math.random() * i);
let temp = this.cards[i];
Vue.set(this.cards, i, this.cards[randomIndex]);
Vue.set(this.cards, randomIndex, temp);
}
this.isDeckShuffled = true;
this.shuffleCount = this.shuffleCount + 1;
}
},
});
Додамо елемент <div class="count-section"></div>
у початок нашої розмітки. Цей елемент буде розташований у верхньому правому кутку інтерфейсу та буде зв'язаний зі значенням shuffleCount
, коли ми вкажемо # of Shuffles: {{ shuffleCount }}
.
Наш користувацький інтерфейс (елемент <div id="app"></div>
) у повному обсязі:
<html>
<head>
// ...
</head>
<body>
<div id="app">
<div class="count-section">
# of Shuffles: {{ shuffleCount }}
</div>
<h1 class="title">
<img class="vue-logo" src="https://vuejs.org/images/logo.png" />
Card Shuffling
</h1>
<div class="speed-buttons">
<button v-for="type in shuffleTypes"
class="button is-small"
:class="{ 'is-light': shuffleSpeed != `shuffle${type}` }"
@click="shuffleSpeed = `shuffle${type}`">
{{ type }}
</button>
</div>
<div class="main-buttons">
<button v-if="isDeckShuffled" @click="displayInitialDeck" class="button is-primary is-outlined">
Reset <i class="fas fa-undo"></i>
</button>
<button @click="shuffleDeck" class="button is-primary">
Shuffle <i class="fas fa-random"></i>
</button>
</div>
<transition-group :name="shuffleSpeed" tag="div" class="deck">
<div v-for="card in cards" :key="card.id"
class="card"
:class="suitColor[card.suit]">
<span class="card__suit card__suit--top">{{ card.suit }}</span>
<span class="card__number">{{ card.rank }} </span>
<span class="card__suit card__suit--bottom">{{ card.suit }}</span>
</div>
</transition-group>
</div>
// ...
</body>
</html>
Ми завершили застосунок! З останніми змінами ми можемо відстежувати кількість послідовних тасувань та скидати перетасовану колоду до початкового стану.
Завершений застосунок:
Висновок
Не звертаючись до деталей реалізації стилів, ми розглянули створення користувацького інтерфейсу колоди карт та використали алгоритм Фішера-Єтса.
Повний застосунок, разом з незавершеними версіями, які ми створювали протягом статті, можна знайти у GitHub-репозиторії awesome-fullstack-tutorials.
Ще немає коментарів