Skip to content

JavaScript Balloon Blasting Game

CODEPEN:

JavaScript Balloon Blasting Game

Written by: Gabriele Corti


HTLM CODE:

<!-- structure
  - a heading, with two kewords each separated in <span> elements
  - a subheading, to show 1 point being scored
  - an SVG to keep track of the score

  the balloons themselves are included (with the same structure of the SVG minus the text) through the script and at the bottom of the body
-->

<h1>
  <span>cpc</span>
  <span>pop</span>
</h1>
<h2>+1</h2>


<svg id="score" viewBox="0 0 150 100" width="100" height="100">
  <path d="M 50 0 a 50 45 0 0 1 50 45 a 50 50 0 0 1 -50 50 a 50 50 0 0 1 -50 -50 a 50 45 0 0 1 50 -45" fill="#FF1EAD" />
  <path d="M 50 94 a 6 6 0 0 1 6 6 h -12 a 6 6 0 0 1 6 -6" fill="#FF1EAD" />
  <path d="M 50 5 a 40 40 0 0 1 40 40 a 5 5 0 0 1 -5 5 a 40 40 0 0 0 -40 -40 a 5 5 0 0 1 5 -5" fill="#fff" opacity="0.3" />

  <text x="150" y="50" fill="#000" text-anchor="end" alignment-baseline="middle">0x</text>
</svg>

CSS CODE:

@import url("https://fonts.googleapis.com/css?family=Montserrat:900");

/* colors for the weekly challenge */
:root {
  --dark: #000;
  --light: #fff;
  --accent: #ff1ead;
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  font-family: "Montserrat", sans-serif;
  height: 100vh;
  overflow: hidden;
  /* split the background betweenn the dark and light picks */
  background: linear-gradient(to right, var(--dark) 50%, var(--light) 50%);
}
/* absolute position the heading in the bottom and center section of the body */
h1 {
  position: absolute;
  bottom: 1rem;
  left: 50%;
  transform: translate(-50%, 0);
  text-transform: uppercase;
  font-size: 3rem;
  /* start out without spacing and animate this property with a bit of a bounce (with the cubic bezier function) */
  letter-spacing: 0;
  /* ! animate after a slight delay, as to animate the letter spacing after the connected pseudo element has finished animating */
  animation: spaceout 0.3s 0.75s cubic-bezier(0.35, -0.75, 0.55, 2) forwards;
}
/* animation increasing the letter spacing of the heading */
@keyframes spaceout {
  to {
    letter-spacing: 0.5rem;
  }
}

/* through a pseudo element add a balloon in the middle of the heading */
h1:before {
  position: absolute;
  content: "";
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 50px;
  height: 50px;
  background-size: 100%;
  background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M 50 0 a 50 45 0 0 1 50 45 a 50 50 0 0 1 -50 50 a 50 50 0 0 1 -50 -50 a 50 45 0 0 1 50 -45" fill="%23FF1EAD" /><path d="M 50 94 a 6 6 0 0 1 6 6 h -12 a 6 6 0 0 1 6 -6" fill="%23FF1EAD" /><path d="M 50 5 a 40 40 0 0 1 40 40 a 5 5 0 0 1 -5 5 a 40 40 0 0 0 -40 -40 a 5 5 0 0 1 5 -5" fill="%23fff" opacity="0.3" /></svg>');
  /* animate the balloon as to pop out of sight */
  animation: pop 0.2s 0.6s ease-out forwards;
}
/* animation 'bursting' the balloon with a small increase in size followed by a sharp quick fade out  */
@keyframes pop {
  0% {
    transform: translate(-50%, -50%) scale(1);
  }
  99% {
    transform: translate(-50%, -50%) scale(1.1);
    opacity: 1;
    visibility: 1;
  }
  100% {
    opacity: 0;
    visibility: 0;
  }
}
/* color the two span elements making up the heading with the color opposite to the hues used in the background color */
h1 span:not(:nth-of-type(1)) {
  color: var(--dark);
}
h1 span:not(:nth-of-type(2)) {
  color: var(--light);
}

/* absolute position the subheading and hide it by default
! the left and top properties are detailed in the script, where the opacity is also restored
! add a transition, but only for the opacity
*/
h2 {
  color: var(--accent);
  position: absolute;
  opacity: 0;
  visibility: 0;
  transition: opacity 0.2s ease-out;
}

/* include the SVG making up the scoreboard in the bottom right of the page
! as the game comes to a close, this is positioned and scaled up in the center of the page
*/
svg#score {
  width: 70px;
  height: 70px;
  position: absolute;
  right: 1rem;
  bottom: 1rem;
}
/* use the selected font family for the text in the svg as well  */
svg#score text {
  font-family: "Montserrat", sans-serif;
  font-size: 1.7rem;
}

/* for each and every balloon give some default value to the transform property
- translation to match the translation in the pop animation
- scale to hide it by default
the idea is to animate it in and out of sight using the scale property
and to have it burst using the already defined pop animation
*/
svg.balloon {
  transform: translate(-50%, -50%) scale(0);
}

/* animation scaling the balloon to its rightful size, before hiding it back again */
@keyframes appear {
  0% {
    transform: translate(-50%, -50%) scale(0);
  }
  20%,
  90% {
    transform: translate(-50%, -50%) scale(1);
  }
  100% {
    transform: translate(-50%, -50%) scale(0);
  }
}

