<section id="wrapper">
  <div id="title">
    <h1>Catch Omanyte!</h1>
    <p>Works best on a mobile device in <a href="https://codepen.io/zingchart/full/BzmdwQ">fullscreen</a> </p>
  </div>
  <div id="info-button" onclick="toggleInfoScreen()">?</div>
  <div id="info-screen" class="hidden">
    <div id="info-shade"></div>
    <div id="info-text">
      <h3>Catch Omanyte:</h3>
      <ul>
        <li>Swipe the pokeball straight up, with just enough velocity </li>
        <li>The smaller the green circle, the greater chance in catching omanyte!</li>
      </ul>
      <h3>Built with:</h3>
      <ul>
        <li><a href="https://zingchart.github.io/zingtouch">ZingTouch</a> - A JavaScript gesture detection library</li>
        <li><a href="http://anime-js.com/">Anime.js</a> - a flexible yet lightweight JavaScript animation library.</li>
      </ul>

      <h3>Brought to you by <a href="https://zingchart.com/">ZingChart</a></h3>
      <span style="font-size: 0.8em;">Disclaimer: ZingChart does not own or is affiliated to the character Omanyte or the pokeball item. This is simply a demonstration of web technologies by people who love playing Pokemon Go!</span>
    </div>

  </div>
  <div id="touch-layer"></div>
  <div id="particles"></div>
  <div id="ring-screen">
    <div id="ring">
      <div id="ring-active">
        <div id="ring-fill"></div>
      </div>
    </div>
  </div>
  <div id="screen" class="gradient-background">
    <div id="target-container">
      <div id="target"></div>
    </div>
    <div id="ball"></div>
    <div id="output"></div>
  </div>


  <div id="capture-screen" class="gradient-background hidden">
    <div id="capture-status" class="hidden">
      You caught Omanyte!
    </div>
    <div id="poof-container" class="hidden">
      <div id="poof"></div>
    </div>
    <div id="capture-confetti"></div>
    <div id="capture-ball"></div>
    <div id="capture-ball-button-container" class="hidden">
      <div id="capture-ball-button"></div>
    </div>
  </div>


</section>
@import 'https://fonts.googleapis.com/css?family=Open+Sans:400,600';
* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  font-family: "Open Sans";
  width: 100%;
  height: 100%;
}

h1,
p {
  margin: 0;
}

ul {
  margin: 0;
}

a {
  color: rgb(71, 211, 255);
}



.gradient-background {
  background: #00c6ff;
  background: -webkit-linear-gradient(top, #1e5799 0%, #00c6ff 25%, #26c6da 55%, #7AE3EF 100%);
  background: linear-gradient(to bottom, #1e5799 0%, #00c6ff 25%, #26c6da 55%, #7AE3EF 100%)
}

#screen,
#wrapper,
#touch-layer,
#waves,
#info-screen {
  position: relative;
  height: 100%;
  width: 100%;
}

#title {
  position: fixed;
  text-align: center;
  width: 100%;
  color: white;
  margin-top: 0.5em;
  z-index: 11;
}

#screen,
#capture-screen,
#ring-screen {
  @extend .flex-center;
  position: relative;
}


/* Accuracy ring */

#ring-screen {
  @extend .flex-center;
  height: 100%;
  width: 100%;
  position: absolute;
  z-index: 3;
}

#ring {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  width: 150px;
  height: 150px;
  border: 2px solid white;
  border-radius: 100px;
}

#ring-active {
  border: 2px solid #64DD17;
  border-radius: 100px;
}

#ring-fill {
  width: 150px;
  height: 150px;
}


/* Capture Screen */

#capture-screen {
  display: flex;
    @extend .fixed-full-width;

  flex-direction: column;
  background-color: #222;

  z-index: 12;
}

#poof-container {
  @extend .flex-center;
  position: fixed;
  z-index: 10;
  height: 100%;
  width: 100%;
  top: 0;
  left: 0;
}

