<div id="container" class="container"></div>
html,
body,
.container {
  height: 100%;
  margin: 0;
  padding: 0;
  width: 100%;
}

body {
  background-color: #616161;
}

.container {
  overflow: hidden;
  perspective: 50px;
}

.bubble {
  animation: 1s linear infinite alternate brightnessChanging;
  border-radius: 50%;
  box-shadow: inset 0 0 5px rgba(255, 255, 255, .25);
  height: 50px;
  margin: -25px 0 0 -25px;
  overflow: hidden;
  position: absolute;
  width: 50px;
  transform: translateZ(0) rotate(-20deg);
  transition: transform;
}

  .bubble::before {
    background-color: rgba(255, 255, 255, 0.3);
    border-radius: 50%;
    content: '';
    height: 4px;
    margin: 4px 0 0px 17px;
    position: absolute;
    width: 15px;
  }

  .bubble::after {
    border-radius: 50%;
    box-shadow: inset 0 -8px 8px rgba(255, 255, 255, .15);
    content: '';
    height: 80%;
    position: absolute;
    margin: 15% 0 0 6%;
    width: 90%;
  }

@keyframes brightnessChanging {
  from {
    opacity: 1;
  }
  to {
    opacity: .75;
  }
}
/* jshint esnext:true */
class Bubble {
  constructor(params) {
    this.isMoving = false;
    this._shakeTimeout = null;
    this.opts = Object.assign({
      template: () => '<div></div>',
      maxMove: 20,
      maxDelay: 2,
      minDelay: 0.5,
      maxTime: 3,
      minTime: 0.5,
      additionalTransform: 'rotate(-20deg)'
    }, params || {});
    this.create();
    this.shake();
  }

  create() {
    let tempCont = this._tempCont;
    if (!tempCont) {
      tempCont = document.createElement('div');
      this._tempCont = tempCont;
    }
    tempCont.innerHTML = this.opts.template();
    this.elem = tempCont.children[0];
  }

  shake() {
    clearTimeout(this._shakeTimeout);
    let duration = 0;
    if (!this.isMoving) {
      const {
        maxMove, maxTime, minTime
      } = this.opts;
      const x = this._getRandom(-maxMove, maxMove, 0);
      const y = this._getRandom(-maxMove, maxMove, 0);
      const z = this._getRandom(-maxMove, maxMove, 0);
      duration = this._getRandom(minTime, maxTime);
      this._setPosition({
        x, y, z
      }, duration);
    }
    const {
      minDelay, maxDelay
    } = this.opts;
    const nextShakeDelay = Math.max(this._getRandom(minDelay, maxDelay),
      duration) * 1000;
    this._shakeTimeout = setTimeout(this.shake.bind(this), nextShakeDelay);
  }

  _setPosition(coords, duration) {
    const {
      x, y, z
    } = coords;
    const transformVal =
      `translate3d(${x}px, ${y}px, ${z}px) ${this.opts.additionalTransform}`;
    const style = this.elem.style;
    style.transitionDuration = `${duration}s`;
    window.requestAnimationFrame(() => style.transform = transformVal);
  }

  /**
   * Returns a random number from a range
   * @param {number} minVal
   * @param {number} maxVal
   * @param {number} [fixed=3]
   * @returns {number}
   */
  _getRandom(minVal, maxVal, fixed = 3) {
    let val = Math.random() * (maxVal - minVal) + minVal;
    let r = Math.pow(10, Math.abs(fixed));
    return Math.round(val * r) / r;
  }
}

// bubbles
// ================
const bubblesTotal = 101;
const bubblesCont = document.querySelector('#container');
const bubbles = [];

for (let i = 0; i < bubblesTotal; i++) {
  let bubble = new Bubble({
    template: bubbleTemplate
  });
  let elem = bubble.elem;
  elem.style.top = Math.round(Math.random() * 100) + '%';
  elem.style.left = Math.round(Math.random() * 100) + '%';
  bubblesCont.appendChild(elem);
  bubbles.push(bubble);
}

window.bubbles = bubbles;

/**
 * @param {object} params
 * @returns (string) html string
 */
function bubbleTemplate(params) {
  return `<div class="bubble"></div>`;
}
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.