- var maxBubbles = 40;

form(action="")
  - for (var b = 0; b < maxBubbles; ++b) {
      input(type="checkbox",id="bubble" + (b + 1))
      label(for="bubble" + (b + 1)).bubble
        span.bubble-inner
            span
  - }
  div.timer= "Time"
    div.time-left
      span
  div.score
  div.intro= "Pop as many bubbles as you can!"
  div.menu
    h1= "Game Over"
    ul
      li
        button(type="submit")
          svg(class="menu-icon",viewBox="0 0 24 24"): path(vector-effect="non-scaling-stroke",fill="#000000",d="M2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2A10,10 0 0,0 2,12M4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12M10,17L15,12L10,7V17Z")/
          = "Play Again"
View Compiled
// game itself
$bg: #08c;
$minGameW: 300px;
$timer: 45s;
// bubbles
$bubbleSize: 15vw;
$bubbleColor: #f44 #f84 #ff8 #8f8 #8ff #88f;
$bubbleSizeCut: 0.5 0.6 0.7 0.8 0.9 1;
$maxBubbles: 40;
$minDur: 2s;
// font
$minFontSize: 20pt;
$addedFontSize: 1.1vw;
// score
$scoreW: 70px;
// bubble texture reference - http://img.brothersoft.com/screenshots/softimage/f/flowbubbles-69406-1.jpeg
@mixin createBubble($color, $size) {
  box-shadow: 0 (-$size * 0.019) ($size * 0.032) #fff inset, 0 (-$size * 0.051) ($size * 0.128) $color inset, 0 ($size * 0.01) ($size * 0.01) $color inset, ($size * 0.01) 0 ($size * 0.032) #fff inset, -($size * 0.01) 0 ($size * 0.032) #fff inset, 0 ($size * 0.026) ($size * 0.128) lighten($color,60%) inset;
  width: $size;
  height: $size;
  max-width: $size;
  max-height: $size;
  &:before {
    top: $size * 0.115;
    left: $size * 0.179;
    width: $size * 0.16;
    height: $size * 0.064;
  }
  &:after {
    opacity: 0.1;
    top: $size * 0.16;
    left: $size * 0.16;
    width: $size;
    height: $size;
  }
  span {
    background: radial-gradient(at center bottom, transparent, transparent 70%, lighten($color,60%));
    top: ($size * 0.01);
    left: $size * 0.096;
    width: $size * 0.808;
    height: $size * 0.622;
  }
}

// used later for random bubble placement and animation delay
@function randomNumber($min, $max) {
  @return ceil((random() * $max) - $min) + $min;
}

body {
  background: $bg linear-gradient(lighten($bg,20%), $bg, darken($bg,20%));
  counter-reset: popped;
  margin: 0;
  overflow: hidden;
}

body,
button {
  font-family: Lato, sans-serif;
  font-size: calc(#{$minFontSize} + #{$addedFontSize});
  font-weight: 300;
  line-height: calc(#{$minFontSize} + #{$addedFontSize});
}

h1 {
  font-size: calc(#{$minFontSize * 1.5} + #{$addedFontSize * 1.5});
  margin-top: 40vh;
}

button,
path {
  transition: all 0.2s;
}

button {
  background: transparent;
  border: 0;
  padding: 0;
  -webkit-appearance: none;
  &:hover {
    color: #fff;
    path {
      fill: #fff;
    }
  }
}

form {
  margin: auto;
  position: relative;
  height: 100vh;
  min-width: $minGameW;
  width: 75%;
}

input {
  position: absolute;
  top: -20px;
  &:checked {
    counter-increment: popped;
    + .bubble {
      display: none;
    }
  }
}

.timer,
.score,
.intro,
.menu,
.bubble {
  position: absolute;
}

.timer,
.score,
.intro {
  z-index: 0;
}

.score,
.intro,
.menu {
  text-align: center;
}

.timer {
  display: flex;
  display: -webkit-flex;
  display: -ms-flex;
  justify-content: space-between;
  -webkit-justify-content: space-between;
  -ms-justify-content: space-between;
  align-items: center;
  -webkit-align-items: center;
  -ms-align-items: center;
  font-size: calc(#{$minFontSize/2} + #{$addedFontSize});
  line-height: calc(#{$minFontSize/2} + #{$addedFontSize});
  top: calc(15px + #{$addedFontSize});
  left: 5%;
  height: calc(15px + #{$addedFontSize});
  width: 90%;
}

.time-left {
  background: #fff;
  height: calc(15px + #{$addedFontSize});
  margin-left: 15px;
  opacity: 0.8;
  width: 100%;
  span {
    display: block;
    background: #c00;
    height: 100%;
    width: 100%;
    animation: timer $timer linear forwards;
  }
}

.score {
  font-size: calc(#{$minFontSize * 1.5} + #{$addedFontSize});
  margin-left: -$scoreW/2;
  top: calc(50px + #{$addedFontSize * 2});
  left: 50%;
  width: $scoreW;
  &::after {
    content: counter(popped);
  }
}

.intro {
  top: 40%;
  width: 100%;
  animation: fade 1s 2s linear reverse forwards;
}

.menu {
  width: 100%;
  height: 100%;
  visibility: hidden;
  z-index: 2;
  animation: fade 1s $timer linear forwards;
}

ul {
  top: 0;
  left: 0;
  list-style: none;
  margin: 0;
  padding: 0;
}

.menu-icon {
  margin-right: calc(10px + #{$addedFontSize});
  vertical-align: top;
  width: calc(32px + #{$addedFontSize});
  height: calc(32px + #{$addedFontSize});
}

// base bubble styles
.bubble {
  animation-name: ascend;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  top: 0;
  transform: translateY(100vh);
  will-change: transform;
  z-index: 1;
  .bubble-inner {
    border-radius: 50%;
    display: block;
    &:before,
    &:after,
    span,
    span:after {
      border-radius: 50%;
      content: "";
      display: block;
      position: absolute;
    }
    &:before {
      background: #fff;
      transform: rotate(-30deg);
    }
    &:after {
      background: radial-gradient(transparent, #000 60%, transparent 70%, transparent);
      transform: scale(1.2, 1.2);
    }
    &:hover {
      animation: shake 0.2s linear;
    }
    &:active {
      animation: pop 0.08s cubic-bezier(0.16, 0.87, 0.48, 0.99) forwards;
    }
  }
}

// style six kinds of bubbles with different animation delay, duration (speed), and size
@for $b from 1 through 6 {
  .bubble:nth-of-type(6n + #{$b}) {
    animation-duration: $minDur + (($b - 1) / 2);
    .bubble-inner {
      @include createBubble(nth($bubbleColor, $b), $bubbleSize * nth($bubbleSizeCut, $b));
    }
  }
}

// randomly assign positions and delays
@for $b from 1 through $maxBubbles {
  .bubble:nth-of-type(#{$b}) {
    left: 0% + randomNumber(0, 80);
    animation-delay: 0s + randomNumber(0, $maxBubbles);
  }
}

// animations
@keyframes ascend {
  from {
    transform: translateY(100vh);
    -webkit-transform: translateY(100vh);
  }
  to {
    transform: translateY(-$bubbleSize * 1.2);
    -webkit-transform: translateY(-$bubbleSize * 1.2);
  }
}

@keyframes shake {
  from {
    transform: scale(1, 1);
  }
  33% {
    transform: scale(1, 1.2);
  }
  66% {
    transform: scale(1.2, 1);
  }
  to {
    transform: scale(1, 1);
  }
}

@keyframes pop {
  from {
    opacity: 1;
    transform: translateZ(0) scale(1, 1);
  }
  to {
    opacity: 0;
    transform: translateZ(0) scale(1.75, 1.75);
  }
}

@keyframes fade {
  from {
    opacity: 0;
    visibility: hidden;
  }
  1% {
    opacity: 0;
    visibility: visible;
  }
  to {
    opacity: 1;
    visibility: visible;
  }
}

@keyframes timer {
  from {
    width: 100%;
  }
  to {
    width: 0%;
  }
}
View Compiled

External CSS

  1. https://fonts.googleapis.com/css?family=Lato

External JavaScript

This Pen doesn't use any external JavaScript resources.