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)
})
This Pen doesn't use any external CSS resources.