/* animation centering the SVG making up the score */
@keyframes showScore {
  to {
    right: 50%;
    bottom: 50%;
    transform: translate(50%, 0) scale(3.5);
  }
}

/* animation highlighting the score through the prescribed text (remember to apply this to the text and after the previous animation, which is applied to the svg#score as a whole) */
@keyframes highlightScore {
  50% {
    visibility: hidden;
    opacity: 0;
  }
}

JAVASCRIPT CODE

// the idea is to wait for the animation on the heading to finish and then begin a simple game in which x balloons appear on page
// as a click event is registered on one of these balloons, the the score highlighted in the bottom right is incremented
// a simple animation plays out to actually pop the balloon (to avoid multiple clicks being registered it'd be useful to remove the event listener after the first instance)

/* necessary elements
- the body (in which the balloons are appended)
- the svg (keeping track of the score, as to animate it in the center of the page as the game comes to a close)
- the heading (animated directly in CSS with the name of the game)
- the subheading (positioned whenever a click on a balloon is registered)
*/
const body = document.querySelector('body');
const scoreboard = body.querySelector('svg#score');
const heading = body.querySelector('h1');
const subheading = body.querySelector('h2');


/* global variables
- a variable to count the number of balloons included in the body
- a variable to cap the number of balloons to be included in the body
- a variable to keep track of the balloon actually being burst
- the width and height of the body
- two variables to distinguish the smaller and greater between the width and height
- a variable to size the SVG to be 1/10th of the greater value
*/
let counter = 0;
const balloons = 15;
let score = 0;
const { offsetWidth: width, offsetHeight: height } = body;
const [min, max] = [width, height].sort((a, b) => (a > b ? 1 : -1));
const tenth = max / 10;

// utility function returning a random integer between 0 and a selected number (defaulted to 10)
const randomInt = (cap = 10) => Math.floor(Math.random() * cap);

/* function called in response to a click event on any SVG element of class .balloon
- animate the SVG out of sight
- show the subheading with the +1 signallng the successful move
- increase the score in the scoreboard
*/
function popBaloon(e) {
  this.style.animation = 'pop 0.2s ease-out forwards';

  const { clientX: left, clientY: top } = e;
  subheading.style.left = `${left}px`;
  subheading.style.top = `${top}px`;
  subheading.style.opacity = 1;


  score += 1;
  scoreboard.querySelector('text').textContent = `${score}x`;

  // remove also the event listener to the specific SVG, as to avoid counting the same balloon twice
  this.removeEventListener('click', popBaloon);
}

/* function called in response to the animationend on the last SVG element
- hide the subheading back
- after a brief delay show the score higlighting the selected SVG in the center of the page
*/
function showScore() {
  subheading.style.opacity = 0;
  const timeoutID = setTimeout(() => {
    scoreboard.style.animation = 'showScore 1s ease-out forwards';
    scoreboard.querySelector('text').style.animation = 'highlightScore 0.5s 1.2s 5 step-end';
    clearTimeout(timeoutID);
  }, 3000);
}

// function called when aniomationend is registered on the heading, as to play the game **after** the trivial animation
function playGame() {
  // include the balloons at a specified interval (repeated for as many balloons as specified in the global variable)
  const intervalID = setInterval(() => {
    // compute the horizontal and vertical coordinate of the SVG through the randomInt function
    // up to the width and height of the body respectively
    const left = randomInt(width);
    const top = randomInt(height);

    // create an svg element with default attributes
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('viewBox', '0 0 100 100');
    svg.setAttribute('width', tenth);
    svg.setAttribute('height', tenth);
    // absolute position the SVG ih the body following the random coordinates
    svg.style.position = 'absolute';
    svg.style.top = `${top}px`;
    svg.style.left = `${left}px`;
    // add a class to distinguish said SVG from the existing one(s)
    svg.classList.add('balloon');
    // play the appear animation created in the stylesheet with a random delay
    svg.style.animation = `appear 2s ${randomInt(5)}s ease-out forwards`;
    // attach the event listener to fire the popBaloon function on click
    svg.addEventListener('click', popBaloon);

    // add to the SVG the path elements making up the balloon's shape
    svg.innerHTML = `
    <path d="M 50 0 a 50 45 0 0 1 50 45 a 50 50 0 0 1 -50 50 a 50 50 0 0 1 -50 -50 a 50 45 0 0 1 50 -45" fill="#FF1EAD" />
    <path d="M 50 94 a 6 6 0 0 1 6 6 h -12 a 6 6 0 0 1 6 -6" fill="#FF1EAD" />
    <path d="M 50 5 a 40 40 0 0 1 40 40 a 5 5 0 0 1 -5 5 a 40 40 0 0 0 -40 -40 a 5 5 0 0 1 5 -5" fill="#fff" opacity="0.3" />
    `;

    // append the balloon to the body
    body.appendChild(svg);

    // increment the counter variable and clear the interval if the maximum number of balloons is reached
    counter += 1;
    // if counter reaches the total number of balloons clear the interval and call the function showing the score, as the last SVG finishes animating
    if (counter >= balloons - 1) {
      svg.addEventListener('animationend', showScore);
      clearInterval(intervalID);
    }
  }, 1000);
}

// listen for the animationend event on the heading, at which point call the function to play the game
heading.addEventListener('animationend', playGame);

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: