menuBg = rgba(255,255,255,0.5)
highlightBg = rgba(255,255,255,0.85)
bigMrg = .5em
lilMrg = .25em

html{overflow:hidden}

#toolbar
  position fixed
  background menuBg
  left 0
  top 0
  max-height 100%
  overflow-y auto
  padding bigMrg
  padding-bottom 0
  font 10pt sans-serif
  
nav  
  margin-bottom bigMrg
  background menuBg
  padding lilMrg
  
nav:not(:first-child)
  transition all .5s ease-in-out
  overflow hidden
  
nav:not(:first-child):not(:last-child)
  height 4em
  
h2
  margin-bottom lilMrg
  padding lilMrg
  background menuBg
  user-select none
  cursor default

input
button:not(:last-child)
  margin-right lilMrg
  
[type='range']
  width 10em
  cursor grab
  
[type='number']
  max-width 4em
  background menuBg
  border none
  outline none

button
  background menuBg
  border none
  outline none
  cursor pointer
  
button:hover
  background highlightBg
  
nav:last-child  
nav:first-child button:last-child
nav:last-child button:last-child
  float right
  
#toolbar.collapsed nav:not(:first-child)
  height 0
  overflow hidden
  border 0
  margin 0
  padding 0
View Compiled
//imported THREE from https://cdnjs.cloudflare.com/ajax/libs/three.js/84/three.js
//imported THREE.OrbitControls from https://threejs.org/examples/js/controls/OrbitControls.js

//prevent autorun log spam
console.clear()

///shortcut junk
const pi = Math.PI,
      pi2 = pi * 2,
      hpi = pi / 2,
      rad = pi / 180,
      pow = Math.pow,
      sqrt = Math.sqrt,
      sin = Math.sin,
      cos = Math.cos,
      abs = Math.abs,
      angleBetween = (x1, y1, x2, y2) => Math.atan2(y2 - y1, x2 - x1),
      eps = 1E-6,
      
      flatGeom = (type, ...args) => {
        let geom = new THREE[type[0].toUpperCase() + type.slice(1, Infinity) +'Geometry'](...args)

        geom.computeFlatVertexNormals()

        return geom
      }


///Basic scene
const renderer = new THREE.WebGLRenderer(),
      scene = new THREE.Scene(),
      camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000),
      controls = new THREE.OrbitControls(camera, renderer.domElement),
      
      //aLight = new THREE.AmbientLight(0x222222),
      //dLight = new THREE.DirectionalLight(0xffffff, 0.5),
      
      gridXZ = new THREE.GridHelper(100, 10)
    
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
document.body.appendChild(renderer.domElement)

camera.position.y = 20;
camera.position.z = 20;
camera.position.x = 20;
camera.lookAt(new THREE.Vector3(0, 0, 0))

//dLight.position.set(50, 20, 0)
//dLight.castShadow = true
//dLight.shadow.mapSize.width = dLight.shadow.mapSize.height = 64

//scene.add(aLight)
//scene.add(dLight)
scene.add(gridXZ)


///kepler orbits
class Orbital {
  constructor (a = 10, e = 0, p = 0, i = 0, l = 0, m = 1, t = 0) {
    this.baseMatrix = new THREE.Matrix4
    this.offsetMatrix = new THREE.Matrix4
    
    this.matrix = new THREE.Matrix4
    this.euler = new THREE.Euler(0, 0, 0,'XZY')
    this.vector = new THREE.Vector3
    
    this.baseVector = new THREE.Vector2
    
    this.majorRadius = a
    this.eccentricity = e
    this.periapsis = p
    this.inclination = i
    this.ascendingLongitude = l
    
    this.parentMass = m
    this.phase = t
  }
  
  get minorRadius () {
    return this.majorRadius * sqrt(1 - pow(this.eccentricity, 2))
  }
  
  get focusOffset () {
    return this.majorRadius * this.eccentricity
  }
  
  get period () {
    return pi2 * sqrt(pow(this.majorRadius, 3) / pow(this.parentMass, 2))
  }

  meanAnomaly (time) {
    let period = this.period
    
    return (pi2 * time + this.phase * period * pi2) / period
  }
  
