<div class="game">

  <!-- /////////// -->
  <!-- Fishing rod -->
  <!-- /////////// -->

  <div class="fishing-rod">
    <img class="fill-container" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-fishing-rod.svg" alt="">
    <canvas class="reel-line-tension" data-element="reel-line-tension"></canvas>
    <div class="reel-handle">
      <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-reel-handle.svg" alt="">
    </div>
    <div class="reel-line"></div>
  </div>

  <!-- ///////// -->
  <!-- Game body -->
  <!-- ///////// -->

  <div class="game-body">
    <img class="fill-container" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-game-body-bg.svg" alt="">
    <canvas class="bubbles" data-element="bubbles"></canvas>
    <canvas class="seaweed" data-element="seaweed"></canvas>
    <img class="seafloor fill-container" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-sea-floor.svg" alt="">
    <div class="fish">
      <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-fish.svg" alt="">
    </div>
    <div class="indicator">
      <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-indicator.svg" alt="">
    </div>
    <img class="glass fill-container" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-glass.svg" alt="">
  </div>

  <!-- //////////// -->
  <!-- Progress bar -->
  <!-- //////////// -->

  <div class="progress-bar">
    <img class="fill-container" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-progress-bar-bg.svg" alt="">
    <div class="progress-gradient-wrapper">
      <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-progress-gradient.svg" class="progress-gradient" alt="">
    </div>
  </div>

</div>
<!-- .game -->

<!-- /////// -->
<!-- Success -->
<!-- /////// -->

<div class="success">
  <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-nice-catch.png" alt="" class="nice-catch">
  <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-perfect.png" alt="" class="perfect">
