Красивая игра на память, созданная с помощью Svelte и TypeScript

В процессе создания своего сайта-портфолио я подумал, что было бы здорово добавить небольшую игру на запоминание, в которую пользователь мог бы играть, и решил, что это может стать отличным забавным проектом, чтобы познакомить некоторых любопытных программистов с этим замечательным компилятором/фреймворком под названием Svelte.

Предварительный просмотр того, что мы собираемся создать:

Предварительный просмотр

В этом уроке мы будем использовать SvelteKit и Tailwind CSS — это очень просто настроить, две команды и готово.

Откройте терминал и напишите эту команду:

npm init svelte your-app-name
Войти в полноэкранный режим Выйти из полноэкранного режима

Svelte задаст вам пару вопросов, для целей этого руководства вы захотите использовать TypeScript, ESLINT и Prettier для форматирования кода, а PlayWright мы можем пропустить, так как не собираемся создавать тесты.

Чтобы установить tailwind для вашего проекта, вы можете просто использовать эту команду:

npx svelte-add@latest tailwindcss
Войти в полноэкранный режим Выйти из полноэкранного режима

Все готово!

Прежде чем приступить к кодированию, будет полезно записать все, что мы ожидаем от нашей программы, давайте подытожим:

  • позволить игроку раскрывать карты, нажимая на них
  • в любой момент времени разрешается раскрывать не более двух карт
  • если две карты раскрыты, то выполняется проверка
  • если две карты имеют одинаковый символ, то это пара
  • если нет, то накройте их снова
  • если все карты раскрыты, игрок выигрывает.

КРАЙНИЕ СЛУЧАИ

  • если карта уже раскрыта, не закрывайте ее
  • если две карты уже раскрыты и пользователь нажимает еще раз, ничего не делайте.

Запись может показаться лишним шагом, но это действительно помогает представить все компоненты, которые могут понадобиться, и найти большинство проблем, с которыми вы можете столкнуться.

Я собираюсь поместить весь игровой код в компонент «Game», поэтому внутри папки «src» я создам папку «lib», которая будет содержать все мои файлы. Папка «lib» полезна в SvelteKit, потому что она предоставляет то, что мы называем «псевдонимом», то есть, если у вас есть файл, который хранится в папке «SvelteKit».

"src/lib/components/game/Game.svelte"
Вход в полноэкранный режим Выйти из полноэкранного режима

вы можете импортировать его, сказав:

import Game from '$lib/components/game/Game.svelte'
Enter fullscreen mode Выйти из полноэкранного режима

Итак, теперь ваш файл «/src/routes/index.svelte» должен выглядеть примерно так:

<script lang="ts">
    import Game from '$lib/components/Game.svelte';
</script>

<Game />

<style>
    :global(body) {
        background-color: rgb(15, 23, 42);
    }
</style>

Войти в полноэкранный режим Выход из полноэкранного режима

Тег :global(body) просто говорит svelte, что текущее правило, примененное к «body», будет применено к каждому файлу в проекте — поэтому для каждой страницы фоном будет
«rgb(15, 23, 42)»

Теперь давайте создадим файл «Game.svelte» в «/src/lib/components».


Посмотрев на список вещей, которые должна делать наша программа, мы можем увидеть все переменные, которые нам понадобятся для реализации базовой версии игры, поэтому давайте создадим их:

  • shouldShow -> поможет нам с начальной анимацией при первой загрузке
  • currentlyUncovered -> помогает отслеживать, какие записи открыты
  • isWon -> позволяет нам узнать, закончена игра или нет
  • pairedCards -> отслеживает, какие записи являются парными
  • cards -> все карты с их состоянием

переведены в код:

<script lang="ts">
    let shouldShow: boolean = false;
    let currentlyUncovered: Array<Card> = [];
    let isWon: boolean = false;

    $: pairedCards = cards.filter((f) => f.paired);

    interface Card {
        symbol: string;
        paired: boolean;
        covered: boolean;
        id: number;
    }

    let cards: Array<Card> = [
        { symbol: '👻', paired: false, covered: true, id: Math.random() },
        { symbol: '💩', paired: false, covered: true, id: Math.random() },
        { symbol: '🐊', paired: false, covered: true, id: Math.random() },
        { symbol: '🐳', paired: false, covered: true, id: Math.random() },
        { symbol: '🦥', paired: false, covered: true, id: Math.random() },
        { symbol: '🍄', paired: false, covered: true, id: Math.random() },

        { symbol: '👻', paired: false, covered: true, id: Math.random() },
        { symbol: '💩', paired: false, covered: true, id: Math.random() },
        { symbol: '🐊', paired: false, covered: true, id: Math.random() },
        { symbol: '🐳', paired: false, covered: true, id: Math.random() },
        { symbol: '🦥', paired: false, covered: true, id: Math.random() },
        { symbol: '🍄', paired: false, covered: true, id: Math.random() }
    ];
