<main class="app" ng-app="app" ng-controller="AppController" ng-class="{'app-ready': appReady}">

  <div class="app__hud">

    <div class="app__hud__game-info" ng-hide="gameCompleted" ng-click="titleAction()" ng-swipe-left="changeIcons()" ng-swipe-right="changeIcons()">Find the match</div>
    <div class="app__hud__game-completed" ng-show="gameCompleted">
      Congratulations!
    </div>
    <div class="app__hud__game-status">clicks: {{clickCount}}, cards: {{cardsLeft}}</div>
    <button ng-click="restart()" class="app__hud__btn-restart">Restart</button>
  </div>

  <div class="app__cards-container">

    <div class="app__cards-container__card" role="button" ng-repeat="card in list" id="{{ 'card-' + card.id }}">

      <div class="front app__cards-container__card__front" ng-click="click(card, $index)" ng-swipe-right="click(card, $index)" ng-swipe-left="click(card, $index)"> {{card.isKnown ? "" : "?"}}</div>
      <div class="back app__cards-container__card__back">
        <span ng-class="card.theme">{{card.icon}}</span>
      </div>

    </div>

  </div>

</main>
*,
*:before,
*:after {
  box-sizing: border-box;
  outline: none;
}

html,
body {
  height: 100%;
}

$card-size: 60px;
$card-size-bigger-device: 120px;
$device-bigger: 600px;
[ng-cloak] {
  opacity: 0;
}

body {
  min-height: 100%;
  margin: 0;
  font: 20px sans-serif;
  background: linear-gradient(to left, dodgerblue, #345);
}

@for $i from 1 through 8 {
  .theme#{$i} {
    background: hsla(($i - 1)*45, 70%, 50%, .7);
  }
}

