Inspiration https://twitter.com/nealagarwal/status/1228002529155395585
A Pen by Aaron Iker on CodePen.
| <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>Win Ping Pong to unsubscribe</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>Unsubscribe</h1> | |
| <p>We are sorry to see you go!</p> | |
| <div class="cta"> | |
| <button class="confirm"> | |
| Confirm | |
| </button> | |
| </div> | |
| </div> | |
| <!-- dribbble - twitter --> | |
| <a class="dribbble" href="https://dribbble.com/ai" target="_blank"><img src="https://cdn.dribbble.com/assets/dribbble-ball-mark-2bd45f09c2fb58dbbfb44766d5d1d07c5a12972d602ef8b32204d28fa3dda554.svg" alt=""></a> | |
| <a class="twitter" target="_blank" href="https://twitter.com/aaroniker_me"><svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72"><path d="M67.812 16.141a26.246 26.246 0 0 1-7.519 2.06 13.134 13.134 0 0 0 5.756-7.244 26.127 26.127 0 0 1-8.313 3.176A13.075 13.075 0 0 0 48.182 10c-7.229 0-13.092 5.861-13.092 13.093 0 1.026.118 2.021.338 2.981-10.885-.548-20.528-5.757-26.987-13.679a13.048 13.048 0 0 0-1.771 6.581c0 4.542 2.312 8.551 5.824 10.898a13.048 13.048 0 0 1-5.93-1.638c-.002.055-.002.11-.002.162 0 6.345 4.513 11.638 10.504 12.84a13.177 13.177 0 0 1-3.449.457c-.846 0-1.667-.078-2.465-.231 1.667 5.2 6.499 8.986 12.23 9.09a26.276 26.276 0 0 1-16.26 5.606A26.21 26.21 0 0 1 4 55.976a37.036 37.036 0 0 0 20.067 5.882c24.083 0 37.251-19.949 37.251-37.249 0-.566-.014-1.134-.039-1.694a26.597 26.597 0 0 0 6.533-6.774z"></path></svg></a> |
| 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; | |
| }; |
| :root { | |
| --text-color: #646B8C; | |
| --headline-color: #2B3044; | |
| --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: #275EFE; | |
| --controls-text: #646B8C; | |
| --controls-icon: #646B8C; | |
| --controls-background: #E1E6F9; | |
| } | |
| #unsubscribe, | |
| #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; | |
| animation: letter 2s ease infinite; | |
| &:before, | |
| &:after { | |
| content: ''; | |
| position: absolute; | |
| left: 0; | |
| bottom: 0; | |
| width: 100%; | |
| height: 48px; | |
| z-index: 1; | |
| } | |
| &:before { | |
| background: var(--mail); | |
| clip-path: polygon(0 0, 50% 55%, 100% 0, 100% 100%, 0 100%); | |
| } | |
| &:after { | |
| background: var(--mail-triangle); | |
| clip-path: polygon(0 100%, 50% 55%, 100% 100%); | |
| } | |
| .background { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: var(--mail-background); | |
| clip-path: polygon(0 24px, 50% 0, 100% 24px, 100% 100%, 0 100%); | |
| } | |
| .shadow { | |
| background: black; | |
| width: 92px; | |
| height: 4px; | |
| border-radius: 50%; | |
| position: absolute; | |
| top: 108%; | |
| left: -4px; | |
| background: var(--mail-shadow); | |
| animation: shadow 2s ease infinite; | |
| } | |
| .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(.2, .16) rotate(90deg); | |
| .game { | |
| width: 360px; | |
| height: 260px; | |
| position: relative; | |
| transition: opacity .3s ease .8s; | |
| .headline { | |
| position: absolute; | |
| left: 0; | |
| right: 0; | |
| top: -32px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| transform: translateZ(0); | |
| span { | |
| color: var(--headline-color); | |
| font-size: 16px; | |
| font-weight: 600; | |
| line-height: 24px; | |
| } | |
| .close { | |
| cursor: pointer; | |
| svg { | |
| width: 20px; | |
| height: 20px; | |
| display: block; | |
| fill: var(--text-color); | |
| padding: 2px; | |
| } | |
| } | |
| } | |
| .paddle, | |
| .ball { | |
| top: 0; | |
| position: absolute; | |
| transition: opacity .3s; | |
| transform: translate(var(--x, 0), var(--y, 0)); | |
| } | |
| .paddle { | |
| width: 6px; | |
| height: 48px; | |
| border-radius: 3px; | |
| --y: 106px; | |
| background: var(--game-paddle); | |
| &.one { | |
| left: 0; | |
| } | |
| &.two { | |
| right: 0; | |
| } | |
| } | |
| .ball { | |
| background: var(--game-ball); | |
| border-radius: 50%; | |
| width: 16px; | |
| height: 16px; | |
| left: 0; | |
| } | |
| .controls { | |
| bottom: -80px; | |
| left: 0; | |
| right: 0; | |
| position: absolute; | |
| span { | |
| display: block; | |
| text-align: center; | |
| margin-bottom: 12px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| color: var(--controls-text); | |
| } | |
| div { | |
| display: flex; | |
| justify-content: center; | |
| button { | |
| width: 64px; | |
| padding: 8px 0; | |
| margin: 0; | |
| background: var(--controls-background); | |
| &:not(:last-child) { | |
| margin-right: 16px; | |
| } | |
| svg { | |
| width: 20px; | |
| height: 20px; | |
| display: block; | |
| margin: 0 auto; | |
| fill: var(--controls-icon); | |
| } | |
| } | |
| } | |
| } | |
| .start { | |
| position: absolute; | |
| text-align: center; | |
| white-space: nowrap; | |
| left: 50%; | |
| top: 50%; | |
| transform: translate(-50%, -50%); | |
| transition: opacity .3s; | |
| button { | |
| color: var(--confirm-color); | |
| background: var(--confirm-background); | |
| } | |
| small { | |
| margin: 4px 0 0 0; | |
| display: block; | |
| font-style: italic; | |
| font-size: 12px; | |
| color: var(--text-color); | |
| } | |
| } | |
| &:not(.idle) { | |
| .start { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| } | |
| &:not(.init) { | |
| .ball { | |
| opacity: 0; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| h1 { | |
| text-align: center; | |
| margin: 0 0 8px 0; | |
| font-family: inherit; | |
| font-weight: 600; | |
| font-size: 24px; | |
| color: var(--headline-color); | |
| } | |
| p { | |
| text-align: center; | |
| margin: 0; | |
| font-size: 16px; | |
| color: var(--text-color); | |
| } | |
| .cta { | |
| margin-top: 32px; | |
| button { | |
| color: var(--confirm-color); | |
| background: var(--confirm-background); | |
| } | |
| } | |
| &:not(.show-game) { | |
| .letter { | |
| .body { | |
| .game { | |
| opacity: 0; | |
| pointer-events: none; | |
| transition-delay: 0s; | |
| } | |
| } | |
| } | |
| } | |
| &.show-game { | |
| .letter { | |
| animation-play-state: paused; | |
| .body { | |
| animation: paper .8s linear forwards; | |
| } | |
| .shadow { | |
| animation-play-state: paused; | |
| } | |
| } | |
| } | |
| &.hide-game { | |
| .letter { | |
| .body { | |
| animation: paper-back .8s linear forwards; | |
| } | |
| } | |
| } | |
| } | |
| @keyframes paper { | |
| 30% { | |
| z-index: 0; | |
| transform: translateY(18%) translateZ(0) scale(.2, .16) rotate(90deg); | |
| } | |
| 60%, | |
| 100% { | |
| z-index: 2; | |
| } | |
| 60% { | |
| transform: translateY(0) translateZ(0) scale(.2, .16) rotate(0deg); | |
| } | |
| 100% { | |
| transform: translateY(63%) translateZ(0); | |
| } | |
| } | |
| @keyframes paper-back { | |
| 0% { | |
| transform: translateY(63%) translateZ(0); | |
| } | |
| 30% { | |
| transform: translateY(0) translateZ(0) scale(.2, .16) rotate(0deg); | |
| } | |
| 60% { | |
| z-index: 0; | |
| transform: translateY(18%) translateZ(0) scale(.2, .16) rotate(90deg); | |
| } | |
| 0%, | |
| 30% { | |
| z-index: 2; | |
| } | |
| 100% { | |
| transform: translateY(36%) translateZ(0) scale(.2, .16) rotate(90deg); | |
| } | |
| } | |
| @keyframes letter { | |
| 50% { | |
| transform: translateY(-4px); | |
| } | |
| } | |
| @keyframes shadow { | |
| 50% { | |
| opacity: .7; | |
| transform: translateY(4px) scale(.8); | |
| } | |
| } | |
| html { | |
| box-sizing: border-box; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| * { | |
| box-sizing: inherit; | |
| &:before, | |
| &:after { | |
| box-sizing: inherit; | |
| } | |
| } | |
| // dribbble & twitter | |
| body { | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-family: 'Inter', 'Inter UI', Arial; | |
| background: #F2F5FF; | |
| .dribbble { | |
| position: fixed; | |
| display: block; | |
| right: 20px; | |
| bottom: 20px; | |
| img { | |
| display: block; | |
| height: 28px; | |
| } | |
| } | |
| .twitter { | |
| position: fixed; | |
| display: block; | |
| right: 64px; | |
| bottom: 14px; | |
| svg { | |
| width: 32px; | |
| height: 32px; | |
| fill: #1da1f2; | |
| } | |
| } | |
| } |
| <link href="https://cdn.jsdelivr.net/npm/inter-ui@3.11.0/inter.min.css" rel="stylesheet" /> |
Inspiration https://twitter.com/nealagarwal/status/1228002529155395585
A Pen by Aaron Iker on CodePen.