</script>
Войти в полноэкранный режим Выйти из полноэкранного режима

Давайте разберем это на части — если вы, как и я, не знакомы с TypeScript, это очень просто.

let currentlyUncovered: Array<Card> = [];

«Array» в основном говорит, что currentlyUncovered будет массивом, и он будет содержать элементы типа «Card», тип «Card» не существует в javascript, это не типичный тип — но это тип, который мы создали сами парой строк ниже:

    interface Card {
        symbol: string;
        paired: boolean;
        covered: boolean;
        id: number;
    }
Вход в полноэкранный режим Выйти из полноэкранного режима

Это говорит о том, что наша карточка является объектом и содержит следующие ключи: «символ» (это строка), «paired» (это булево значение), «covered» и так далее…

Это практически только объявление типов для каждой записи в нашем объекте Card.

$: pairedCards = cards.filter((f) => f.paired);

Эта строка может быть довольно сложной для понимания, если вы не знакомы со Svelte, «$:» — это то, что мы называем «реактивный оператор».

Это объявление переменной (если она еще не объявлена), которое, по сути, говорит: Каждый раз, когда что-то меняется в этом выражении, я буду реагировать на это.

так что в данном случае каждый раз, когда результат

cards.filter((f) => f.paired)

будет меняться, значение pairedCards будет отражать результат.

Это невероятно мощный инструмент, который может быть обоюдоострым мечом, поскольку вы можете себе представить, как трудно отлаживать реактивные операторы в большом приложении, если ими злоупотреблять.

Небольшое замечание по поводу «Math.random()» для нашего идентификатора — для целей данного руководства это сработает, у нас только 16 карт в нашем массиве и вероятность того, что идентификатор будет дублироваться, очень мала… но если вы хотите быть абсолютно уверены, что этого не произойдет, вы можете использовать библиотеку вроде uuid для генерации уникального идентификатора.


Разметка

Все готово для нашей базовой установки, и мы можем перейти к созданию базовой HTML структуры.

Предварительные требования

Я использую библиотеку под названием Confetti Explosion для создания красивого визуального эффекта, когда игра выиграна, это делает весь процесс немного более веселым. Вот как ее установить:

npm install svelte-confetti-explosion

и вот как его использовать:

                    <ConfettiExplosion
                        particleCount={200}
                        force={0.7}
                    />
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь, когда Confetti Explosion установлен и готов, мы можем перейти к созданию разметки для игры.

Вот как это выглядит: (не волнуйтесь, я разберу это ниже)