button {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  border-bottom: 1px solid currentColor;
  cursor: pointer;
  // Remove blink effect on mobile device button click
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

.app {
  display: block;
  opacity: 0;
  transition: opacity 2s;
  color: white;
  &.app-ready {
    opacity: 1;
  }
  &__hud {
    background: rgba(0, 0, 0, .1);
    text-align: center;
    padding: 1em;
    &__game-info,
    &__game-completed {
      display: inline-block;
      margin-right: 1em;
    }
    &__game-status {
      display: inline-block;
      font-size: 70%;
      margin-right: 1em;
    }
    &__btn-restart {
      font-size: 70%;
    }
  }
  &__cards-container {
    margin: 5px auto 0;
    max-width: 320px;
    @media(min-width: $device-bigger) {
      max-width: $device-bigger;
    }
    display: flex;
    justify-content: center;
    align-items: center;
    flex-flow: row wrap;
    &__card {
      display: inline-block;
      margin: 5px;
      overflow: hidden;
      -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
      user-select: none;
      width: $card-size;
      height: $card-size;
      @media(min-width: $device-bigger) {
        width: $card-size-bigger-device;
        height: $card-size-bigger-device;
      }
      // ngAnimate
      &.ng-leave {
        opacity: 1;
        transform: scale3d(1, 1, 1);
        transition: 2s ease-in-out;
      }
      &.ng-leave.ng-leave-active {
        opacity: 0;
        width: 0;
        margin: 0;
        transform: scale3d(0, 0, 0);
      }
      &.ng-enter {
        transition: 1s ease-in-out;
        opacity: 0;
      }
      &.ng-enter.ng-enter-active {
        opacity: 1;
      }
      @for $i from 1 through 16 {
        &:nth-child(#{$i}) {
          .front {
            animation-delay: #{$i / 1.5}s;
          }
        }
      }
      &__front {
        text-align: center;
        line-height: $card-size;
        @media(min-width: $device-bigger) {
          line-height: $card-size-bigger-device;
        }
        background: rgba(255, 255, 255, .1);
        cursor: pointer;
        animation: front-cover 2s ease-in-out alternate infinite;
        &:hover {
          background: rgba(255, 255, 255, .3);
        }
      }
      &__back {
        text-align: center;
        line-height: $card-size;
        @media(min-width: $device-bigger) {
          line-height: $card-size-bigger-device;
        }
        font-size: 40px;
        @media(min-width: $device-bigger) {
          font-size: 60px;
        }
        > span {
          // Using a span in case we want to do fancy stuff like rotating animation
          display: block;
          animation: back-cover 2s ease-in-out alternate infinite;
        }
      }
    }
  }
}

@keyframes back-cover {
  0% {
    transform: scale(1);
  }
  100% {
    transform: scale(1.2);
  }
}

@keyframes front-cover {
  0% {
    // Play with some animations for the front cover :)
  }
  100% {}
}

@keyframes fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

.ng-enter-stagger {
  transition-delay: 0.1s;
  transition-duration: 0s;
}

.ng-leave-stagger {
  transition-delay: 0.1s;
  transition-duration: 0s;
}
View Compiled
; {
  let
    selected1,
    selected2;

  // http://unicode.org/emoji/charts/full-emoji-list.html
  const iconAnimals = [
    '🐵',
    '🐶',
    '🐺',
    '🐱',
    '🐯',
    '🐴',
    '🐮',
    '🐷',
    '🐗',
    '🐭',
    '🐹',
    '🐰',
    '🐻',
    '🐨',
    '🐼',
    '🐔',
    '🐤',
    '🐦',
    '🐧',
    '🐸',
    //'🐢	',
    //'🐍',
    //'🐳',
    //'🐬',
    //'🐠',
    //'🐙',
  ];
  const iconFruits = [
    '🍇',
    '🍈',
    '🍉',
    '🍊',
    '🍋',
    '🍌',
    '🍍',
    '🍎',
    '🍏',
    '🍑',
    '🍒',
    '🍓',
  ];

  let icons = iconAnimals;

  const app = angular
    // Using ngTouch to remove the 300ms click delay on mobile devices
    .module('app', ['ngRoute', 'ngAnimate', 'ngTouch'])
    .controller('AppController', AppController);

  AppController.$inject = ['$scope', '$timeout', '$animate'];
  function AppController($scope, $timeout, $animate) {

    // This is a prototype, everything is in this controller.
    // For cleaner code you probably would refactor stuff out from here. 
    // I also use DOM manipulation for faster dev (prototyping)

    const vm = this;

    $timeout(() => {
      // Angular DOM has finished rendering
      $scope.appReady = true;
    });

    function showAllCards() {
      $scope.list.forEach((card) => {
        $("#card-" + card.id).flip(true);
      });
    }

    function startNewGame() {

      shuffleArray(icons); // randomize displayed icons        

      $animate.enabled(false); // disable remove animation

      $scope.list = [];

      //$animate.enabled(true); // re-enable animation

      let list = [],
        cardCount = 16;
      for (var i = 1; i <= cardCount; i++) {
        let type = (i % (cardCount / 2)) + 1;
        list.push({
          id: i,
          type: type,
          icon: icons[type - 1],
          theme: 'theme' + type,
          isKnown: false
        });
      }

      shuffleArray(list); // randomize card on the board

      // Update state
      selected1 = selected2 = null;
      $scope.clickCount = 0;

      $scope.cardsLeft = list.length;
      $scope.gameCompleted = false;
      $scope.titleAction = () => {};
      $scope.changeIcons = () => {
        icons = icons === iconFruits ? iconAnimals : iconFruits;
      };

      $timeout(() => {
        // Angular DOM has finished rendering

        $animate.enabled(true); // re-enable animation

        $scope.list = list;

        $timeout(() => {
          // Angular DOM has finished rendering

          // Apply flip to the cards in the DOM
          let $card = $(".app__cards-container__card");
          $card.flip({
            axis: 'y',
            trigger: 'manual',
            reverse: true
          }, () => {
            //callback
          });

          //showAllCards(); // debug
        });
      });
    }

    $scope.clickCount = 0;
    $scope.cardsLeft = 0;
    $scope.gameCompleted = false;
    $scope.restart = () => {
      startNewGame();
    };

    $scope.click = function(card, index) {

      if (card.flipped) return; // dont allow to flip already flipped cards

      $scope.clickCount++;

      // Update state
      card.flipped = true;
      selected2 = selected1;
      selected1 = card;

      // Update UI
      $("#card-" + card.id).flip(true); // use flip api

      if (selected1 && selected2 && selected1.type === selected2.type) {
        // We found a match

        $scope.list.splice($scope.list.indexOf(selected1), 1);
        $scope.list.splice($scope.list.indexOf(selected2), 1);
        selected1 = selected2 = null;

        $scope.cardsLeft = $scope.list.length;

        if ($scope.list.length === 0) {
          // We have finished the game
          $scope.gameCompleted = true;
        }

        return;
      }

      if (selected2) {

        // Flip back the 2nd shown card

        let id = selected2.id;
        ((id) => {
          $timeout(() => {
            $scope.list.forEach((card) => {
              if (card.id === id) {
                // Update state
                card.flipped = false;
                card.isKnown = true;
              }
            });

            $("#card-" + id).flip(false); // use flip api

          }, 800);
        })(id);
      }
    }

    startNewGame();
  }

  /**
   * Randomize array element order in-place.
   * Durstenfeld shuffle algorithm.   
   */
  function shuffleArray(array) {
    for (var i = array.length - 1; i > 0; i--) {
      var j = Math.floor(Math.random() * (i + 1));
      var temp = array[i];
      array[i] = array[j];
      array[j] = temp;
    }
    return array;
  }

};
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. //cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js
  2. //cdn.rawgit.com/nnattawat/flip/v1.0.19/dist/jquery.flip.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.9/angular.min.js
  4. https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.9/angular-route.min.js
  5. https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.9/angular-animate.min.js
  6. https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.9/angular-touch.min.js