  eccentricAnomaly (meanAnomaly) {
    let E = meanAnomaly, E_next = 0, loops = 0
    
    while ( loops++ < 10 ) {
      E_next = E + (meanAnomaly - (E - this.eccentricity *  sin(E))) / (1 - this.eccentricity * cos(E))
      
      if (abs(E_next - E) < eps) break
      
      E = E_next
    }
    
    return E
  }
  
  trueAnomaly (eccentricAnomaly) {
    let eccSqr = pow(this.eccentricity, 2),
        cosAnom = cos(eccentricAnomaly),
        denom = 1 - this.eccentricity * cosAnom,
        
        cosF = (cosAnom - this.eccentricity) / denom,
        sinF = (sqrt(1 - eccSqr) * sin(eccentricAnomaly)) / denom,
        len = this.majorRadius * (1 - eccSqr) / (1 + this.eccentricity * cosF)
    
    this.baseVector.set(len * cosF, len * sinF)
    
    this.offsetMatrix.makeRotationFromEuler(this.euler.set(pi,-angleBetween(this.baseVector.x, this.baseVector.y, -this.focusOffset, 0),pi))
    
    this.baseMatrix.makeTranslation(this.baseVector.x, 0, this.baseVector.y).multiply(this.offsetMatrix)
    
    this.offsetMatrix.makeRotationFromEuler(this.euler.set(this.inclination * pi, this.periapsis * pi, this.ascendingLongitude * pi))
    
    this.matrix.identity().multiply(this.offsetMatrix).multiply(this.baseMatrix)
    
    return this.vector.set(0,0,0).applyMatrix4(this.matrix)
  }
  
  solveForVector (time = 0) {
    return this.trueAnomaly(this.eccentricAnomaly(this.meanAnomaly(time)))
  }
  
  solveForMatrix (time = 0) {
    this.trueAnomaly(this.eccentricAnomaly(this.meanAnomaly(time)))
    
    return this.matrix
  }
}

class OrbitalPath {
  constructor (orbital) {
    this.orbital = orbital
    this.mesh = new THREE.Line(new THREE.Geometry, new THREE.LineBasicMaterial({ color : 0xff0000 }))
  }
  
  update () {
    if(!this.orbital) return;
    
    let geom = this.mesh.geometry,
        width = this.orbital.majorRadius,
        height = this.orbital.minorRadius,
        offset = this.orbital.focusOffset
    
    
    for(let i = 0, j = 0, step = pi2 / 32, max = pi2 + step; i <= max; i += step, j++) geom.vertices[j] = new THREE.Vector3(Math.cos(i) * width - offset, 0, Math.sin(i) * height).applyMatrix4(this.orbital.offsetMatrix)
    
    this.mesh.geometry.verticesNeedUpdate = true
  }
}



///init
let sun = new THREE.Mesh(flatGeom('icosahedron', 1, 1), new THREE.MeshStandardMaterial({ emissive: 0xFFFF60, emissiveIntensity: 1.2 })),
    pLight = new THREE.PointLight(0xffffDE, 1, 60),
    planet1 = new THREE.Mesh(flatGeom('icosahedron', .5, 1), new THREE.MeshStandardMaterial({ color: 0xFFFFFF })),
    orbiter = new Orbital,
    path = new OrbitalPath(orbiter),
    
    update = time => {
      orbiter.solveForMatrix(time).decompose(planet1.position, planet1.rotation, planet1.scale)
      path.update()
    }




sun.position.y = 5




orbiter.majorRadius = 7
orbiter.parentMass = 3
orbiter.eccentricity = 0.7
orbiter.periapsis = -0.5
orbiter.inclination = 0.25
orbiter.ascendingLongitude = 0.15
orbiter.phase = .5

update()
sun.add(pLight)
sun.add(path.mesh)
planet1.add(new THREE.AxisHelper())
sun.add(planet1)
scene.add(sun)



///loop
let now = 0,
    lastFrame = Date.now(),
    paused = true,
    opening = true,
    camRad = 50,
    camHeight = 4,
    camPhase = pi,
    destination = new THREE.Vector3(-15, 10, 15),
    alpha = 1,
    alpha2 = 1,
    tgt = new THREE.Vector3,
    pos = new THREE.Vector3,
    demo = false