</div>
html {
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: inherit;
  user-select: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  -o-user-select: none;
  user-select: none;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: radial-gradient(ellipse at center, #88c4c2 0%,#56838f 80%);
  display: flex;
  align-items: center;
  justify-content: center;
}

.game {
  display: flex;
  align-items: center;
  opacity: 1;
}

.fill-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

/* ----------- */
/* Fishing rod */
/* ----------- */

.fishing-rod {
  width: 46px;
  height: 352px;
  position: relative;
  margin-right: 10px;
}

.reel-handle {
  width: 14px;
  height: 14px;
  position: absolute;
  top: 81.2%;
  left: 1.8%;
  z-index: 10;
}

.reel-handle img {
  animation-name: rotate;
  animation-iteration-count: infinite;
  animation-duration: 0.35s;
  animation-timing-function: linear;
  transform-origin: 12px 2.5px;
  animation-play-state: paused;
}

@keyframes rotate {
  0% {transform: rotate(0deg)}
  100% {transform: rotate(360deg)}
}

.indicator-active .reel-handle img {
  animation-play-state: running;
}

.reel-line {
  width: 18px;
  height: 16px;
  position: absolute;
  top: 265px;
  left: 4px;
  background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/game-reel-loop.png') top left repeat-x;
  background-size: auto 100%;
  z-index: 5;
  animation-name: reel;
  animation-duration: 1s;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
  animation-play-state: paused;
  opacity: 0.3;
}

.indicator-active .reel-line {
  animation-play-state: running;
}

@keyframes reel {
  0% {background-position: 0;}
  100% {background-position: 139px}
}

.reel-line-tension {
  width: 42%;
  height: 13.5%;
  position: absolute;
  bottom: 26.8%;
  left: 29%;
  /* background: red;
  opacity: 0.5; */
}

/* --------- */
/* Game body */
/* --------- */

.game-body {
  width: 36px;
  height: 350px;
  position: relative;
}

.seafloor {
  position: relative;
  z-index: 5;
}

.seaweed {
  width: 94.5%;
  height: 19%;
  position: absolute;
  bottom: 4%;
  left: 3%;
  z-index: 10;
}

.bubbles {
  width: 94.5%;
  height: 95.2%;
  position: absolute;
  bottom: 4%;
  left: 2.7%;
  /* background: red;
  opacity: 0.5; */
}

.fish {
  width: 25px;
  height: 17px;
  position: absolute;
  top: calc(100% - 17px);
  left: 18%;
  opacity: 0.8;
  will-change: top;
}

.indicator {
  width: 34px;
  height: 46px;
  position: absolute;
  top: calc(100% - 46px);
  left: 3%;
  opacity: 0.8;
  z-index: 30;
}

.glass {
  z-index: 20;
}

/* ------------ */
/* Progress bar */
/* ------------ */

.progress-bar {
  width: 16px;
  height: 334px;
  position: relative;
  overflow: hidden;
}

.progress-gradient-wrapper {
  position: absolute;
  bottom: 1px;
  left: 0;
  width: 15px;
  height: 100%;
  overflow: hidden;
  border-top-right-radius: 4px;
  will-change: height;
}

.progress-gradient {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 15px;
  height: 332px;
  display: block;
}

/* ------- */
/* Success */
/* ------- */

.success {
  width: 200px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  flex-direction: column;
  z-index: 100;
  opacity: 0;
  display: none;
  cursor: pointer;
}

.nice-catch {
  width: 100%;
  position: relative;
  z-index: 20;
}

.perfect {
  width: 46.743295%;
  margin-top: -4px;
  transform-origin: top;
  position: relative;
  z-index: 10;
}
// ----
// Game
// ----

(function() {
  let gameOver = false;

  // --------------
  // Animation loop
  // --------------

  function animationLoop() {
    if (!gameOver) {
      indicator.updatePosition();
      indicator.detectCollision();
      progressBar.updateUi();
      progressBar.detectGameEnd();
      fish.updateFishPosition();
      requestAnimationFrame(animationLoop);
    }
  }

  // ---------
  // Indicator
  // ---------

  class Indicator {
    constructor() {
      this.indicator = document.querySelector('.indicator');
      this.height = this.indicator.clientHeight;
      this.y = 0;
      this.velocity = 0;
      this.acceleration = 0;
      this.topBounds = (gameBody.clientHeight * -1) + 48;
      this.bottomBounds = 0;
    }

    applyForce(force) {
      this.acceleration += force;
    }

    updatePosition() {
      this.velocity += this.acceleration;
      this.y += this.velocity;
      
      //  Reset acceleration
      indicator.acceleration = 0;

      // Change direction when hitting the bottom + add friction
      if (this.y > this.bottomBounds) {
        this.y = 0;
        this.velocity *= 0.5;
        this.velocity *= -1;
      }

      // Prevent from going beyond the top
      // Don't apply button forces when beyond the top
      // console.log(indicator.y, this.topBounds);
      if (indicator.y < this.topBounds) {
        indicator.y = this.topBounds;
        indicator.velocity = 0;
      } else {
        if (keyPressed) {
          indicator.applyForce(-0.5);
        }
      }

      // Apply constant force
      indicator.applyForce(0.3);

      // Update object position
      this.indicator.style.transform = `translateY(${this.y}px)`;
    }

    detectCollision() {
      if (
        fish.y < this.y && fish.y > this.y - this.height ||
        fish.y - fish.height < this.y && fish.y - fish.height > this.y - this.height
      ) {
        progressBar.fill();
        document.body.classList.add('collision');
      } else {
        progressBar.drain();
        document.body.classList.remove('collision');
      }
    }
  }

  // ----
  // Fish
  // ----

  class Fish {
    constructor() {
      this.fish = document.querySelector('.fish');
      this.height = this.fish.clientHeight;
      this.y = 5;
      this.direction = null;
      this.randomPosition = null;
      this.randomCountdown = null;
      this.speed = 2;
    }

    resetPosition() {
      this.y = 5;
    }

    updateFishPosition() {
      if (!this.randomPosition || this.randomCountdown < 0) {
        this.randomPosition = Math.ceil(Math.random() * (gameBody.clientHeight - this.height)) * -1;
        this.randomCountdown = Math.abs(this.y - this.randomPosition);
        this.speed = Math.abs(Math.random() * (3 - 1) + 1);
      };

      if (this.randomPosition < this.y) {
        this.y -= this.speed;
      } else {
        this.y += this.speed;
      }
      
      this.fish.style.transform = `translateY(${this.y}px)`;
      this.randomCountdown -= this.speed;
    }
  }

  // ------------
  // Progress bar
  // ------------

  class ProgressBar {
    constructor() {
      this.wrapper = document.querySelector('.progress-bar');
      this.progressBar = this.wrapper.querySelector('.progress-gradient-wrapper');
      this.progress = 50;
    }

    reset() {
      this.progress = 50;
    }

    drain() {
      if (this.progress > 0) this.progress -= 0.4;
      if (this.progress < 1) this.progress = 0;
    }

    fill() {
      if (this.progress < 100) this.progress += 0.3;
    }

    detectGameEnd() {
      if (this.progress >= 100) {
        // successTimeline().play();
        successTimeline().invalidate().play(0);

        gameOver = true;
      }
    }

    updateUi() {
      this.progressBar.style.height = `${this.progress}%`
    }
  }

  // -----------
  // Application
  // -----------

  const gameBody = document.querySelector('.game-body');
  let keyPressed = false;
  const indicator = new Indicator();
  const progressBar = new ProgressBar();
  const fish = new Fish();

  // ------------
  // Mouse events
  // ------------

  window.addEventListener('mousedown', indicatorActive);
  window.addEventListener('mouseup', indicatorInactive);
  window.addEventListener('keydown', indicatorActive);
  window.addEventListener('keyup', indicatorInactive);
  window.addEventListener('touchstart', indicatorActive);
  window.addEventListener('touchend', indicatorInactive);

  function indicatorActive () {
    if (!keyPressed) {
      keyPressed = true;
      document.body.classList.add('indicator-active');
    }
  }

  function indicatorInactive () {
    if (keyPressed) {
      keyPressed = false;
      document.body.classList.remove('indicator-active');
    }
  }

  // ----------
  // Reset game
  // ----------

  const niceCatch = document.querySelector('.nice-catch');
  const perfect = document.querySelector('.perfect');
  const successButton = document.querySelector('.success');
  const game = document.querySelector('.game');
  successButton.addEventListener('click', resetGame);

  function resetGame() {
    progressBar.reset();
    fish.resetPosition();

    successButton.removeAttribute('style');
    niceCatch.removeAttribute('style');
    perfect.removeAttribute('style');
    game.removeAttribute('style');

    gameOver = false;
    animationLoop();
  }

  // ----------------
  // Success timeline
  // ----------------

  function successTimeline() {
    TweenMax.set(".success", {display: 'flex'});
    TweenMax.set(".nice-catch", {y: 50});
    TweenMax.set(".perfect", {perspective:800});
    TweenMax.set(".perfect", {transformStyle:"preserve-3d"});
    TweenMax.set(".perfect", {rotationX:-90});

    const tl = new TimelineMax({paused: true})
    .to('.game', 0.2, {opacity: 0})
    .to('.success', 0.5, {ease: Power3.easeOut, opacity: 1}, 'ending')
    .to('.nice-catch', 0.5, {ease: Power3.easeOut, y: 0}, 'ending')
    .to('.perfect', 3, {ease: Elastic.easeOut.config(1, 0.3), rotationX: 0}, '+=0.2');

    return tl;
  }

  // -------------
  // Initiate loop
  // -------------

  animationLoop();

})();

// -------
// Seaweed
// -------

(function() {
  let seaweed = [];
  const canvas = document.querySelector('[data-element="seaweed"]');
  canvas.width = canvas.clientWidth * 2;
  canvas.height = canvas.clientHeight * 2;
  const context = canvas.getContext('2d');

  function animationLoop() {
    clearCanvas();
    seaweed.forEach(seaweed => seaweed.draw());

    requestAnimationFrame(animationLoop);
  }

  function clearCanvas() {
    context.clearRect(0, 0, canvas.width, canvas.height);
  }

  class Seaweed {
    constructor(segments, spread, xoff) {
      this.segments = segments;
      this.segmentSpread = spread;
      this.x = 0;
      this.xoff = xoff;
      this.y = 0;
      this.radius = 1;
      this.sin = Math.random() * 10;
    }

    draw() {
      context.beginPath();
      context.strokeStyle="#143e5a";
      context.fillStyle="#143e5a";
      context.lineWidth=2;
      for (let i = this.segments; i >= 0; i--) {
        if (i === this.segments) {
          context.moveTo(
            Math.sin(this.sin + i) * i/2.5 + this.xoff,
            canvas.height + (-i * this.segmentSpread),
          );
        } else {
          context.lineTo(
            Math.sin(this.sin + i) * i/2.5 + this.xoff,
            canvas.height + (-i * this.segmentSpread),
          );
        }
        // context.arc(Math.sin(this.sin + i) * 10 + 30, this.y + (this.segmentSpread * i), this.radius, 0, 2*Math.PI); 
      }
      context.stroke();

      this.sin += 0.05;
    }
  }

  seaweed.push(new Seaweed(6, 8, 25));
  seaweed.push(new Seaweed(8, 10, 35));
  seaweed.push(new Seaweed(4, 8, 45));

  animationLoop()
})();

// -----------------
// Reel line tension
// -----------------

(function() {
  let line = null;
  const canvas = document.querySelector('[data-element="reel-line-tension"]');
  canvas.width = canvas.clientWidth * 2;
  canvas.height = canvas.clientHeight * 2;
  const context = canvas.getContext('2d');

  function animationLoop() {
    clearCanvas();
    line.draw();
    line.animate();

    requestAnimationFrame(animationLoop);
  }

  function clearCanvas() {
    context.clearRect(0, 0, canvas.width, canvas.height);
  }

  class Line {
    constructor() {
      this.tension = 0;
      this.tensionDirection = 'right';
    }

    draw() {
      context.beginPath();
      context.strokeStyle="#18343d";
      context.lineWidth=1.3;
      context.moveTo(canvas.width, 0);
      context.bezierCurveTo(
        canvas.width,canvas.height/2 + this.tension,
        canvas.width/2,canvas.height + this.tension,
        0,canvas.height
      );
      context.stroke();
    }

    animate() {
      if (document.body.classList.contains('collision')) {
        if (this.tension > -30) this.tension -= 8;
      } else {
        if (this.tension < 0) this.tension += 4;
      }
    }
  }

  line = new Line();
  animationLoop()
})();

// -------
// Bubbles
// -------

(function() {
  let bubbles = {};
  let bubblesCreated = 0;
  const canvas = document.querySelector('[data-element="bubbles"]');
  canvas.width = canvas.clientWidth * 2;
  canvas.height = canvas.clientHeight * 2;
  const context = canvas.getContext('2d');

  function animationLoop() {
    clearCanvas();
    Object.keys(bubbles).forEach(bubble => bubbles[bubble].draw());

    requestAnimationFrame(animationLoop);
  }

  function clearCanvas() {
    context.clearRect(0, 0, canvas.width, canvas.height);
  }

  class Bubble {
    constructor() {
      this.index = Object.keys(bubbles).length;
      this.radius = Math.random() * (6 - 2) + 2;
      this.y = canvas.height + this.radius;
      this.x = canvas.width * Math.random() - this.radius;
      this.sin = (this.style > 0.5) ? 0 : 5;
      this.style = Math.random();
      this.childAdded = false;
      this.speed = 1;
      this.sway = Math.random() * (0.03 - 0.01) + 0.01;
      this.swayDistance = Math.random() * (canvas.width - canvas.width/2) + canvas.width/2;
    }

    draw() {
      context.beginPath();
      context.strokeStyle="#abe2f9";
      context.lineWidth=2;
      context.arc(this.x + this.radius, this.y + this.radius, this.radius, 0, 2*Math.PI);
      context.stroke();
      this.x = (Math.sin(this.sin) * this.swayDistance) + this.swayDistance - this.radius;
      this.sin += this.sway;
      this.y -= this.speed;

      if (this.y + this.radius < 0) {
        delete bubbles[this.index];
      }

      if (this.y < canvas.height * 0.6) {
        if (!this.childAdded) {
          bubbles[bubblesCreated] = new Bubble();
          bubblesCreated++;
          this.childAdded = true;
        }
      }
    }
  }

  bubbles[bubblesCreated] = new Bubble();
  bubblesCreated++;

  animationLoop()
})();
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js