#poof {
  width: 100%;
  height: 20px;
  opacity: 0.8;
  background-image: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/374756/pkmngo-poof.svg');
  background-repeat: no-repeat;
  background-position: center center;
  background-size: contain;
}

#capture-ball {
  position: relative;
  width: 200px;
  height: 200px;
  background-image: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/374756/pkmngo-pokeball.png');
  background-repeat: no-repeat;
  background-position: center center;
  background-size: cover;
}

#capture-ball-button-container {
  @extend .flex-center;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 5;
}

#capture-ball-button {
  width: 30px;
  height: 30px;
  margin-top: -20px;
  border-radius: 20px;
  opacity: 0.8;
  background-color: red;
}

#target-container {
  @extend .flex-center;
  position: relative;
}

#target {
  width: 140px;
  height: 140px;
  transform: rotate(-10deg);
  background-image: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/374756/pkmngo-helix.png');
  background-repeat: no-repeat;
  background-position: center center;
  background-size: cover;
  z-index: 2;
}

#ball {
  position: fixed;
  height: 50px;
  width: 50px;
  border-radius: 25px;
  bottom: 10%;
  background-image: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/374756/pkmngo-pokeball.png');
  background-repeat: no-repeat;
  background-position: center center;
  background-size: cover;
  z-index: 3;
}

#touch-layer {
  position: fixed;
  top: 0;
  left: 0;
  opacity: 0;
  z-index: 10;
}

#capture-status {
  position: absolute;
  font-size: 1.5em;
  top: 0;
  left: 0;
  width: 100%;
  text-align: center;
  padding: 1em 0.2em;
  color: #FFF;
}

#particles {
  position: fixed;
  width: 100%;
  height: 100%;
  z-index: 1;
}

.particle {
  position: fixed;
  width: 7px;
  height: 7px;
  background-color: #fff;
  border-radius: 5px;
  opacity: 0.7;
  z-index: 1;
}

#capture-confetti {
  position: fixed;
  width: 100%;
  height: 100%;
  z-index: 6;
}

#capture-confetti> div.particle {
  height: 10px;
  width: 10px;
  opacity: 0.9;
  background-color: #4aa6fb;
}

/*Info screen*/

#info-button {
  @extend .flex-center;
  position: fixed;
  top: 5px;
  right: 5px;
  z-index: 15;
  color: button;
  width: 30px;
  height: 30px;
  color: #FFF;
  border: 2px solid #FFF;
  border-radius: 15px;
  cursor: pointer;
}

#info-screen {
  @extend .fixed-full-width;
  z-index: 14;
}

#info-shade {
  @extend .fixed-full-width;
  background-color: #222;
  opacity: 0.89;
}

#info-text {
  @extend .fixed-full-width;
  display: flex;
  flex-direction: column;
  padding: 1em;
  font-size: 0.9em;
  color: white;
}

.flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

.fixed-full-width{
  position: fixed;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}

.hidden {
  display: none !important;
}

View Compiled
var Screen = {
    height: window.innerHeight,
    width: window.innerWidth
};
var MAX_VELOCITY = Screen.height * 0.009;
var Resources = {
    pokeball: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/374756/pkmngo-pokeball.png',
    pokeballActive: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/374756/pkmngo-pokeballactive.png',
    pokeballClosed: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/374756/pkmngo-pokeballclosed.png'
};

var Ball = {
    id: 'ball',
    size: 50,
    x: 0,
    y: 0,
    inMotion: false,
    moveBall: function(x, y) {
        Ball.x = x;
        Ball.y = y;
        var BallElement = document.getElementById(Ball.id);
        BallElement.style.top = Ball.y + 'px';
        BallElement.style.left = Ball.x + 'px';
    },
    getElement() {
        return document.getElementById(Ball.id);
    },
    resetBall: function() {
        Ball.moveBall(Screen.width / 2 - (Ball.size / 2), Screen.height - (Ball.size + 10));
        var BallElement = document.getElementById(Ball.id);
        BallElement.style.transform = "";
        BallElement.style.width = BallElement.style.height = Ball.size + 'px';
        BallElement.style.backgroundImage = "url('" + Resources.pokeball + "')";
        Ball.inMotion = false;
    },
    savePosition: function() {
        var ballEle = document.getElementById('ball');
        var ballRect = ballEle.getBoundingClientRect();
        ballEle.style.transform = "";
        ballEle.style.top = ballRect.top + 'px';
        ballEle.style.left = ballRect.left + 'px';
        ballEle.style.height = ballEle.style.width = ballRect.width + 'px';
    }
};