;(function animate () {
  requestAnimationFrame(animate)
  let time = Date.now(),
      delta = time - lastFrame
  
  if(!opening) {
    if(demo) camera.lookAt(planet1.position)
    
    if(!paused) update(now += delta / 100)
  } else {
    camPhase += delta / 1000
    
    if(camPhase >= pi2) {
      if(alpha > 0 || alpha2 > 0) {
        alpha = Math.max(0, alpha - delta / 2000)
        alpha2 = Math.max(0, alpha2 -delta / 1000)
      } else opening = paused = false, demo = true
    }
    
    pos.x = cos(camPhase) * camRad
    pos.y = cos(camPhase + pi) * camHeight + camHeight + 1
    pos.z = sin(camPhase) * camRad
    
    camera.position.lerpVectors(destination, pos, alpha)
    camera.lookAt(tgt.lerpVectors(planet1.position, sun.position, alpha))
  }
  
  renderer.render(scene, camera)
  
  lastFrame = time
})()



///Interface
window.addEventListener('resize', (function size () {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  
  renderer.setSize(window.innerWidth, window.innerHeight)
  
  return size
})())



let toolbar = document.body.appendChild(document.createElement('section'))

toolbar.id = 'toolbar'



let buttons = toolbar.appendChild(document.createElement('nav')),
    pButton = buttons.appendChild(document.createElement('button')),
    sButton = buttons.appendChild(document.createElement('button')),
    dButton = buttons.appendChild(document.createElement('button')),
    
    toolbarOut = false,
    toolbarSwitch = buttons.appendChild(document.createElement('button'))

pButton.innerHTML = 'pause'
sButton.innerHTML = 'stop'
dButton.innerHTML = 'track'
toolbarSwitch.innerHTML = 'options'

toolbar.classList.add('collapsed')

pButton.addEventListener('click', _ => {
  paused = !paused
  
  if(paused) pButton.innerHTML = 'play'
  else pButton.innerHTML = 'pause'
})

sButton.addEventListener('click', _ => {
  paused = true
  update(now = 0)
  pButton.innerHTML = 'play'
})

dButton.addEventListener('click', _ => demo = !demo)

toolbarSwitch.addEventListener('click', _ => {
  toolbarOut = !toolbarOut
  
  if(toolbarOut) {
    toolbar.classList.remove('collapsed')
    toolbarSwitch.innerHTML = 'collapse'
  } else {
    toolbar.classList.add('collapsed')
    toolbarSwitch.innerHTML = 'options'
  }
})



let settings = {
      majorRadius: [1, 30],
      eccentricity: [0, .95],
    
    },
    defaults = {}

settings.phase = settings.ascendingLongitude = settings.inclination = settings.periapsis = [-1, 1]
settings.parentMass = [.1, 30]

for(let s in settings) {
  let originalValue = orbiter[s],
      
      elem = toolbar.appendChild(document.createElement('nav')),
      title = elem.appendChild(document.createElement('h2')),
      range = elem.appendChild(document.createElement('input')),
      number = elem.appendChild(document.createElement('input')),
      reset = elem.appendChild(document.createElement('button'))
  
  title.innerHTML = s
  
  range.type = 'range'
  number.type = 'number'
  range.min = number.min = settings[s][0]
  range.max = number.max = settings[s][1]
  range.step = number.step = .01
  range.value = number.value = orbiter[s]
  
  range.addEventListener('input', () => {
    number.value = orbiter[s] = parseFloat(range.value)
    update(now)
  })
  
  number.addEventListener('input', () => {
    range.value = orbiter[s] = parseFloat(number.value)
    update(now)
  })
  
  reset.innerHTML = 'R'
  defaults[s] = _ => {
    range.value = number.value = orbiter[s] = originalValue
  }
  
  reset.addEventListener('click', () => {
    defaults[s]()
    update(now)
  })
}



let reset = toolbar.appendChild(document.createElement('nav')).appendChild(document.createElement('button'))

reset.innerHTML = 'reset all'

reset.addEventListener('click', _ => {
  for(let s in defaults) defaults[s]()
  update(now)
})
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/three.js/84/three.js
  2. https://threejs.org/examples/js/controls/OrbitControls.js