<form class="gravity-form">

  <span class="screen-reader">Set Pull</span>
  <button type="button" id="decrease-pull" class="decrease-pull"><span class="screen-reader">Decrease Pull</span>-</button>
  <button type="button" id="increase-pull" class="increase-pull"><span class="screen-reader">Increase Pull</span>+</button>

</form>



<div id="field">
  <div id="balls"></div>
  <div class="field-info-container" aria-live="polite">
    <div class="field-info">
      <h3>Current Pull</h3>
      <p class="current-pull">
        <span id="current-gravity"></span>
        <span class="percentage-sign">%</span>
      </p>
    </div>
  </div>
</div>

body {
  background: #d8e8e3;
  font-family: Trebuchet MS,Lucida Grande,Lucida Sans Unicode,Lucida Sans,Tahoma, sans-serif;
  padding: 200px 20px 800px;
  max-width: 600px;
  margin: auto;
}

.screen-reader {  
  clip: rect(1px, 1px, 1px, 1px);
	position: absolute !important;
	height: 1px;
	width: 1px;
	overflow: hidden;

	&:focus {
		background-color: #ddd;
		border-radius: 3px;
		box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);
		clip: auto !important;
		color: #444;
		display: block;
		font-size: 1.2rem;
		font-weight: bold;
		height: auto;
		left: 5px;
		line-height: normal;
		padding: 15px 23px 14px;
		text-decoration: none;
		top: 5px;
		width: auto;
		z-index: 100000; /* Above WP toolbar. */
	}
}

#field,
.field-info-container {
  background: #eee;
  position: absolute;
  width: 200px;
  height: 200px;
  // border: 1px solid #fff;
  border-radius: 50%;
  margin: auto;
  top: 250px;
  left: 40%;
}

.field-info-container {
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  flex-direction: column;
  background: #fff;
  box-shadow: 0 1px 15px rgba(255,255,255,.8);
  z-index: 5;
  top: 0;
  left: 0;
  
  &:after,
  &:before {
    display: block;
    content: '';
    position: absolute;
    width: 200%;
    height: 200%;
    border: 2px solid #fff;
    border-radius: 50%;
  }
  
  &:before {
    width: 350%;
    height: 350%;
    border: 2px solid rgba(255,255,255,0.5);
  }
}

.ball {
  position: absolute;
  border-radius: 50%;
  width: 10px;
  height: 10px;
  background: #0ebeff;
}

button {
  float: left;
  border: none;
  background: #fff;
  width: 50px;
  height: 50px;
  font-size: 20px;
  font-weight: bold;
  border-radius: 50%;
  cursor: pointer;
  color: #0ebeff;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: row;
  margin-right: 10px;
  transition: all .15s ease-out;
  
  &:hover {
    transform: scale(1.15);
  }
}

.decrease-pull {
  margin-top: 4px;
  width: 42px;
  height: 42px;
  &:hover {
    transform: scale(0.9);
  }
}


button:hover,
button:focus {
  outline: none;
  box-shadow: 0 0 2px #0ebeff;
}

.current-pull {
  font-size: 40px;
  position: relative;
}

.percentage-sign {
  font-size: 15px;
  position: absolute;
  top: 5px;
}

.gravity-form {
  position: relative;
  z-index: 99;
}

button:disabled {
  color: #ccc;
  opacity: 0.7;
}
View Compiled
// Original JavaScript code by Chirp Internet: www.chirp.com.au
// Please acknowledge use of this code by including this header.
// Modified very, very heavily by Jeremy Jones: https://jeremyjon.es
const numberOfBalls = 80;
const minSize = 4;
const maxSize = 18;
const colors = [
  '#0ebeff',
  '#0ebeff', // I want more of the blue ones
  '#59C9A5',
  '#EDCA04' 
];

class Orbit {
  constructor() {
    this.field = document.getElementById('field')
    this.ballsEl = document.getElementById('balls')
    this.gravitationalPull = 50

    this.gravityText = document.getElementById('current-gravity')
    this.increasePullBtn = document.getElementById('increase-pull')
    this.decreasePullBtn = document.getElementById('decrease-pull')
    this.balls = null

    this.ballSettings = {
      num: numberOfBalls,
      minSize: minSize,
      maxSize: maxSize,
    }

    this.start = 0

    this.init(40)
  }

  init(ballsNum) {
    this.balls = this.createBalls(ballsNum)
    window.requestAnimationFrame(this.step.bind(this))
    this.setPullButtons(this.gravitationalPull)
    this.increasePullBtn.addEventListener('click', this.increaseGravitationalPull.bind(this))
    this.decreasePullBtn.addEventListener('click', this.decreaseGravitationalPull.bind(this))
    

    // uncomment to have planet track cursor
   // document.onmousemove = getCursorXY;

  }

  createBalls() {
    let size;
    for(let i = 0; i < this.ballSettings.num; i++) {
      // get random size between setting sizes
      size = Math.ceil(this.ballSettings.minSize + (Math.random() * (this.ballSettings.maxSize - this.ballSettings.minSize)))
      this.createBall(size)
    }

    // return all the balls
    return document.querySelectorAll('.ball');
  }