//Initial Setup

resetState();

//Move omanyte
anime({
    targets: ['#target'],
    rotate: 20,
    duration: 800,
    loop: true,
    easing: 'easeInOutQuad',
    direction: 'alternate'
});

window.onresize = function() {
    Screen.height = window.innerHeight;
    Screen.width = window.innerWidth;
    MAX_VELOCITY = Screen.height * 0.009;
    resetState();
}


/* Gesture Bindings */
var touchElement = document.getElementById('touch-layer');
var touchRegion = new ZingTouch.Region(touchElement);
var CustomSwipe = new ZingTouch.Swipe({
    escapeVelocity: 0.1
})

var CustomPan = new ZingTouch.Pan();
var endPan = CustomPan.end;
CustomPan.end = function(inputs) {
    setTimeout(function() {
        if (Ball.inMotion === false) {
            Ball.resetBall();
        }
    }, 100);
    return endPan.call(this, inputs);
}

touchRegion.bind(touchElement, CustomPan, function(e) {
    Ball.moveBall(e.detail.events[0].x - Ball.size / 2, e.detail.events[0].y - Ball.size / 2);
});

touchRegion.bind(touchElement, CustomSwipe, function(e) {
    Ball.inMotion = true;
    var screenEle = document.getElementById('screen');
    var screenPos = screenEle.getBoundingClientRect();
    var angle = e.detail.data[0].currentDirection;
    var rawVelocity = velocity = e.detail.data[0].velocity;
    velocity = (velocity > MAX_VELOCITY) ? MAX_VELOCITY : velocity;

    //Determine the final position.
    var scalePercent = Math.log(velocity + 1) / Math.log(MAX_VELOCITY + 1);
    var destinationY = (Screen.height - (Screen.height * scalePercent)) + screenPos.top;
    var movementY = destinationY - e.detail.events[0].y;

    //Determine how far it needs to travel from the current position to the destination.
    var translateYValue = -0.75 * Screen.height * scalePercent;
    var translateXValue = 1 * (90 - angle) * -(translateYValue / 100);

    anime.remove('#ring-fill');

    anime({
            targets: ['#ball'],
            translateX: {
                duration: 300,
                value: translateXValue,
                easing: 'easeOutSine'
            },
            translateY: {
                value: movementY * 1.25 + 'px',
                duration: 300,
                easing: 'easeOutSine'
            },
            scale: {
                value: 1 - (0.5 * scalePercent),
                easing: 'easeInSine',
                duration: 300
            },
            complete: function() {
                if (movementY < 0) {
                    throwBall(movementY, translateXValue, scalePercent);
                } else {
                    setTimeout(resetState, 400);
                }
            }
        })
        //End
});

function throwBall(movementY,translateXValue, scalePercent){
    //Treat translations as fixed.
    Ball.savePosition();
    anime({
        targets: ['#ball'],
        translateY: {
            value: movementY * -0.5 + 'px',
            duration: 400,
            easing: 'easeInOutSine'
        },
        translateX: {
            value: -translateXValue * 0.25,
            duration: 400,
            easing: 'linear'
        },
        scale: {
            value: 1 - (0.25 * scalePercent),
            easing: 'easeInSine',
            duration: 400
        },
        complete: determineThrowResult
    })
}

