๐ Overview
Memory Unmasked Game is a visually rich and playful memory card matching game. Each card hides an emoji mask that gets revealed through delightful animations. The game tracks tries, combos, and best scores while rewarding the player with animated feedback.
This project focuses on:
- Advanced CSS animations & transitions
- Clean JavaScript game logic
- Fun, emoji-driven UI
- Smooth user experience and game states
๐ง Core Concept
The goal is simple:
Match all pairs of emoji faces with the fewest possible tries.
But the experience is elevated using:
- Animated card flips
- Combo streak effects
- Dynamic marquee messages
- Elastic and spring-like CSS motion
๐ป Code
Below is the complete project code, separated into HTML, CSS, and JavaScript.
๐น HTML
1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5 <meta charset="UTF-8">
6 <title>Memory Unmasked Game | CodewithLord</title>
7 <meta name="viewport" content="width=device-width, initial-scale=1">
8 <link rel="stylesheet" href="https://public.codepenassets.com/css/reset-2.0.min.css">
9 <link rel="stylesheet" href="./style.css">
10</head>
11
12<body>
13 <main>
14 <div class="game">
15 <div class="board is-loading">
16 <button id="alien" class="card">
17 <div class="content">
18 <span class="face">๐</span>
19 <div class="prop">
20 <div class="alien-mask">
21 <span class="mask">๐ฝ</span>
22 <span class="hand">๐ซฐ</span>
23 </div>
24 </div>
25 </div>
26 </button>
27 <button id="ogre" class="card">
28 <div class="content">
29 <span class="face">๐</span>
30 <div class="prop">
31 <div class="ogre-mask">
32 <span class="hand">๐ซธ</span>
33 <span class="mask">๐น</span>
34 <span class="hand">๐ซท</span>
35 </div>
36 </div>
37 </div>
38 </button>
39 <button id="robot" class="card">
40 <div class="content">
41 <span class="face">๐</span>
42 <div class="prop">
43 <div class="robot-mask">
44 <span class="mask">๐ค</span>
45 <span class="hand">๐ซณ</span>
46 </div>
47 </div>
48 </div>
49 </button>
50 <button id="clown" class="card">
51 <div class="content">
52 <span class="face">๐</span>
53 <div class="prop">
54 <div class="clown-mask">
55 <span class="hand">๐ซธ</span>
56 <span class="mask">๐คก</span>
57 <span class="hand">๐ซท</span>
58 </div>
59 </div>
60 </div>
61 </button>
62 <button id="pumpkin" class="card">
63 <div class="content">
64 <span class="face">๐</span>
65 <div class="prop">
66 <div class="pumpkin-mask">
67 <span class="mask">๐</span>
68 <span class="hand">๐ซณ</span>
69 </div>
70 </div>
71 </div>
72 </button>
73 <button id="frog" class="card">
74 <div class="content">
75 <span class="face">๐</span>
76 <div class="prop">
77 <div class="frog-mask">
78 <span class="mask">๐ธ</span>
79 <span class="hand">๐ซฐ</span>
80 </div>
81 </div>
82 </div>
83 </button>
84 <button id="skull" class="card">
85 <div class="content">
86 <span class="face">๐</span>
87 <div class="prop">
88 <div class="skull-mask">
89 <span class="mask">๐</span>
90 <span class="hand">๐</span>
91 </div>
92 </div>
93 </div>
94 </button>
95 <button id="cow" class="card">
96 <div class="content">
97 <span class="face">๐</span>
98 <div class="prop">
99 <div class="cow-mask">
100 <span class="mask">๐ฎ</span>
101 <span class="hand">๐ซณ</span>
102 </div>
103 </div>
104 </div>
105 </button>
106 <button id="disguise" class="card">
107 <div class="content">
108 <span class="face">๐</span>
109 <div class="prop">
110 <div class="disguise-mask">
111 <span class="hand">๐ซธ</span>
112 <span class="mask">๐ฅธ</span>
113 <span class="hand">๐ซท</span>
114 </div>
115 </div>
116 </div>
117 </button>
118 <button id="eye" class="card">
119 <div class="content">
120 <span class="face">๐</span>
121 <div class="prop">
122 <div class="eye-mask">
123 <span class="mask">๐๏ธ</span>
124 <span class="hand">๐</span>
125 </div>
126 </div>
127 </div>
128 </button>
129 <button id="dragon" class="card">
130 <div class="content">
131 <span class="face">๐</span>
132 <div class="prop">
133 <div class="dragon-mask">
134 <span class="mask">๐ฒ</span>
135 <span class="hand">๐ซฐ</span>
136 </div>
137 </div>
138 </div>
139 </button>
140 <button id="fox" class="card">
141 <div class="content">
142 <span class="face">๐</span>
143 <div class="prop">
144 <div class="fox-mask">
145 <span class="mask">๐ฆ</span>
146 <span class="hand">๐ซฐ</span>
147 </div>
148 </div>
149 </div>
150 </button>
151 </div>
152 <div class="marquee">
153 <span class="marquee-text">Unmasked!</span>
154 </div>
155 </div>
156 <div class="interface">
157 <div class="interface-data interface-data-tries">Tries: <span class="tries-value">0</span></div>
158 <div class="interface-data interface-data-best">Best: <span class="best-value">0</span></div>
159 <button id="reset-game" class="interface-btn">Reset game</button>
160 </div>
161 </main>
162 <script src="./script.js"></script>
163
164</body>
165
166</html>
๐น CSS
1@import url("https://fonts.googleapis.com/css2?family=Grenze+Gotisch&display=swap"); 2 3:root { 4 --text-color: oklch(98.5% 0 0); 5 --bg-color: oklch(14.5% 0 0); 6 --surface-color: oklch(39.6% 0.141 25.723); 7 8 --button-bg-color: oklch(40.8% 0.123 38.172); 9 --button-hover-bg-color: oklch(47% 0.157 37.304); 10 --button-active-bg-color: oklch(26.6% 0.079 36.259); 11 --button-match-bg-color: oklch(20.5% 0 0); 12 --button-match-active-bg-color: oklch(62.3% 0.214 259.815); 13 --button-border-color: oklch(47% 0.157 37.304); 14 --button-active-border-color: oklch(47% 0.157 37.304); 15 --button-match-border-color: oklch(27.4% 0.006 286.033); 16 --button-match-shine-color: oklch(64.6% 0.222 41.116); 17 18 /* prettier-ignore */ 19 --ease-elastic: linear(0, 1.114 8.5%, 1.37 12.9%, 1.315 16.2%, 0.941 24%, 0.869 27.8%, 0.882 30.7%, 1.012 38.3%, 1.046 42.7%, 0.984, 1.006 72.2%, 1); 20 --ease-out: cubic-bezier(0.25, 1, 0.5, 1); 21 --ease-in-back: cubic-bezier(0.68, -0.6, 0.32, 1); 22} 23 24* { 25 box-sizing: border-box; 26} 27 28html, 29body { 30 height: 100%; 31} 32 33body { 34 display: grid; 35 place-items: center; 36 font-family: system-ui, sans-serif; 37 font-family: "Grenze Gotisch", system-ui; 38 color: var(--text-color); 39 background: var(--bg-color); 40 text-rendering: optimizeLegibility; 41 -webkit-font-smoothing: antialiased; 42 -moz-osx-font-smoothing: grayscale; 43} 44 45main { 46 display: grid; 47 gap: 2rem; 48 inline-size: 85vmin; 49 margin: auto; 50} 51 52.game { 53 position: relative; 54 container-type: inline-size; 55} 56 57.interface { 58 display: flex; 59 flex-wrap: wrap; 60 align-items: baseline; 61 padding-inline: 0.2rem; 62 gap: 1rem; 63 font-size: calc(0.6rem + 2vmin); 64} 65 66.interface-data { 67 display: flex; 68 align-items: center; 69 gap: 0.3rem; 70 line-height: 1; 71 72 span { 73 display: inline-block; 74 position: relative; 75 top: -0.1em; 76 font-size: 1.8em; 77 font-variant-numeric: tabular-nums; 78 letter-spacing: -0.05em; 79 } 80} 81 82.interface-data-best { 83 display: none; 84} 85 86.interface-btn { 87 margin-inline-start: auto; 88} 89 90button { 91 appearance: none; 92 font: inherit; 93 line-height: 1; 94 border: unset; 95 color: inherit; 96 padding: 0.5rem 1rem 0.6rem; 97 border-radius: 0.3rem; 98 background: var(--button-bg-color); 99 border: 1px solid var(--button-border-color); 100 cursor: pointer; 101 touch-action: manipulation; 102 103 &:focus-visible { 104 outline: 1px dashed white; 105 outline-offset: 2px; 106 } 107 108 &:not(:disabled):where(:hover, :focus-visible) { 109 --button-bg-color: var(--button-hover-bg-color); 110 } 111} 112 113.marquee { 114 --deg: -6deg; 115 --duration: 2.2s; 116 display: none; 117 place-content: center; 118 position: absolute; 119 inset: -5%; 120 width: 110%; 121 z-index: 1; 122 font-size: 26cqmin; 123 line-height: 1; 124 white-space: nowrap; 125 transform: rotate(var(--deg)) skew(var(--deg)); 126 pointer-events: none; 127 paint-order: stroke; 128 -webkit-text-stroke: 6cqmin var(--bg-color); 129 animation: marquee-reveal var(--duration) var(--ease-out), 130 marquee-mask-position 3s var(--ease-out); 131 mask-image: linear-gradient(to left, transparent 50%, black 75%); 132 mask-position: 100% center; 133 mask-size: 400% 100%; 134} 135 136.marquee.is-combo { 137 --duration: 600ms; 138 letter-spacing: -0.05em; 139} 140 141.marquee-text { 142 animation: marquee-drift var(--duration) linear forwards; 143} 144 145.board { 146 position: relative; 147 display: grid; 148 grid-template-columns: repeat(4, 1fr); 149 gap: 2cqmin; 150} 151 152.card { 153 position: relative; 154 display: grid; 155 place-items: center; 156 overflow: hidden; 157 padding: 0; 158 font-size: 6cqmin; 159 aspect-ratio: 1; 160 border-radius: 10cqmin; 161 corner-shape: squircle; 162 will-change: transform; 163 transition: opacity 300ms var(--ease-out), background 100ms var(--ease-out); 164 box-shadow: inset 0 0 1cqmin 0.5cqmin var(--button-bg-color), 165 inset 0 0 0 1.5cqmin hsl(0 0% 0% / 0.1); 166 167 &[aria-pressed="true"] { 168 --button-bg-color: var(--button-active-bg-color); 169 --button-border-color: var(--button-active-border-color); 170 } 171 172 &:disabled { 173 --button-bg-color: var(--button-match-bg-color); 174 --button-border-color: var(--button-match-border-color); 175 cursor: revert; 176 color: currentcolor; 177 } 178 179 @supports not (corner-shape: squircle) { 180 border-radius: 7cqmin; 181 } 182} 183 184/* Card highlight effect */ 185.card::before { 186 content: ""; 187 position: absolute; 188 inset: 0; 189 border-radius: inherit; 190 corner-shape: inherit; 191 box-shadow: inset 0 0 4cqmin 3cqmin var(--button-match-shine-color); 192 opacity: 0; 193} 194 195.content { 196 position: relative; 197 margin: auto; 198 pointer-events: none; 199 user-select: none; 200 transform: translateZ(0); 201} 202 203.prop { 204 position: relative; 205 transition: opacity 200ms 500ms var(--ease-out); 206} 207 208.face { 209 display: inline-block; 210 font-size: 1.25em; 211} 212 213.mask { 214 display: inline-block; 215 font-size: 1.8em; 216} 217 218.hand { 219 --opacity-duration: 60ms; 220 display: inline-block; 221 opacity: 0; 222 transform: scale(0.5); 223 transition: transform 0s var(--opacity-duration) var(--ease-out), 224 opacity var(--opacity-duration) var(--ease-out); 225} 226 227.card:is(:hover, :focus-visible, [aria-pressed="true"]) .hand { 228 opacity: 1; 229 transform: scale(1); 230 transition: transform 200ms var(--ease-out), opacity 0s; 231} 232 233/* <br><br><br><br>-- Alien <br><br><br><br>-- */ 234.alien-mask { 235 position: absolute; 236 top: 100%; 237 left: 50%; 238 translate: -50% 0; 239 transform-origin: bottom left; 240 transition: rotate 500ms var(--ease-out); 241} 242 243.alien-mask .mask { 244 position: absolute; 245 bottom: 100%; 246 translate: -25% 10%; 247} 248 249.alien-mask .hand { 250 position: relative; 251} 252 253.card[aria-pressed="true"] .alien-mask { 254 rotate: 45deg; 255 transition-duration: 300ms; 256} 257 258/* -------- Clown -------- */ 259.clown-mask { 260 display: flex; 261 position: absolute; 262 bottom: 0; 263 left: 50%; 264 translate: -50% 15%; 265 transition: translate 300ms var(--ease-out); 266} 267 268.clown-mask .hand { 269 --offset: 92%; 270 --rotate: 10deg; 271 position: absolute; 272 top: 60%; 273 translate: 0 -50%; 274 275 &:first-child { 276 right: var(--offset); 277 rotate: calc(var(--rotate) * -1); 278 } 279 280 &:last-child { 281 left: var(--offset); 282 rotate: var(--rotate); 283 } 284} 285 286.card[aria-pressed="true"] .clown-mask { 287 translate: -50% -55%; 288 transition-duration: 300ms; 289 transition-timing-function: var(--ease-in-back); 290} 291 292/* -------- Cow -------- */ 293.cow-mask { 294 position: absolute; 295 bottom: 0; 296 left: 0; 297 translate: -50% -80%; 298 transform-origin: bottom right; 299 transition: 400ms var(--ease-out); 300 transition-property: translate, rotate; 301} 302 303.cow-mask .mask { 304 position: absolute; 305 top: 30%; 306 translate: 12% 0; 307} 308 309.cow-mask .hand { 310 position: relative; 311 rotate: -10deg; 312} 313 314.card[aria-pressed="true"] .cow-mask { 315 rotate: -15deg; 316 translate: -80% -190%; 317 transition-duration: 200ms; 318} 319 320/* -------- Disguise -------- */ 321.disguise-mask { 322 display: flex; 323 position: absolute; 324 bottom: 0; 325 left: 50%; 326 translate: -50% 15%; 327 transition: 300ms var(--ease-out); 328 transition-property: translate, rotate; 329 transition-origin: bottom left; 330} 331 332.disguise-mask .hand { 333 --offset: 92%; 334 --rotate: 10deg; 335 position: absolute; 336 top: 60%; 337 translate: 0 -50%; 338 339 &:first-child { 340 right: var(--offset); 341 rotate: calc(var(--rotate) * -1); 342 } 343 344 &:last-child { 345 left: var(--offset); 346 rotate: var(--rotate); 347 } 348} 349 350.card[aria-pressed="true"] .disguise-mask { 351 translate: 10% 80%; 352 rotate: 25deg; 353 transition-duration: 300ms; 354 transition-timing-function: var(--ease-in-back); 355} 356 357/* -------- Dragon -------- */ 358.dragon-mask { 359 position: absolute; 360 top: 100%; 361 left: 50%; 362 translate: -50% 0; 363 transform-origin: bottom right; 364 transition: rotate 500ms var(--ease-out); 365} 366 367.dragon-mask .mask { 368 position: absolute; 369 bottom: 80%; 370 left: -40%; 371} 372 373.dragon-mask .hand { 374 position: relative; 375 translate: 55% -10%; 376 rotate: -30deg; 377 scale: -1 1; 378} 379 380.card[aria-pressed="true"] .dragon-mask { 381 rotate: -45deg; 382 transition-duration: 300ms; 383} 384 385/* -------- Eye -------- */ 386.eye-mask { 387 position: absolute; 388 top: 80%; 389 left: 50%; 390 translate: -50% 0; 391 transform-origin: bottom left; 392 transition: 400ms var(--ease-out); 393 transition-property: translate, rotate; 394} 395 396.eye-mask .mask { 397 position: absolute; 398 bottom: 0; 399 translate: -25% -40%; 400} 401 402.eye-mask .hand { 403 position: relative; 404 translate: 100% -80%; 405 rotate: 5deg; 406} 407 408.card[aria-pressed="true"] .eye-mask { 409 rotate: 15deg; 410 translate: 5% -90%; 411 transition-duration: 200ms; 412} 413 414/* -------- Fox -------- */ 415.fox-mask { 416 position: absolute; 417 top: 100%; 418 left: 50%; 419 translate: -50% 0; 420 transform-origin: bottom right; 421 transition: 500ms var(--ease-out); 422 transition-property: translate, rotate; 423} 424 425.fox-mask .mask { 426 position: absolute; 427 bottom: 80%; 428 left: -40%; 429} 430 431.fox-mask .hand { 432 position: relative; 433} 434 435.card[aria-pressed="true"] .fox-mask { 436 rotate: 15deg; 437 translate: 0 100%; 438 transition-duration: 300ms; 439} 440 441/* -------- Frog -------- */ 442.frog-mask { 443 position: absolute; 444 top: 0; 445 left: 50%; 446 translate: -50% -25%; 447 transform-origin: bottom left; 448 transition: rotate 500ms var(--ease-out); 449} 450 451.frog-mask .mask { 452 position: absolute; 453 bottom: 0; 454 left: 0; 455 translate: -24% -30%; 456} 457 458.frog-mask .hand { 459 position: relative; 460 translate: -75% 0; 461 rotate: 30deg; 462} 463 464.card[aria-pressed="true"] .frog-mask { 465 rotate: -45deg; 466 transition-duration: 300ms; 467} 468 469/* -------- Ogre -------- */ 470.ogre-mask { 471 display: flex; 472 position: absolute; 473 bottom: 0; 474 left: 50%; 475 translate: -50% 15%; 476 transition: translate 300ms var(--ease-out); 477} 478 479.ogre-mask .hand { 480 --offset: 90%; 481 --rotate: -10deg; 482 position: absolute; 483 top: 40%; 484 translate: 0 -50%; 485 486 &:first-child { 487 right: var(--offset); 488 rotate: calc(var(--rotate) * -1); 489 } 490 491 &:last-child { 492 left: var(--offset); 493 rotate: var(--rotate); 494 } 495} 496 497.card[aria-pressed="true"] .ogre-mask { 498 translate: -50% -60%; 499 transition-duration: 300ms; 500 transition-timing-function: var(--ease-in-back); 501} 502 503/* -------- Pumpkin -------- */ 504.pumpkin-mask { 505 position: absolute; 506 bottom: 0; 507 left: 50%; 508 translate: -50% 10%; 509 transform-origin: bottom right; 510 transition: 400ms var(--ease-out); 511 transition-property: rotate, translate; 512} 513 514.pumpkin-mask .hand { 515 position: absolute; 516 bottom: 60%; 517 left: 40%; 518 scale: -1 1; 519 rotate: 35deg; 520} 521 522.card[aria-pressed="true"] .pumpkin-mask { 523 rotate: 25deg; 524 translate: -35% -35%; 525 transition-duration: 250ms; 526} 527 528/* -------- Robot -------- */ 529.robot-mask { 530 position: absolute; 531 bottom: 0; 532 left: 0; 533 translate: -16% 10%; 534 transform-origin: bottom left; 535 transition: 400ms var(--ease-out); 536 transition-property: rotate, translate; 537} 538 539.robot-mask .hand { 540 position: absolute; 541 bottom: 70%; 542 left: 8%; 543 rotate: -15deg; 544} 545 546.card[aria-pressed="true"] .robot-mask { 547 rotate: -25deg; 548 translate: -10% -40%; 549 transition-duration: 250ms; 550} 551 552/* -------- Skull -------- */ 553.skull-mask { 554 position: absolute; 555 top: 0; 556 left: 50%; 557 translate: -51% -20%; 558 transform-origin: bottom left; 559 transition: 400ms var(--ease-out); 560 transition-property: translate, rotate; 561} 562 563.skull-mask .mask { 564 position: absolute; 565 bottom: 60%; 566 left: -40%; 567} 568 569.skull-mask .hand { 570 position: relative; 571 translate: 50% -40%; 572 rotate: 15deg; 573} 574 575.card[aria-pressed="true"] .skull-mask { 576 rotate: 15deg; 577 translate: 25%; 578 transition-duration: 200ms; 579} 580 581/* Card match state */ 582.card.is-matched { 583 --duration: 1.2s; 584 --delay: 50ms; 585 animation: card-pop-up var(--duration) var(--delay) forwards, 586 z-position 1s forwards; 587 588 &::before { 589 animation: match-shine calc(var(--duration) / 1.5) var(--delay) var(--ease-out) forwards; 590 } 591 592 .face, 593 .prop { 594 animation: pop-up var(--duration) var(--delay) forwards; 595 } 596 597 .face { 598 --scale: 1; 599 } 600 601 .prop { 602 --scale: 0.4; 603 opacity: 0; 604 } 605} 606 607/* Loading state */ 608.board.is-loading { 609 cursor: wait; 610 611 .card { 612 opacity: 0; 613 pointer-events: none; 614 animation: load-card-scale-up 1.2s calc(var(--i, 1) * 0.033s) var(--ease-elastic) forwards, 615 load-card-fade-in 400ms calc(var(--i, 1) * 0.033s) var(--ease-out) forwards; 616 } 617} 618 619/* Waiting state */ 620.board.is-waiting { 621 --shake-offset: 1%; 622 pointer-events: none; 623 624 .card:not([aria-pressed="true"]) { 625 opacity: 0.25; 626 transition-duration: 200ms; 627 } 628 629 .card[aria-pressed="true"]:not(.is-matched) { 630 animation: shake 200ms 300ms ease infinite; 631 transition-origin: bottom center; 632 } 633} 634 635/* Completed state */ 636.board.is-complete { 637 pointer-events: none; 638 639 .card, 640 .face { 641 animation: 1.5s calc(var(--i) * 0.1s + 800ms) infinite; 642 } 643 644 .card { 645 overflow: visible; 646 animation-name: card-jump; 647 } 648 649 .face { 650 animation-name: face-jump; 651 } 652} 653 654/* Animation keyframes */ 655@keyframes shake { 656 25% { 657 translate: calc(var(--shake-offset) * -1); 658 } 659 660 75% { 661 translate: var(--shake-offset); 662 } 663} 664 665@keyframes z-position { 666 667 0%, 668 99% { 669 z-index: 1; 670 } 671 672 100% { 673 z-index: 0; 674 } 675} 676 677@keyframes card-pop-up { 678 0% { 679 animation-timing-function: ease-out; 680 } 681 682 10%, 683 40% { 684 scale: 1.5; 685 /* prettier-ignore */ 686 animation-timing-function: linear(0, 1.114 8.5%, 1.37 12.9%, 1.315 16.2%, 0.941 24%, 0.869 27.8%, 0.882 30.7%, 1.012 38.3%, 1.046 42.7%, 0.984, 1.006 72.2%, 1); 687 } 688 689 100% { 690 scale: 1; 691 } 692} 693 694@keyframes pop-up { 695 0% { 696 animation-timing-function: ease-out; 697 } 698 699 25%, 700 50% { 701 scale: 1.2; 702 /* prettier-ignore */ 703 animation-timing-function: linear(0, 1.114 8.5%, 1.37 12.9%, 1.315 16.2%, 0.941 24%, 0.869 27.8%, 0.882 30.7%, 1.012 38.3%, 1.046 42.7%, 0.984, 1.006 72.2%, 1); 704 } 705 706 100% { 707 scale: var(--scale); 708 } 709} 710 711@keyframes card-jump { 712 0% { 713 animation-timing-function: ease; 714 } 715 716 10% { 717 translate: 0 -5%; 718 /* prettier-ignore */ 719 animation-timing-function: linear(0, 1.114 8.5%, 1.37 12.9%, 1.315 16.2%, 0.941 24%, 0.869 27.8%, 0.882 30.7%, 1.012 38.3%, 1.046 42.7%, 0.984, 1.006 72.2%, 1); 720 } 721 722 75% { 723 translate: 0 0; 724 } 725} 726 727@keyframes face-jump { 728 0% { 729 animation-timing-function: ease; 730 } 731 732 15% { 733 translate: 0 -120%; 734 scale: 1.2; 735 /* prettier-ignore */ 736 animation-timing-function: linear(0, 1.114 8.5%, 1.37 12.9%, 1.315 16.2%, 0.941 24%, 0.869 27.8%, 0.882 30.7%, 1.012 38.3%, 1.046 42.7%, 0.984, 1.006 72.2%, 1); 737 } 738 739 60% { 740 scale: 1; 741 } 742 743 75% { 744 translate: 0 0; 745 } 746} 747 748@keyframes match-shine { 749 750 0%, 751 25% { 752 opacity: 1; 753 } 754 755 to { 756 opacity: 0; 757 scale: 2; 758 } 759} 760 761@keyframes marquee-drift { 762 from { 763 translate: 0 2%; 764 } 765 766 to { 767 translate: 0 -5%; 768 } 769} 770 771@keyframes marquee-reveal { 772 773 0%, 774 80% { 775 scale: 1; 776 } 777 778 100% { 779 scale: 0.98; 780 } 781 782 0%, 783 100% { 784 opacity: 0; 785 } 786 787 10%, 788 80% { 789 opacity: 1; 790 } 791} 792 793@keyframes marquee-mask-position { 794 795 30%, 796 100% { 797 mask-position: 0% center; 798 } 799} 800 801@keyframes load-card-scale-up { 802 from { 803 scale: 0.9; 804 } 805 806 to { 807 scale: 1; 808 } 809} 810 811@keyframes load-card-fade-in { 812 from { 813 opacity: 0; 814 } 815 816 to { 817 opacity: 1; 818 } 819}
๐น JavaScript
1const board = document.querySelector(".board");
2const cards = board.querySelectorAll(".card");
3const triesValue = document.querySelector(".tries-value");
4const best = document.querySelector(".interface-data-best");
5const bestValue = best.querySelector(".best-value");
6const marquee = document.querySelector(".marquee");
7const marqueeText = document.querySelector(".marquee-text");
8const faces = ["๐", "๐", "๐", "๐ฎ", "๐", "๐"];
9const cls = {
10 completed: "is-complete",
11 combo: "is-combo",
12 loading: "is-loading",
13 matched: "is-matched",
14 waiting: "is-waiting"
15};
16let selectedCard;
17let triesCount = 0;
18let matchCount = 0;
19let comboCount = 0;
20let bestCount;
21let completeCount = faces.length;
22
23const shuffle = (arr) => {
24 for (let i = arr.length - 1; i > 0; i--) {
25 const j = Math.floor(Math.random() * (i + 1));
26 [arr[i], arr[j]] = [arr[j], arr[i]];
27 }
28
29 return arr;
30};
31
32const displayMarquee = (str, isCombo) => {
33 marquee.classList.toggle(cls.combo, isCombo);
34 marqueeText.textContent = str;
35 marquee.style.setProperty("display", "grid");
36};
37
38const toggleCardSelected = (card) => {
39 const isPressed = card.getAttribute("aria-pressed") === "true";
40 card.setAttribute("aria-pressed", isPressed ? "false" : "true");
41};
42
43const setMatchedProps = (el) => {
44 el.setAttribute("disabled", "");
45 el.classList.add(cls.matched);
46};
47
48const updateTries = (value) => {
49 triesCount = value;
50 triesValue.textContent = value;
51};
52
53const checkMatch = (card) => {
54 const cardFace = card.getAttribute("data-face");
55 const selectedCardFace = selectedCard.getAttribute("data-face");
56
57 board.classList.add(cls.waiting);
58
59 if (cardFace === selectedCardFace) {
60 setMatchedProps(card);
61 setMatchedProps(selectedCard);
62 matchCount++;
63 comboCount++;
64
65 if (comboCount > 1) {
66 displayMarquee(`${comboCount}ร!`, true);
67 }
68
69 setTimeout(() => {
70 card.removeAttribute("aria-pressed");
71 selectedCard.removeAttribute("aria-pressed");
72 selectedCard = null;
73 board.classList.remove(cls.waiting);
74 }, 500);
75 } else {
76 comboCount = 0;
77
78 setTimeout(() => {
79 toggleCardSelected(card);
80 toggleCardSelected(selectedCard);
81 selectedCard = null;
82 board.classList.remove(cls.waiting);
83 }, 1000);
84 }
85
86 updateTries(triesCount + 1);
87};
88
89const checkBest = () => {
90 if (!bestCount || triesCount < bestCount) {
91 best.style.setProperty("display", "flex");
92 bestCount = triesCount;
93 bestValue.textContent = bestCount;
94 }
95};
96
97const checkComplete = () => {
98 if (matchCount !== completeCount) {
99 return;
100 }
101
102 displayMarquee("Unmasked!", false);
103
104 setTimeout(() => {
105 board.classList.add(cls.completed);
106 }, 1000);
107};
108
109const resetGame = () => {
110 if (board.classList.contains(cls.completed)) {
111 checkBest();
112 }
113
114 matchCount = 0;
115 comboCount = 0;
116 updateTries(0);
117 selectedCard = null;
118
119 cards.forEach((card) => {
120 card.removeAttribute("disabled");
121 card.removeAttribute("aria-pressed");
122 card.classList.remove(cls.matched);
123 });
124
125 board.classList.remove(cls.completed);
126};
127
128const setupGame = () => {
129 const fragment = document.createDocumentFragment();
130 const shuffledFaces = shuffle(faces.concat(faces));
131 const shuffledCards = shuffle([...cards]);
132
133 shuffledCards.forEach((card, index) => {
134 const face = shuffledFaces[index];
135
136 card.setAttribute("data-face", face);
137 card.style.setProperty("--i", index + 1);
138 card.querySelector(".face").innerHTML = face;
139 fragment.append(card);
140 });
141
142 board.classList.add(cls.loading);
143 board.replaceChildren(fragment);
144 setTimeout(() => {
145 board.classList.remove(cls.loading);
146 }, 1000);
147};
148
149cards.forEach((card) =>
150 card.addEventListener("click", () => {
151 toggleCardSelected(card);
152
153 if (!selectedCard) {
154 selectedCard = card;
155 return;
156 }
157
158 if (card === selectedCard) {
159 selectedCard = null;
160 return;
161 }
162
163 checkMatch(card);
164 checkComplete();
165 })
166);
167
168marquee.addEventListener("animationend", (e) => {
169 if (e.animationName !== "marquee-reveal") {
170 return;
171 }
172 marquee.style.setProperty("display", "none");
173});
174
175document.querySelector("#reset-game").addEventListener("click", () => {
176 resetGame();
177 setupGame();
178});
179
180setupGame();
๐ Explanation of the Code
๐งฉ HTML Structure
- Uses semantic
<main>layout - Each card is a
<button>for accessibility - Emoji faces and masks are layered inside
.contentand.prop - Marquee is used to display combo and win messages
๐จ CSS Styling & Animations
- CSS Variables define colors, easing, and animation timing
- Cards animate using
scale,rotate, andtranslate - Each mask has its own animation logic
- Match state triggers glow, pop, and shine effects
โ๏ธ JavaScript Game Logic
-
Fisher-Yates shuffle for random card order
-
Tracks:
- Selected cards
- Tries count
- Combo streaks
- Best score
-
Prevents input during animations using board states
-
Displays animated marquee on combos and completion
โจ Key Features
- ๐ Emoji-based memory cards
- ๐๏ธ Smooth elastic animations
- ๐ Combo streak detection
- ๐ Best score tracking
- โฟ Accessible button-based cards
๐ฑ Responsive Behavior
- Uses
cqmin,vmin, and CSS Grid - Fully responsive across devices
- Container queries adapt card sizing
๐ Use Cases
- Portfolio showcase project
- CSS animation inspiration
- JavaScript game logic demo
- YouTube / Shorts content
- Blog tutorial or walkthrough
๐ฎ Possible Enhancements
- Sound effects for matches
- Difficulty levels
- Timer-based scoring
- Mobile haptic feedback
- Leaderboard with localStorage
๐ก Built with โค๏ธ by CodewithLord
