HTML
<!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <title>CodeAtNow - PingPong Game</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css"> <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/[email protected]/inter.min.css'><link rel="stylesheet" href="./style.css"> </head> <body> <!-- partial:index.partial.html --> <div id="unsubscribe"> <div class="letter"> <div class="shadow"></div> <div class="background"></div> <div class="body"> <div class="game idle"> <div class="headline"> <span>Exit Ping Pong </span> <div class="close"> <svg viewBox="0 0 24 24"> <path d="M19.7,4.3c-0.4-0.4-1-0.4-1.4,0L12,10.6L5.7,4.3c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l6.3,6.3l-6.3,6.3 c-0.4,0.4-0.4,1,0,1.4C4.5,19.9,4.7,20,5,20s0.5-0.1,0.7-0.3l6.3-6.3l6.3,6.3c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3 c0.4-0.4,0.4-1,0-1.4L13.4,12l6.3-6.3C20.1,5.3,20.1,4.7,19.7,4.3z"></path> </svg> </div> </div> <div class="paddle one"></div> <div class="ball"></div> <div class="paddle two"></div> <div class="controls"> <span>Use <strong>up</strong> & <strong>down</strong> arrow keys or</span> <div> <button class="up"> <svg viewBox="0 0 24 24"> <path d="M5.23 10.64a1 1 0 0 0 1.41.13L11 7.14V19a1 1 0 0 0 2 0V7.14l4.36 3.63a1 1 0 1 0 1.28-1.54l-6-5-.15-.09-.13-.07a1 1 0 0 0-.72 0l-.13.07-.15.09-6 5a1 1 0 0 0-.13 1.41z"></path> </svg> </button> <button class="down"> <svg viewBox="0 0 24 24"> <path d="M18.77 13.36a1 1 0 0 0-1.41-.13L13 16.86V5a1 1 0 0 0-2 0v11.86l-4.36-3.63a1 1 0 1 0-1.28 1.54l6 5 .15.09.13.07a1 1 0 0 0 .72 0l.13-.07.15-.09 6-5a1 1 0 0 0 .13-1.41z"> </svg> </button> </div> </div> <div class="start"> <button>Start</button> <small>or press up/down key</small> </div> </div> </div> </div> <h1>Ping Pong</h1> <p>Just Play Ping Pong</p> <div class="cta"> <button class="confirm"> Confirm </button> </div> </div> <script src="./script.js"></script> </body> </html>
CSS
:root { --text-color: #646B8C; --headline-color: #fcfcfc; --mail: #555B77; --mail-triangle: #494F69; --mail-background: #404660; --mail-shadow: #D1D6EE; --paper: #fff; --paper-border: #D1D6EE; --confirm-color: #fff; --confirm-background: #275EFE; --game-paddle: #404660; --game-ball: #252525; --controls-text: #646B8C; --controls-icon: #646B8C; --controls-background: #E1E6F9; } #unsubscribe button, #game button { outline: none; border: none; display: table; margin: 0 auto; font-size: 14px; font-weight: 500; font-family: inherit; padding: 8px 20px; line-height: 21px; border-radius: 7px; cursor: pointer; -webkit-appearance: none; -webkit-tap-highlight-color: transparent; } #unsubscribe .letter { width: 84px; height: 72px; margin: 0 auto 32px auto; position: relative; -webkit-animation: letter 2s ease infinite; animation: letter 2s ease infinite; } #unsubscribe .letter:before, #unsubscribe .letter:after { content: ""; position: absolute; left: 0; bottom: 0; width: 100%; height: 48px; z-index: 1; } #unsubscribe .letter:before { background: var(--mail); -webkit-clip-path: polygon(0 0, 50% 55%, 100% 0, 100% 100%, 0 100%); clip-path: polygon(0 0, 50% 55%, 100% 0, 100% 100%, 0 100%); } #unsubscribe .letter:after { background: var(--mail-triangle); -webkit-clip-path: polygon(0 100%, 50% 55%, 100% 100%); clip-path: polygon(0 100%, 50% 55%, 100% 100%); } #unsubscribe .letter .background { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: var(--mail-background); -webkit-clip-path: polygon(0 24px, 50% 0, 100% 24px, 100% 100%, 0 100%); clip-path: polygon(0 24px, 50% 0, 100% 24px, 100% 100%, 0 100%); } #unsubscribe .letter .shadow { background: black; width: 92px; height: 4px; border-radius: 50%; position: absolute; top: 108%; left: -4px; background: var(--mail-shadow); -webkit-animation: shadow 2s ease infinite; animation: shadow 2s ease infinite; } #unsubscribe .letter .body { width: 360px; height: 260px; bottom: 0; left: -138px; border-radius: 1px; background: var(--paper); box-shadow: inset 0 0 0 1px var(--paper-border); position: absolute; transform: translateY(36%) translateZ(0) scale(0.2, 0.16) rotate(90deg); } #unsubscribe .letter .body .game { width: 360px; height: 260px; position: relative; transition: opacity 0.3s ease 0.8s; } #unsubscribe .letter .body .game .headline { position: absolute; left: 0; right: 0; top: -32px; display: flex; justify-content: space-between; align-items: center; transform: translateZ(0); } #unsubscribe .letter .body .game .headline span { color: var(--headline-color); font-size: 16px; font-weight: 600; line-height: 24px; } #unsubscribe .letter .body .game .headline .close { cursor: pointer; } #unsubscribe .letter .body .game .headline .close svg { width: 20px; height: 20px; display: block; fill: var(--text-color); padding: 2px; } #unsubscribe .letter .body .game .paddle, #unsubscribe .letter .body .game .ball { top: 0; position: absolute; transition: opacity 0.3s; transform: translate(var(--x, 0), var(--y, 0)); margin: 4px; } #unsubscribe .letter .body .game .paddle { width: 6px; height: 48px; border-radius: 3px; --y: 106px; background: var(--game-paddle); } #unsubscribe .letter .body .game .paddle.one { left: 0; } #unsubscribe .letter .body .game .paddle.two { right: 0; } #unsubscribe .letter .body .game .ball { background: var(--game-ball); border-radius: 50%; width: 16px; height: 16px; left: 0; } #unsubscribe .letter .body .game .controls { bottom: -80px; left: 0; right: 0; position: absolute; } #unsubscribe .letter .body .game .controls span { display: block; text-align: center; margin-bottom: 12px; font-size: 14px; font-weight: 500; color: var(--controls-text); } #unsubscribe .letter .body .game .controls div { display: flex; justify-content: center; } #unsubscribe .letter .body .game .controls div button { width: 64px; padding: 8px 0; margin: 0; background: var(--controls-background); } #unsubscribe .letter .body .game .controls div button:not(:last-child) { margin-right: 16px; } #unsubscribe .letter .body .game .controls div button svg { width: 20px; height: 20px; display: block; margin: 0 auto; fill: var(--controls-icon); } #unsubscribe .letter .body .game .start { position: absolute; text-align: center; white-space: nowrap; left: 50%; top: 50%; transform: translate(-50%, -50%); transition: opacity 0.3s; } #unsubscribe .letter .body .game .start button { color: var(--confirm-color); background: var(--confirm-background); } #unsubscribe .letter .body .game .start small { margin: 4px 0 0 0; display: block; font-style: italic; font-size: 12px; color: var(--text-color); } #unsubscribe .letter .body .game:not(.idle) .start { opacity: 0; pointer-events: none; } #unsubscribe .letter .body .game:not(.init) .ball { opacity: 0; } #unsubscribe h1 { text-align: center; margin: 0 0 8px 0; font-family: inherit; font-weight: 600; font-size: 24px; color: var(--headline-color); } #unsubscribe p { text-align: center; margin: 0; font-size: 16px; color: var(--text-color); } #unsubscribe .cta { margin-top: 32px; } #unsubscribe .cta button { color: var(--confirm-color); background: var(--confirm-background); } #unsubscribe:not(.show-game) .letter .body .game { opacity: 0; pointer-events: none; transition-delay: 0s; } #unsubscribe.show-game .letter { -webkit-animation-play-state: paused; animation-play-state: paused; } #unsubscribe.show-game .letter .body { -webkit-animation: paper 0.8s linear forwards; animation: paper 0.8s linear forwards; } #unsubscribe.show-game .letter .shadow { -webkit-animation-play-state: paused; animation-play-state: paused; } #unsubscribe.hide-game .letter .body { -webkit-animation: paper-back 0.8s linear forwards; animation: paper-back 0.8s linear forwards; } @-webkit-keyframes paper { 30% { z-index: 0; transform: translateY(18%) translateZ(0) scale(0.2, 0.16) rotate(90deg); } 60%, 100% { z-index: 2; } 60% { transform: translateY(0) translateZ(0) scale(0.2, 0.16) rotate(0deg); } 100% { transform: translateY(63%) translateZ(0); } } @keyframes paper { 30% { z-index: 0; transform: translateY(18%) translateZ(0) scale(0.2, 0.16) rotate(90deg); } 60%, 100% { z-index: 2; } 60% { transform: translateY(0) translateZ(0) scale(0.2, 0.16) rotate(0deg); } 100% { transform: translateY(63%) translateZ(0); } } @-webkit-keyframes paper-back { 0% { transform: translateY(63%) translateZ(0); } 30% { transform: translateY(0) translateZ(0) scale(0.2, 0.16) rotate(0deg); } 60% { z-index: 0; transform: translateY(18%) translateZ(0) scale(0.2, 0.16) rotate(90deg); } 0%, 30% { z-index: 2; } 100% { transform: translateY(36%) translateZ(0) scale(0.2, 0.16) rotate(90deg); } } @keyframes paper-back { 0% { transform: translateY(63%) translateZ(0); } 30% { transform: translateY(0) translateZ(0) scale(0.2, 0.16) rotate(0deg); } 60% { z-index: 0; transform: translateY(18%) translateZ(0) scale(0.2, 0.16) rotate(90deg); } 0%, 30% { z-index: 2; } 100% { transform: translateY(36%) translateZ(0) scale(0.2, 0.16) rotate(90deg); } } @-webkit-keyframes letter { 50% { transform: translateY(-4px); } } @keyframes letter { 50% { transform: translateY(-4px); } } @-webkit-keyframes shadow { 50% { opacity: 0.7; transform: translateY(4px) scale(0.8); } } @keyframes shadow { 50% { opacity: 0.7; transform: translateY(4px) scale(0.8); } } html { box-sizing: border-box; -webkit-font-smoothing: antialiased; } * { box-sizing: inherit; } *:before, *:after { box-sizing: inherit; } body { min-height: 100vh; display: flex; justify-content: center; align-items: center; font-family: "Inter", "Inter UI", Arial; background: #2e2f2c; } body .dribbble { position: fixed; display: block; right: 20px; bottom: 20px; } body .dribbble img { display: block; height: 28px; } body .twitter { position: fixed; display: block; right: 64px; bottom: 14px; } body .twitter svg { width: 32px; height: 32px; fill: #1da1f2; }
JS
const $ = (s, o = document) => o.querySelector(s); const $$ = (s, o = document) => o.querySelectorAll(s); var unsubscribe = $('#unsubscribe'), game = $('.game', unsubscribe), confirmButton = $('.confirm', unsubscribe), upButton = $('.controls .up', game), downButton = $('.controls .down', game), startButton = $('.start', game), closeButton = $('.close', game); var ball = { elem: $('.ball', game), x: 0, y: 0, top: 0, left: 0 }, one = { elem: $('.paddle.one', game), y: 0, top: 0, score: 0 }, two = { elem: $('.paddle.two', game), y: 0, score: 0 }, interval; function init() { if(game.classList.contains('idle')) { one.y = game.offsetHeight / 2 - one.elem.offsetHeight / 2; two.y = game.offsetHeight / 2 - one.elem.offsetHeight / 2; start(); game.classList.remove('idle'); game.classList.add('init'); } } startButton.addEventListener('click', init); confirmButton.addEventListener('click', e => { unsubscribe.classList.add('show-game'); }); closeButton.addEventListener('click', e => { unsubscribe.classList.add('hide-game'); unsubscribe.classList.remove('show-game'); setTimeout(() => unsubscribe.classList.remove('hide-game'), 800); }); function start() { ball.x = game.offsetWidth / 2 - ball.elem.offsetWidth / 2; ball.y = game.offsetHeight / 2 - ball.elem.offsetHeight / 2; ball.top = Math.random() * 2 + 2; //ball.left = ((Math.random() < .5) ? 1 : -1) * (Math.random() * 2 + 2); ball.left = (1 * Math.random() * 2 + 2); interval = window.setInterval(render, 1000 / 60); } function render() { one.y += one.top; two.y = ball.y - two.elem.offsetHeight / 2; ball.x += ball.left; ball.y += ball.top; if(one.y <= 0) { one.y = 0; } if(two.y <= 0) { two.y = 0; } if(one.y >= game.offsetHeight - one.elem.offsetHeight) { one.y = game.offsetHeight - one.elem.offsetHeight; } if(two.y > game.offsetHeight - two.elem.offsetHeight) { two.y = game.offsetHeight - two.elem.offsetHeight; } if(ball.y <= 0 || ball.y >= game.offsetHeight - ball.elem.offsetHeight) { ball.top = -ball.top; } if(ball.x <= (one.elem.offsetWidth - 2)) { if((ball.y + ball.elem.offsetHeight / 2 ) > one.y && (ball.y + ball.elem.offsetHeight / 2 ) < one.y + one.elem.offsetHeight) { ball.left = -ball.left; } else { two.score++; setTimeout(() => game.classList.add('idle'), 500); clearInterval(interval); //start(); } } if(ball.x >= game.offsetWidth - ball.elem.offsetWidth - (two.elem.offsetWidth - 2)) { if((ball.y + ball.elem.offsetHeight / 2 ) > two.y && (ball.y + ball.elem.offsetHeight / 2 ) < two.y + two.elem.offsetHeight) { ball.left = -ball.left; } else { one.score++ setTimeout(() => game.classList.add('idle'), 500); clearInterval(interval); //start(); } } one.elem.style.setProperty('--y', one.y + 'px'); two.elem.style.setProperty('--y', two.y + 'px'); ball.elem.style.setProperty('--x', ball.x + 'px'); ball.elem.style.setProperty('--y', ball.y + 'px'); } document.addEventListener('keydown', e => { e.preventDefault(); init(); if(e.keyCode == 38 || e.which == 38) { one.top = -8; } if(e.keyCode == 40 || e.which == 40) { one.top = 8; } }, false); document.addEventListener('keyup', e => { e.preventDefault(); init(); if(e.keyCode == 38 || e.which == 38) { one.top = 0; } if(e.keyCode == 40 || e.which == 40) { one.top = 0; } }, false); upButton.onmousedown = e => { init(); one.top = -8; }; downButton.onmousedown = e => { init(); one.top = 8; }; upButton.onmouseup = e => { one.top = 0; }; downButton.onmouseup = e => { one.top = 0; }; upButton.ontouchstart = e => { init(); one.top = -8; }; downButton.ontouchstart = e => { init(); one.top = 8; }; upButton.ontouchend = e => { one.top = 0; }; downButton.ontouchend = e => { one.top = 0; };