function determineThrowResult() {
    //Determine hit-region
    var targetCoords = getCenterCoords('target');
    var ballCoords = getCenterCoords('ball');

    //Determine if the ball is touching the target.
    var radius = document.getElementById('target').getBoundingClientRect().width / 2;
    if (ballCoords.x > targetCoords.x - radius &&
        ballCoords.x < targetCoords.x + radius &&
        ballCoords.y > targetCoords.y - radius &&
        ballCoords.y < targetCoords.y + radius) {

        Ball.savePosition();
        var ballOrientation = (ballCoords.x < targetCoords.x) ? -1 : 1;
        anime({
            targets: ['#ball'],
            translateY: {
                value: -1.15 * radius,
                duration: 200,
                easing: 'linear'
            },
            translateX: {
                value: 1.15 * radius * ballOrientation,
                duration: 200,
                easing: 'linear'
            },
            scaleX: {
                value: ballOrientation,
                duration: 200,
            },
            complete: function() {
                var ball = Ball.getElement();
                ball.style.backgroundImage = "url('" + Resources.pokeballActive + "')";
                emitParticlesToPokeball();
            }
        });
    } else {
        setTimeout(resetState, 400);
    }
}


function emitParticlesToPokeball() {
    var particles = [];
    var targetEle = getCenterCoords('target');
    var ballEle = Ball.getElement();
    var ballRect = ballEle.getBoundingClientRect();
    var particleLeft;
    var particleRight;
    var palette = [
        '#E4D3A8',
        '#6EB8C0',
        '#FFF',
        '#2196F3'
    ]
    var particleContainer = document.getElementById('particles');
    for (var i = 0; i < 50; i++) {
        var particleEle = document.createElement('div');
        particleEle.className = 'particle';
        particleEle.setAttribute('id', 'particle-' + i);;
        particleLeft = getRandNum(-60, 60) + targetEle.x;
        particleEle.style.left = particleLeft + 'px';
        particleRight = getRandNum(-60, 60) + targetEle.y;
        particleEle.style.top = particleRight + 'px';
        particleEle.style.backgroundColor = palette[getRandNum(0, palette.length)]
        particleContainer.appendChild(particleEle);
        anime({
            targets: ['#particle-' + i],
            translateX: {
                value: ballRect.left - particleLeft,
                delay: 100 + (i * 10)
            },
            translateY: {
                value: ballRect.top + (Ball.size / 2) - particleRight,
                delay: 100 + (i * 10),
            },
            opacity: {
                value: 0,
                delay: 100 + (i * 10),
                duration: 800,
                easing: 'easeInSine'
            }
        });
        anime({
            targets: ['#target'],
            opacity: {
                value: 0,
                delay: 200,
                easing: 'easeInSine'
            }
        });
    }
    setTimeout(function() {
        var ball = Ball.getElement();
        ball.style.backgroundImage = "url('" + Resources.pokeballClosed + "')";
        document.getElementById('particles').innerHTML = "";
        Ball.savePosition();

        anime({
            targets: ['#ball'],
            translateY: {
                value: "200px",
                delay: 400,
                duration: 400,
                easing: 'linear'
            },
            complete: function() {
                Ball.resetBall();
            }
        });
        setTimeout(function() {
            animateCaptureState();
            resetState();
        }, 750);

    }, 1000);
}