{#if shouldShow}
    <div
        transition:scale={{ duration: 400 }}
        class="game absolute w-max z-50 p-8 bg-gray-900 shadow-3xl rounded-3xl text-white top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
    >
        <h2 class="text-center font-extrabold mb-8 text-3xl text-gray-200 tracking-tighter">Memory</h2>
        <div class="game-grid relative grid grid-cols-3 gap-1">
            {#if isWon}
                <div class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
                    <ConfettiExplosion particleCount={200} force={0.7} />
                </div>
                <div
                    class="absolute w-full top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2  confetti-explosion mx-auto text-center py-16"
                >
                    <div class="win-message flex flex-wrap justify-center">
                        <p class="text-2xl w-full font-light tracking-tighter italic mb-0">congratulations</p>
                        <h2 class="text-6xl w-full font-black tracking-tighter mb-4">YOU WON!</h2>
                        <div class="flex gap-3">
                            <button
                                class="text-white bg-transparent border border-white hover:bg-white hover:text-black transition-all py-1 px-4 rounded-md cursor-pointer w-max"
                                on:click={playAgain}>Play Again</button
                            >
                        </div>
                    </div>
                </div>
            {/if}

            {#each cards as card}
                <div
                    class:uncovered={!card.covered}
                    class:covered={card.covered}
                    class="card text-gray-600 cursor-pointer w-32 h-32 {isWon
                        ? 'opacity-0 pointer-events-none'
                        : ''} {card.covered
                        ? 'bg-gray-800 hover:bg-blue-800 hover:text-white'
                        : 'bg-white'} rounded-3xl p-4 relative"
                    on:click={() => handleUncovering(card)}
                >
                    <div
                        class="absolute symbol top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-6xl"
                    >
                        {#if card.covered}
                            <span
                                in:fade={{ duration: 500, delay: 200 }}
                                out:fade={{ duration: 0, delay: 0 }}
                                class="text-6xl font-black"
                            >
                                ?
                            </span>
                        {:else}
                            <div
                                in:fade={{ duration: 500, delay: 200 }}
                                out:fade={{ duration: 0, delay: 0 }}
                                class="symbol"
                            >
                                {card.symbol}
                            </div>
                        {/if}
                    </div>
                </div>
            {/each}
        </div>
    </div>
{/if}

<style>
    .uncovered {
        transform: rotateY(180deg);
    }
    .card {
        transition: 0.8s all ease;
    }
    .covered {
        transform: rotateY(0deg);
    }
</style>


Вход в полноэкранный режим Выход из полноэкранного режима

Как вы можете видеть, весь блок обернут в оператор if.

{#if shouldShow}{/if}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это сделано для того, чтобы у нас был переход при установке компонента.

Прямо под ним первый div, содержащий всю игру, имеет установленную шкалу перехода. Он будет выполняться, когда мы изменим «shouldShow» с false на true.

Давайте теперь сосредоточимся на блоке «each».

Мы перебираем наши карты, и нам нужно убедиться, что они имеют два разных состояния: покрытое и непокрытое.

Мы можем сделать это с помощью CSS-трансформации и повернуть их по оси Y.

Как вы можете видеть, мы применили эти условные классы к div

class:uncovered={!card.covered}
class:covered={card.covered}
Войти в полноэкранный режим Выход из полноэкранного режима

Это «условные классы», то есть класс «uncovered» будет добавлен к div, если текущая карта не закрыта, а «covered» будет добавлен, если карта закрыта.

Если вы не знакомы с циклами, то вот откуда берется «card»: когда мы открыли блок each {#each cards as card} -> «cards» — это наш объект с 16 карточками, а «card» — это экземпляр, который мы сейчас перебираем в цикле «each».

стоит обратить внимание и на это:

class="card text-gray-600 cursor-pointer w-32 h-32 {isWon
                        ? 'opacity-0 pointer-events-none'
                        : ''} {card.covered
                        ? 'bg-gray-800 hover:bg-blue-800 hover:text-white'
                        : 'bg-white'} rounded-3xl p-4 relative"
Войти в полноэкранный режим Выход из полноэкранного режима

если игра выиграна, мы меняем непрозрачность карт на 0 и удаляем все события указателя, чтобы пользователь не мог нажать на них — это потому, что мы хотим сохранить игру на той же высоте и сообщение «ВЫ ВЫИГРАЛИ» в центре (чтобы избежать уменьшения контейнера и прыгучести); Это также можно было бы сделать, установив определенную высоту для div игры, но это просто вопрос предпочтения, я нашел это более простым, особенно если игра должна быть отзывчивой.

on:click={() => handleUncovering(card)}

это просто функция, которую мы скоро напишем, и она передает текущую карту в качестве аргумента.

Обратите внимание, что в событиях мы используем функцию стрелки для передачи аргумента, если бы мы сделали так

on:click={handleUncovering(card)}
Войти в полноэкранный режим Выйти из полноэкранного режима

функция была бы вызвана сразу, а это не то, что нам нужно в данном случае.

Другие примечания к разметке таковы:

                                in:fade={{ duration: 500, delay: 200 }}
                                out:fade={{ duration: 0, delay: 0 }}
Ввести полноэкранный режим Выйти из полноэкранного режима

для обоих состояний — закрытия и открытия — переход out установлен на 0 с задержкой 0. Это потому, что мы хотим, чтобы следующая анимация начиналась сразу же.

В остальном все, что мы делаем, это показываем символ карты, если она раскрыта, и знак вопроса, если она раскрыта.


Логика игры

Наконец-то пришло время построить логику игры — наша функция handleUncovering будет выполнять большую часть работы, но прежде чем мы начнем, нам нужно найти способ перетасовать наши карты, как если бы это была колода карт.

Я нашел хороший алгоритм, который делает именно это.

    // helper function to shuffle array
    function shuffleArray(array: Array<any>) {
        for (let i = array.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
        // reassign entries to trigger an update
        cards = cards;
    }
Вход в полноэкранный режим Выход из полноэкранного режима

Подробнее об этом можно прочитать здесь

Итак, теперь все, что нам нужно сделать, когда компонент монтируется, это перетасовать массив и установить shouldShow в true, чтобы наш компонент мог отображаться, как показано ниже

    onMount(() => {
        shouldShow = true;
        shuffleArray(cards);
    });
Вход в полноэкранный режим Выйти из полноэкранного режима

Не забудьте импортировать onMount:

import { onMount } from 'svelte'
Enter fullscreen mode Выход из полноэкранного режима

onMount — это хук Svelte, по сути, функция, которая запускается, когда компонент монтируется, если вы пришли из react, это эквивалент старого componentDidMount или нового способа сделать это с помощью useEffect.

Прежде чем приступить к большой функции раскрытия карты, мы можем создать две вспомогательные функции:

  • раскрыть карту
  • сбросить массив currentlyUncovered.

Они не обязательны, но помогут сократить количество повторений в нашем коде.

  // resets the currently uncovered array
  // and reassign cards to itself so svelte knows
  // it needs to re-render
  function reset() {
    currentlyUncovered = [];
    cards = cards;
  }

  // uncovers an card and reassigns cards to let svelte
  // know to re-render
  function uncover(card: Card) {
    if (card?.covered) {
      card.covered = false;
    }
    cards = cards;
  }
Войти в полноэкранный режим Выйти из полноэкранного режима

Не беспокойтесь об этом сейчас, мы только что объявили их, но еще не использовали (не вызывали), мы сделаем это через минуту.

Теперь самое интересное, давайте создадим нашу функцию handleUncovering

сначала объявим ее

function handleUncovering(card: Card) {
  // logic here
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Наша функция будет принимать карту в качестве параметра с ранее объявленным типом Card.

Теперь давайте посмотрим на наш список, который мы создали ранее. Помните, что эта функция запускается каждый раз, когда пользователь нажимает на любую карточку, потому что мы установили обработчик on:click на запись — поэтому давайте начнем выполнять некоторые проверки.

  // if the card is already uncovered, do nothing
  if (pairedCards.find((f) => f === card)) {
    return;     
  }
Вход в полноэкранный режим Выйти из полноэкранного режима

проверяет наш массив pairedCards, реактивный массив, чтобы проверить, находится ли карта, которую мы пытаемся раскрыть, уже там, если да, то мы можем прекратить выполнение (return)

возврат означает прекращение выполнения функции, поэтому ни один из кодов внутри скобок нашей функции не будет выполнен

Давайте добавим еще одну проверку для устранения крайнего случая, мы хотим предотвратить быстрое нажатие кнопки мыши для открытия третьей карты, если две из них уже открыты, вот как мы это сделаем:

        // if you've already uncovered two entries do nothing
        if (currentlyUncovered.length === 2) {
            return;
        }
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь, если ни одна карта в данный момент не раскрыта ИЛИ мы раскрыли только одну карту, мы можем выполнить несколько других проверок:

        // if there is already an uncovered card and the user clicks on the same one again
        // do nothing
        if (!currentlyUncovered.length || currentlyUncovered.length === 1) {
            if (card === currentlyUncovered[0]) {
                return;
            }
            // find the card the user has clicked on
            const cardToPush: any = cards.find((f) => f.id === card.id);
            // uncover the card and push it to the currently uncovered array
            uncover(cardToPush);
            currentlyUncovered.push(card);
        }
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь мы выполняем раскрытие карт, если в данный момент есть 0 или 1 раскрытая карта, то мы продолжаем раскрытие.

Как вы видите, мы находим нужную нам карточку, используя cards.find(), который является методом массива, позволяющим находить элементы, соответствующие переданному выражению

так что

const cardToPush: any = cards.find((f) => f.id === card.id)

будет найден элемент внутри массива cards, который имеет тот же id, что и элемент, на который мы нажали, после того как он будет найден, мы используем нашу вспомогательную функцию, созданную ранее, чтобы раскрыть его.

uncover(cardToPush);
Вход в полноэкранный режим Выйти из полноэкранного режима

и мы также помещаем открытую карту в массив ‘currentlyUncovered’, чтобы мы могли отслеживать открытые карты:

currentlyUncovered.push(card);
Вход в полноэкранный режим Выход из полноэкранного режима

Все раскрытия теперь отсортированы, теперь мы можем работать с ошибками и условиями выигрыша.

На данный момент возможны два варианта развития событий:

  • карты совпадают, в этом случае мы можем пометить их как «парные»
  • карты не совпадают, в этом случае мы снова их закрываем.

вот логика:

// if the user uncovers two entries then start doing checks
  if (currentlyUncovered.length === 2) {
  // if the two entries have the same symbol then they're a match
    if (currentlyUncovered[0].symbol === currentlyUncovered[1].symbol) {
    // loop through them and change their state to 'paired'
      currentlyUncovered.forEach((f) => {
        f.paired = true;
      });

      reset();
    } else {
      // if it's not a match then loop through the uncovered
      // entries and cover them back again
      // timeout to not make it instant, let the user
      // realise they made a mistake
      setTimeout(() => {
        currentlyUncovered.forEach((f) => {
        f.covered = true;
      });

        reset();
      }, 800);
     }
    }
Войти в полноэкранный режим Выйти из полноэкранного режима

Комментарии к этому практически объясняют каждый шаг, единственное, что я хотел бы прокомментировать, это тайм-аут в 800 мс, это сделано только для того, чтобы искусственно задержать переворачивание карт, только для того, чтобы было понятно, что произошла ошибка и карты переворачиваются снова.

Осталось только задать условие выигрыша:

    // determine if the game is won by checking
    // if the user has paired all the entries
    $: if (pairedCards.length === cards.length) {
        setTimeout(() => {
            isWon = true;
        }, 800);
    }
Войти в полноэкранный режим Выйти из полноэкранного режима

Это реактивный оператор, который проверяет длину нашего массива pairedCards, если количество парных карт равно количеству общих карт, то игра завершена, и мы можем перейти к показу экрана выигрыша, установив isWon в true.

ВОТ И ВСЕ! 🥳

Теперь ваша игра завершена, вот как должен выглядеть код:

Тег скрипта

<script lang="ts">
    // import { v4 as uuidv4 } from 'uuid';
    import { onMount } from 'svelte';
    import { fade, scale } from 'svelte/transition';
    import { ConfettiExplosion } from 'svelte-confetti-explosion';

    let currentlyUncovered: Array<Card> = [];
    let isWon: boolean = false;
    let shouldShow: boolean = false;

    // reactive statement to determine how many
    // entries the user has paid
    $: pairedCards = cards.filter((f) => f.paired);

    // determine if the game is won by checking
    // if the user has paired all the entries
    $: if (pairedCards.length === cards.length) {
        setTimeout(() => {
            isWon = true;
        }, 800);
    }

    interface Card {
        symbol: string;
        paired: boolean;
        covered: boolean;
        id: number;
    }

    let cards: Array<Card> = [
        { symbol: '👻', paired: false, covered: true, id: Math.random() },
        { symbol: '💩', paired: false, covered: true, id: Math.random() },
        { symbol: '🐊', paired: false, covered: true, id: Math.random() },
        { symbol: '🐳', paired: false, covered: true, id: Math.random() },
        { symbol: '🦥', paired: false, covered: true, id: Math.random() },
        { symbol: '🍄', paired: false, covered: true, id: Math.random() },

        { symbol: '👻', paired: false, covered: true, id: Math.random() },
        { symbol: '💩', paired: false, covered: true, id: Math.random() },
        { symbol: '🐊', paired: false, covered: true, id: Math.random() },
        { symbol: '🐳', paired: false, covered: true, id: Math.random() },
        { symbol: '🦥', paired: false, covered: true, id: Math.random() },
        { symbol: '🍄', paired: false, covered: true, id: Math.random() }
    ];

    // helper function to shuffle array
    function shuffleArray(array: Array<any>) {
        for (let i = array.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
        // reassign entries to trigger an update
        cards = cards;
    }

    function handleUncovering(card: Card) {
        // if the card is already uncovered, do nothing
        if (pairedCards.find((f) => f === card)) {
            return;
        }
        // if you've already uncovered two entries do nothing
        if (currentlyUncovered.length === 2) {
            return;
        }
        // if there is already an uncovered card and the user clicks on the same one again
        // do nothing
        if (!currentlyUncovered.length || currentlyUncovered.length === 1) {
            if (card === currentlyUncovered[0]) {
                return;
            }
            // find the card the user has clicked on
            const cardToPush: any = cards.find((f) => f.id === card.id);
            // uncover the card and push it to the currently uncovered array
            uncover(cardToPush);
            currentlyUncovered.push(card);
        }

        // if the user uncovers two entries then start doing checks
        if (currentlyUncovered.length === 2) {
            // if the two entries have the same symbol then they're a match
            if (currentlyUncovered[0].symbol === currentlyUncovered[1].symbol) {
                // loop through them and change their state to 'paired'
                currentlyUncovered.forEach((f) => {
                    f.paired = true;
                });

                reset();
            } else {
                // if it's not a match then loop through the uncovered
                // entries and cover them back again
                // timeout to not make it instant, let the user
                // realise they made a mistake
                setTimeout(() => {
                    currentlyUncovered.forEach((f) => {
                        f.covered = true;
                    });

                    reset();
                }, 800);
            }
        }
    }
    // resets the currently uncovered array
    // and reassign entries to itself so svelte knows
    // it needs to re-render
    function reset() {
        currentlyUncovered = [];
        cards = cards;
    }

    // uncovers an card and reassigns entries to let svelte
    // know to re-render
    function uncover(card: Card) {
        if (card?.covered) {
            card.covered = false;
        }
        cards = cards;
    }

    onMount(() => {
        shouldShow = true;
        shuffleArray(cards);
    });

    function playAgain() {
        cards.forEach((f) => {
            f.paired = false;
            f.covered = true;
        });
        pairedCards = [];
        isWon = false;
        cards = cards;
    }
</script>
Вход в полноэкранный режим Выход из полноэкранного режима

Разметка + стиль

{#if shouldShow}
    <div
        transition:scale={{ duration: 400 }}
        class="game absolute w-max z-50 p-8 bg-gray-900 shadow-3xl rounded-3xl text-white top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
    >
        <h2 class="text-center font-extrabold mb-8 text-3xl text-gray-200 tracking-tighter">Memory</h2>
        <div class="game-grid relative grid grid-cols-3 gap-1">
            {#if isWon}
                <div class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
                    <ConfettiExplosion particleCount={200} force={0.7} />
                </div>
                <div
                    class="absolute w-full top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2  confetti-explosion mx-auto text-center py-16"
                >
                    <div class="win-message flex flex-wrap justify-center">
                        <p class="text-2xl w-full font-light tracking-tighter italic mb-0">congratulations</p>
                        <h2 class="text-6xl w-full font-black tracking-tighter mb-4">YOU WON!</h2>
                        <div class="flex gap-3">
                            <button
                                class="text-white bg-transparent border border-white hover:bg-white hover:text-black transition-all py-1 px-4 rounded-md cursor-pointer w-max"
                                on:click={playAgain}>Play Again</button
                            >
                        </div>
                    </div>
                </div>
            {/if}

            {#each cards as card}
                <div
                    class:uncovered={!card.covered}
                    class:covered={card.covered}
                    class="card text-gray-600 cursor-pointer w-32 h-32 {isWon
                        ? 'opacity-0 pointer-events-none'
                        : ''} {card.covered
                        ? 'bg-gray-800 hover:bg-blue-800 hover:text-white'
                        : 'bg-white'} rounded-3xl p-4 relative"
                    on:click={() => handleUncovering(card)}
                >
                    <div
                        class="absolute symbol top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-6xl"
                    >
                        {#if card.covered}
                            <span
                                in:fade={{ duration: 500, delay: 200 }}
                                out:fade={{ duration: 0, delay: 0 }}
                                class="text-6xl font-black"
                            >
                                ?
                            </span>
                        {:else}
                            <div
                                in:fade={{ duration: 500, delay: 200 }}
                                out:fade={{ duration: 0, delay: 0 }}
                                class="symbol"
                            >
                                {card.symbol}
                            </div>
                        {/if}
                    </div>
                </div>
            {/each}
        </div>
    </div>
{/if}

<style>
    .uncovered {
        transform: rotateY(180deg);
    }
    .card {
        transition: 0.8s all ease;
    }
    .covered {
        transform: rotateY(0deg);
    }
</style>
Вход в полноэкранный режим Выйти из полноэкранного режима

Спасибо, что проверили это руководство!

Я надеюсь, что это помогло вам освоить Svelte и Tailwind, я буду делать больше уроков по SvelteKit в виде статей и видео в ближайшие месяцы, так что следите за обновлениями!

Оцените статью
Procodings.ru
Добавить комментарий