  createBall(size) {
    let newBall, stretchDir

    newBall = document.createElement("div")
    stretchDir = (Math.round((Math.random() * 1)) ? 'x' : 'y') 
    newBall.classList.add('ball')
    newBall.style.width = size + 'px'
    newBall.style.height = size + 'px'
          newBall.style.background = this.getRandomColor();
    newBall.setAttribute('data-stretch-dir', stretchDir) // either x or y
    newBall.setAttribute('data-stretch-val',  1 + (Math.random() * 5))
    newBall.setAttribute('data-grid', field.offsetWidth + Math.round((Math.random() * 100))) // min orbit = 30px, max 130
    newBall.setAttribute('data-duration', 3.5 + Math.round((Math.random() * 8))) // min duration = 3.5s, max 8s
    newBall.setAttribute('data-start', 0)
    this.ballsEl.appendChild(newBall)
  }

  callStep(timestamp) {
    return this.step(timestamp)
  }

  step(timestamp) {
    let progress, x, xPos, yPos, y, stretch, gridSize, duration, start;

    for(let i = 0; i < this.balls.length; i++) {

      start = this.balls[i].getAttribute('data-start')
      if(start == 0) {
        start = timestamp
        this.balls[i].setAttribute('data-start', start)
      }

      gridSize = this.balls[i].getAttribute('data-grid')
      duration = this.balls[i].getAttribute('data-duration')
      progress = (timestamp - start) / duration / 1000 // percent
      stretch = this.balls[i].getAttribute('data-stretch-val')

      if(this.balls[i].getAttribute('data-stretch-dir') === 'x') {
        x = ((stretch * Math.sin(progress * 2 * Math.PI)) * (1.05 - (this.gravitationalPull/100)))// x = ƒ(t)
        y = Math.cos(progress * 2 * Math.PI) // y = ƒ(t)
      } else {
        x = Math.sin(progress * 2 * Math.PI) // x = ƒ(t)
        y = ((stretch * Math.cos(progress * 2 * Math.PI)) * (1.05 - (this.gravitationalPull/100))) // y = ƒ(t)
      }

      xPos = field.clientWidth/2 + (gridSize * x)
      yPos = field.clientHeight/2 + (gridSize * y)
      this.balls[i].style.transform = 'translate3d('+xPos + 'px, '+yPos + 'px, 0)'

      // if these are true, then it's behind the planet
      if(((this.balls[i].getAttribute('data-stretch-dir') === 'x') && (((field.offsetWidth/2) - this.balls[i].offsetWidth) * -1) < xPos && xPos < ((field.offsetWidth/2) + this.balls[i].offsetWidth)) || ((this.balls[i].getAttribute('data-stretch-dir') === 'y') && (((field.offsetWidth/2) - this.balls[i].offsetWidth) * -1) < yPos && yPos < ((field.offsetWidth/2) + this.balls[i].offsetWidth))) {
        // backside of the moon
        this.balls[i].style.zIndex = '-1'
      } else {
        // ...front side of the moon
        this.balls[i].style.zIndex = '9'
      }

      if(progress >= 1) {
        this.balls[i].setAttribute('data-start', 0) // reset to start position
      }
    }

    window.requestAnimationFrame(this.step.bind(this))
  }


  // since I don't know physics, this is an approriximation
  setGravitationalPull(percent) {
    let step, steps, time, direction

    if(percent < 0) {
      return
    }

    if(100 < percent) {
      return
    }

    if(percent === this.gravitationalPull) {
      return
    }

    steps = 20
    step = Math.abs(percent - this.gravitationalPull)/steps
    direction = (percent < this.gravitationalPull ? '-' : '+')

    // get the current pull and step it down over 20 steps so it's smoother than jumping straight there
    for(let i = 0; i < steps; i++) {
      // set the time this will fire
      time = i * (i/Math.PI)
      // minimum time span
      if(time < 4) {
        time = 4
      }
      // set the function
      setTimeout(()=>{
        if(direction === '-') {
          this.gravitationalPull -= step 
        } else {
          this.gravitationalPull += step 
        }
        
      }, time);

      // on our last one, set the gravitationalPull to its final, nicely rounded number
      if(i === steps - 1) {
        setTimeout(()=>{
          this.gravitationalPull = Math.round(this.gravitationalPull)
          this.setPullButtons()
        }, time + 20)
      }
    }

  }


  setPullButtons() {
    if(this.gravitationalPull <= 0) {
      this.decreasePullBtn.disabled = true
      this.increasePullBtn.disabled = false
    } 

    else if(100 <= this.gravitationalPull) {
      this.decreasePullBtn.disabled = false
      this.increasePullBtn.disabled = true
    } 

    else {
      this.decreasePullBtn.disabled = false
      this.increasePullBtn.disabled = false
    }

    this.gravityText.innerHTML = this.gravitationalPull
  }

  increaseGravitationalPull() {
    this.setGravitationalPull(this.gravitationalPull + 10)
  }

  decreaseGravitationalPull() {
    this.setGravitationalPull(this.gravitationalPull - 10)
  }

  // if you want the planet to track the cursor
  getCursorXY(e) {
    let cursorPos = {
      x: (window.Event) ? e.pageX : event.clientX + (document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft),
      y: (window.Event) ? e.pageY : event.clientY + (document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop)
    }
    
    field.style.left = cursorPos.x - (field.offsetWidth/2) + "px"
    field.style.top = cursorPos.y - (field.offsetHeight/2) + "px"
  }

  getRandomColor() {
    
    return colors[Math.floor((Math.random() * colors.length))]
  }
} 

new Orbit()
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.