function animateCaptureState() {
    var ballContainer = document.getElementById('capture-screen');
    ballContainer.classList.toggle('hidden');

    var duration = 500;
    anime({
        targets: ['#capture-ball'],
        rotate: 40,
        duration: duration,
        easing: 'easeInOutBack',
        loop: true,
        direction: 'alternate'
    });

    var ringRect = (document.getElementById('ring-active')).getBoundingClientRect();
    var successRate = ((150 - ringRect.width) / 150) * 100;
    var seed = getRandNum(0, 100);
    setTimeout(function() {

        anime.remove('#capture-ball');

        if (seed < Math.floor(successRate)) {
            var captureBall = document.getElementById('capture-ball');
            var buttonContainer = document.getElementById('capture-ball-button-container');
            buttonContainer.classList.toggle('hidden');

            //Captured
            var captureStatus = document.getElementById('capture-status');
            captureStatus.classList.toggle('hidden');
            captureStatus.innerHTML = "You caught Omanyte!"
            makeItRainConfetti();

            anime({
                targets: ['#capture-ball-button-container'],
                opacity: {
                    value: 0,
                    duration: 800,
                    easing: 'easeInSine'
                },
                complete: function() {
                    setTimeout(function() {
                        var ballContainer = document.getElementById('capture-screen');
                        ballContainer.classList.toggle('hidden');
                        var buttonContainer = document.getElementById('capture-ball-button-container');
                        buttonContainer.classList.toggle('hidden');
                        buttonContainer.style.opacity = "";
                        document.getElementById('capture-status').classList.toggle('hidden');
                    }, 800);
                }
            });

        } else {
            var poofContainer = document.getElementById('poof-container');
            poofContainer.classList.toggle('hidden');

            var captureStatus = document.getElementById('capture-status');
            captureStatus.innerHTML = "Omanyte Escaped!"
            captureStatus.classList.toggle('hidden');

            anime({
                targets: ['#poof'],
                scale: {
                    value: 20,
                    delay: 400,
                    easing: 'linear',
                    duration: 600
                },
                complete: function() {
                    var ballContainer = document.getElementById('capture-screen');
                    ballContainer.classList.toggle('hidden');

                    var poofEle = document.getElementById('poof');
                    poofEle.style.transform = "";
                    var poofContainer = document.getElementById('poof-container');
                    poofContainer.classList.toggle('hidden');

                    var captureStatus = document.getElementById('capture-status');
                    captureStatus.classList.toggle('hidden');
                }
            })
        }
    }, duration * 6);

}


function makeItRainConfetti() {
    for (var i = 0; i < 100; i++) {
        var particleContainer = document.getElementById('capture-confetti');
        var particleEle = document.createElement('div');
        particleEle.className = 'particle';
        particleEle.setAttribute('id', 'particle-' + i);
        particleLeft = window.innerWidth / 2;
        particleEle.style.left = particleLeft + 'px';
        particleTop = window.innerHeight / 2;
        particleEle.style.top = particleTop + 'px';
        particleEle.style.backgroundColor = ((getRandNum(0, 2)) ? '#FFF' : '#4aa6fb')
        particleContainer.appendChild(particleEle);
        anime({
            targets: ['#particle-' + i],
            translateX: {
                value: ((getRandNum(0, 2)) ? -1 : 1) * getRandNum(0, window.innerWidth / 2),
                delay: 100
            },
            translateY: {
                value: ((getRandNum(0, 2)) ? -1 : 1) * getRandNum(0, window.innerHeight / 2),
                delay: 100,
            },
            opacity: {
                value: 0,
                duration: 800,
                easing: 'easeInSine'
            },
            complete: function() {
                document.getElementById('capture-confetti').innerHTML = "";
            }
        });
    }
}

function toggleInfoScreen() {
    var infoScreen = document.getElementById('info-screen');
    var infoButton = document.getElementById('info-button');
    infoScreen.classList.toggle('hidden');
    infoButton.innerHTML = (infoScreen.className === 'hidden') ? "?" : 'X';
}

/* * * * * * * * * * * * *
* Universal Helpers
* * * * * * * * * * * * */
function resetState() {
    Ball.resetBall();
    document.getElementById('target').style.opacity = 1;
    //Adjust Ring
    var ring = document.getElementById('ring-fill');
    ring.style.height = "150px";
    ring.style.width = "150px";
    anime({
        targets: ['#ring-fill'],
        height: "5px",
        width: "5px",
        duration: 3000,
        loop: true,
        easing: 'linear'
    })
}

function getCenterCoords(elementId) {
    var rect = document.getElementById(elementId).getBoundingClientRect();
    return {
        x: rect.left + rect.width / 2,
        y: rect.top + rect.height / 2
    };
}

function getRandNum(min, max) {
    return Math.floor(Math.random() * (max - min)) + min;
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/animejs/1.0.0/anime.min.js
  2. https://s3-us-west-2.amazonaws.com/s.cdpn.io/374756/zingtouch.min.js