<form id="buttons">
  <fieldset>
    <legend>Focus and zoom to target</legend>
    <button type="button">Sphere 1</button>
    <button type="button">Sphere 2</button>
    <button type="button">Sphere 3</button>
    <button type="button">Sphere 4</button>
    <button type="button">Sphere 5 - Line Vibrates</button>
    <button type="button">Sphere 6 - Line Vibrates</button>
  </fieldset>
</form>
html, body {
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
  font-family: sans-serif;
}

body {
  position: relative;
}

#buttons {
  color: #FFF;
  position: absolute;
  top: 10px;
  left: 10px;
}
import * as THREE from 'https://cdn.skypack.dev/three@0.132.0/build/three.module.js'
import CameraControls from "https://cdn.skypack.dev/camera-controls@1.36.0";

const scene = new THREE.Scene();
const clock = new THREE.Clock();
const maxDistance = 100000000000;
const camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, maxDistance );

const renderer = new THREE.WebGLRenderer({
  logarithmicDepthBuffer: true,
  antialias: true
});
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const sphere = new THREE.Mesh(
  new THREE.SphereBufferGeometry( 100, 100, 100 ),
  new THREE.MeshBasicMaterial({
    color: new THREE.Color('white')
  })
);
scene.add( sphere );

camera.position.z = 1000;

CameraControls.install( { THREE: THREE } );
const controls = new CameraControls(camera, renderer.domElement);
controls.maxDistance = maxDistance;


// ///////////
// SKYBOX
// ///////////
const textureLoader = new THREE.TextureLoader(THREE.DefaultLoadingManager);
const spaceImages = {
  spaceFt: 'https://i.imgur.com/rU5MD9T.jpg',
  spaceBk: 'https://i.imgur.com/naP6ygJ.jpg',
  spaceUp: 'https://i.imgur.com/NpI2TqZ.jpg',
  spaceDn: 'https://i.imgur.com/gGpcZ0S.jpg',
  spaceRt: 'https://i.imgur.com/RAyAyiS.jpg',
  spaceLt: 'https://i.imgur.com/aprdPTY.jpg'
};
const skyboxTexturePaths = Object.values(spaceImages);

const skybox = (texturePaths) => {
  const skyboxMaterialArray = texturePaths.map(
    (image) => new THREE.MeshBasicMaterial({ map: textureLoader.load(image), side: THREE.BackSide })
  );
  const skybox = new THREE.Mesh(
    new THREE.BoxBufferGeometry(1000000000000, 1000000000000, 1000000000000),
    skyboxMaterialArray
  );
  skybox.name = 'skybox';

  return skybox;
};
scene.add(skybox(skyboxTexturePaths));

// ///////////
// COLOUR LERP
// ///////////
const amountOfOrbitToDraw = 330 // out of 360
const generateColors = (startColor, geometryCount) => {
  // how much fade we want, closer to 0 means fades earlier
  const lerpAcc = 1;
  const lerpIncrementer = 1 / amountOfOrbitToDraw / lerpAcc;

  const colors = new Float32Array(geometryCount * 3);
  for (let c = 0; c <= amountOfOrbitToDraw; c += 1) {
    const lerpColor = new THREE.Color(startColor);
    lerpColor.lerpColors(startColor, new THREE.Color('black'), c * lerpIncrementer);

    colors[c * 3 + 0] = lerpColor.r;
    colors[c * 3 + 1] = lerpColor.g;
    colors[c * 3 + 2] = lerpColor.b;
  }

  return colors;
}

// ///////////
// ORBIT LINES
// ///////////
const createOrbitLine = (radius, color) => {
  const points = [];
  for (let i = 0; i <= amountOfOrbitToDraw; i++) {
    const v = new THREE.Vector3(Math.sin(THREE.MathUtils.degToRad(i))*radius, 0, Math.cos(THREE.MathUtils.degToRad(i))*radius);
    points.push(v);
  }

  const geometryLine = new THREE.BufferGeometry().setFromPoints(points);  
  geometryLine.setAttribute('color', new THREE.BufferAttribute(generateColors(new THREE.Color('white'), geometryLine.getAttribute('position').count), 3));
  const material = new THREE.LineBasicMaterial({
    transparent: true,
    blending: THREE.AdditiveBlending,
    vertexColors: true
  });

  const line = new THREE.Line(geometryLine, material)
  return line;
}

const orbitLines = [
  {
    color: 'red',
    radius: 10000
  },
  {
    color: 'orange',
    radius: 1000000
  },
  {
    color: 'yellow',
    radius: 10000000
  },
  {
    color: 'green',
    radius: 100000000
  },
  {
    color: 'blue',
    radius: 1000000000
  },
  {
    color: 'purple',
    radius: 10000000000
  }
]

// ///////////
// ORBIT SPHERES
// ///////////
const orbitSpheres = [];
const orbitSphereSize = 1000;
orbitLines.forEach((orbitLine, i) => {
  scene.add(createOrbitLine(orbitLine.radius, new THREE.Color(orbitLine.color)));

  const orbitSphere = new THREE.Mesh(
    new THREE.SphereBufferGeometry( orbitSphereSize, orbitSphereSize, 100 ),
    new THREE.MeshBasicMaterial({
      color: new THREE.Color(orbitLine.color),
      transparent: true,
      opacity: 0.8
    })
  );
  orbitSphere.position.set(0, 0, orbitLine.radius);
  scene.add(orbitSphere);
  orbitSpheres.push(orbitSphere);
});

const focusOnTarget = (target) => {
  controls.setLookAt(
    target.position.x + (orbitSphereSize * 2), target.position.y, target.position.z + (orbitSphereSize * 2),
    target.position.x, target.position.y, target.position.z,
    true
  )
}

// ///////////
// RENDER LOOP
// ///////////
(function animate() {
  const delta = clock.getDelta();
  const elapsed = clock.getElapsedTime();
  const updated = controls.update( delta );

  requestAnimationFrame( animate );

  if (updated) {
    renderer.render( scene, camera );
  }
})();

[...document.querySelectorAll('#buttons button')].forEach((button, i) => {
  button.addEventListener('click', () => {
    focusOnTarget(orbitSpheres[i]);
  })
});

document.addEventListener('DOMContentLoaded', () => {
  // this seems to be needed, or else the initial load renders a black screen
  renderer.render( scene, camera );
});
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.