<script src="https://typpo.github.io/spacekit/build/spacekit.js"></script>
<script src="https://threejs.org/build/three.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

<!-- ... -->

<div 
  id='canvas_cont'>
  <!-- VISUAL GLOBE 3D - CANVAS -->
  <canvas 
    id='rotatingGlobe'>
  </canvas>
</div>
  // PLANET VISUAL & INTERACTION

const planet_markers = [
  {
      // ... Marker_1
      id: '1',
      coord: [ 28.524167, -80.650833 ],
      info: {
          icon_img: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Kennedy_Space_Center_composite_photograph.jpg/450px-Kennedy_Space_Center_composite_photograph.jpg',
          description: `Hello!`
      }
  }
  ,
  {
    // ... Marker_1
    id: '2',
    coord: [ 58.524167, -80.650833 ],
    info: {
        icon_img: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Kennedy_Space_Center_composite_photograph.jpg/450px-Kennedy_Space_Center_composite_photograph.jpg',
        description: `Bye!`
    }
}
]

const renderCompleteGlobe = () => {

    function _convertLatLonToVec3(lat, lon) {
      lat = lat * Math.PI / 180.0;
      lon = -lon * Math.PI / 180.0;
      return new THREE.Vector3(
        Math.cos(lat) * Math.cos(lon),
        Math.sin(lat),
        Math.cos(lat) * Math.sin(lon));
    }

    // --------------------------------
    // 
    // --------------------------------

    function LabelLine(marker, radius, domElement) {
      this._screenVector = new THREE.Vector3(0, 0, 0);
      this.position = _convertLatLonToVec3(marker.coord[0], marker.coord[1]).multiplyScalar(radius);
      // create html overlay box
      this.box = document.createElement('div');
      this.box.innerHTML = marker.name;
      this.box.className = "hudLabel";
      // ...
      this.domElement = domElement;
      this.domElement.appendChild(this.box);
    }
    LabelLine.prototype.update = function () {
      this._screenVector.copy(this.position);
      this._screenVector.project(camera);
      // ...
      var posx = Math.round((this._screenVector.x + 1) * this.domElement.offsetWidth / 2);
      var posy = Math.round((1 - this._screenVector.y) * this.domElement.offsetHeight / 2);
      // ...
      var boundingRect = this.box.getBoundingClientRect();
      // ...
      // update the box overlays position
      this.box.style.left = (posx - boundingRect.width) + 'px';
      this.box.style.top = posy + 'px';
    };

    // --------------------------------
    // ✅ Working - 3D Stand-alone Object
    // --------------------------------

    function InfoBox(marker, radius, domElement) {
      this._screenVector = new THREE.Vector3(0, 0, 0);
      this.position = _convertLatLonToVec3(marker.coord[0], marker.coord[1]).multiplyScalar(radius);
      // create html overlay box
      this.box = document.createElement('div');
      this.box.innerHTML = marker.name;
      this.box.className = "hudLabel";
      // ...
      this.domElement = domElement;
      this.domElement.appendChild(this.box);
    }
    InfoBox.prototype.update = function () {
      this._screenVector.copy(this.position);
      this._screenVector.project(camera);
      // ...
      var posx = Math.round((this._screenVector.x + 1) * this.domElement.offsetWidth / 2);
      var posy = Math.round((1 - this._screenVector.y) * this.domElement.offsetHeight / 2);
      // ...
      var boundingRect = this.box.getBoundingClientRect();
      // ...
      // update the box overlays position
      this.box.style.left = (posx - boundingRect.width) + 'px';
      this.box.style.top = posy + 'px';
    };

    // --------------------------------
    // ❌ Not Working
    // --------------------------------
    
    function Marker() {
      var radius = 0.005;
      var sphereRadius = 0.02;
      var height = 0.05;
      var material = new THREE.MeshPhongMaterial({
        color: 0xDC143C
      });
      var cone = new THREE.Mesh(new THREE.ConeBufferGeometry(radius, height, 8, 1, true), material);
      cone.position.y = height * 0.5;
      cone.rotation.x = Math.PI;
      var sphere = new THREE.Mesh(new THREE.SphereBufferGeometry(sphereRadius, 16, 8), material);
      sphere.position.y = height * 0.95 + sphereRadius;
      this.add(cone, sphere);
    }
    Marker.prototype = Object.create(new THREE.Object3D());

    // --------------------------------
    // ------ Titan object ------------
    // --------------------------------

    function Titan(radius) {
      this.userData.radius = radius;
      var titan = new THREE.Mesh(
        new THREE.SphereBufferGeometry(radius, 64.0, 48.0),
        new THREE.MeshPhongMaterial({
            color: 0x0eeeee,
          })
      );
      this.add(titan);
    }
    var markerarry = [];
    Titan.prototype = Object.create(new THREE.Object3D());
    // ...
    Titan.prototype.createMarker = function (lat, lon) {
      var marker = new Marker();
      var latRad = lat * (Math.PI / 180);
      var lonRad = -lon * (Math.PI / 180);
      var r = this.userData.radius;
      marker.position.set(Math.cos(latRad) * Math.cos(lonRad) * r, Math.sin(latRad) * r, Math.cos(latRad) * Math.sin(
        lonRad) * r);
      marker.rotation.set(0.0, -lonRad, latRad - Math.PI * 0.5);
      this.add(marker);
    };

    // --------------------------------
    // ------ Three.js code -----------
    // --------------------------------

    var scene, camera, renderer, label;
    var controls;
    var container
    // ...
    var textlabels = []

    init();

    // [WORKING]
    function init() {
      // ...
      scene = new THREE.Scene();
      // ...
      camera = new THREE.PerspectiveCamera(45, 4 / 3, 0.1, 100);
      camera.position.set(0.0, 1.5, 3.0);
      var canvReference = document.getElementById('rotatingGlobe');
      container = document.getElementById('canvas_cont');
      // ...
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        canvas: canvReference
      });
      // ...
      controls = new THREE.OrbitControls(camera, renderer.domElement);
      controls.autoRotate = true;
      controls.autoRotateSpeed = -7.0;
      controls.enablePan = false;
      // ...
      controls.enableDamping = true;
      controls.dampingFactor = 0.25;
      controls.enableZoom = true;
      controls.enableKeys = false;
      // ...
      var ambient = new THREE.AmbientLight(0xffffff, 0.5);
      scene.add(ambient);
      // ...
      var direcitonal = new THREE.DirectionalLight(0xffffff, 0.5);
      direcitonal.position.set(5.0, 2.0, 5.0).normalize();
      scene.add(direcitonal);
      // ...
      var titan = new Titan(0.5);
      // ... [❌]
      // titan.createMarker(48.856700, 2.350800);    // Paris
      // titan.createMarker(51.507222, -0.1275);     // London
      // titan.createMarker(34.050000, -118.250000); // LA
      // ...
      scene.add(titan);

      // ...
      // ... PROPER-MARKER GENERATION
      var radius1 = 0.5;
      var count = 0
      // ...
      planet_markers.forEach(marker => {
        // ...
        var label = new InfoBox(marker, radius1, canvReference);
        //  ....
        var material = new THREE.MeshPhongMaterial({
          color: 0xffffff
        });
        // ...
        var _marker = new THREE.Mesh(new THREE.SphereGeometry(0.05, 16, 16), material);
        _marker.userData = marker.info
        // ...
        _marker.position.copy(label.position);
        scene.add(_marker);
        markerarry.push(_marker)
        // ... GENERATE LABELS;
        count++
        var text = _createMarkerTextLabel();
        text.setHTML("Label " + count);
        text.setParent(_marker);
        textlabels.push(text);
        container.appendChild(text.element);
      });
      
      // --------------------------------
      window.onmousedown = function (event) {
        // the following line would stop any other event handler from firing
        // (such as the mouse's TrackballControls)
        // event.preventDefault();
        // ...
        const rect = renderer.domElement.getBoundingClientRect();
        const left = event.clientX - rect.left;
        const top = event.clientY - rect.top;
        // ...
        const x = (left / rect.width) * 2 - 1;
        const y = -(top / rect.height) * 2 + 1;
        // ...
        const raycaster = new THREE.Raycaster();
        raycaster.ray.origin.setFromMatrixPosition(camera.matrixWorld);
        raycaster.ray.direction.set(x, y, 0.5).unproject(camera).sub(raycaster.ray.origin).normalize();
        // ...
        const intersects = raycaster.intersectObjects(markerarry, true);
        intersects.forEach(sel_marker => {
          // ...
          onSelectedMarker(sel_marker)
        });
      }
      // --------------------------------

      window.addEventListener('resize', onResize);
      onResize();
      animate();
      controls.addEventListener('change', changeLinePosition);
    }

    // ... [WORKING]
    var oldLine = undefined;
    var oldMarker = undefined;
    var oldExtraInfo = undefined;
    var materialLine;
    var points = [];
    // ... OUTWARD-LINE GENERATION;
    function onSelectedMarker(marker) {
      // ... remove previous line;
      if (oldLine) scene.remove(oldLine)
      if (oldMarker) oldMarker.object.material.color.set(0x00AADF);
      if (oldExtraInfo) container.removeChild(oldExtraInfo.element);
      // ... extract-marker-position;
      let sel_mark_point = marker.object.position
      // if (dev) console.debug('_point', sel_mark_point)
      // ...
      marker.object.material.color.set(0xff0000);
      // ...
      let _label_point_1 = _convertLatLonToVec3(90, -100).multiplyScalar(1)
      // let _label_point_2 = _convertLatLonToVec3(120, -100).multiplyScalar(1)
      // if (dev) console.debug('_label_point_1', _label_point_1)
      points = [ sel_mark_point, _label_point_1 ]; // ... instances of Vector3
      const geometry = new THREE.BufferGeometry().setFromPoints( points );
      materialLine = new THREE.LineBasicMaterial({
        color: 0xff0000,
        linewidth: 100
        });
      const line = new THREE.Line(geometry, materialLine);
      oldLine = line
      oldMarker = marker
      // ...
      scene.add(line);
      // ... GENERATE POPUP;
      var text = _createMarkerInfoBoxText();
      text.setHTML(marker.object.userData);
      text.setParent(scene);
      container.appendChild(text.element);
      oldExtraInfo = text
    }

    function changeLinePosition() {
      // ...
      if (oldLine != undefined) {
        // console.debug('camera.position', camera.position, 'camera.rotation', camera.rotation)
        // console.debug('oldLine', oldLine)
        // oldLine.position.copy( camera.position );
        // oldLine.rotation.copy( camera.rotation );
        // oldLine.updateMatrix();
        // oldLine.translateZ( - 10 );

        // oldLine.scale.set(1, 2, 1);
        oldLine.geometry.verticesNeedUpdate = true;
      }
    }

    // [WORKING]
    function onResize() {
      camera.aspect = (window.innerWidth / 2) / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth / 2, window.innerHeight);
    }

    // [WORKING]
    function animate() {
      //  label.update();
      requestAnimationFrame(animate);
      controls.update();
      // ... update-text-label-for-point;
      for (var i = 0; i < textlabels.length; i++) {
        textlabels[i].updatePosition();
      }
      // ... update-position-end-of-line-coordinate;
      if (oldLine != undefined && oldExtraInfo != undefined) {
        oldExtraInfo.updatePosition();
        updatePositionOfLine(oldLine, oldExtraInfo)
      }
      // ...
      renderer.render(scene, camera);
    }

    // [WORKING]
    function _createMarkerTextLabel() {
      // ...
      var div = document.createElement('div');
      div.className = 'text-label';
      div.style.position = 'absolute';
      div.style.width = '100';
      div.style.height = '100';
      div.innerHTML = "hi there!";
      div.style.top = '-1000';
      div.style.left = '-1000';
      div.style.background = '#000000';
      div.style.padding = '5px';
      div.style.color = 'white';
      div.style.borderRadius = '5px'
      // ...
      return {
        element: div,
        parent: false,
        position: new THREE.Vector3(0, 0, 0),
        setHTML: function (html) {
          this.element.innerHTML = html;
        },
        setParent: function (threejsobj) {
          this.parent = threejsobj;
        },
        updatePosition: function () {
          if (parent) {
            this.position.copy(this.parent.position);
          }
          // ...
          var coords2d = this.get2DCoords(this.position, camera);
          this.element.style.left = coords2d.x + 'px';
          this.element.style.top = coords2d.y + 'px';
        },
        get2DCoords: function (position, camera) {
          var vector = position.project(camera);
          vector.x = (vector.x + 1) / 2 * (window.innerWidth / 2);
          vector.y = -(vector.y - 1) / 2 * window.innerHeight;
          return vector;
        }
      };
    }

    // [WORKING]
    function _createMarkerInfoBoxText() {
      // ...
      var div = document.createElement('div');
      div.className = 'text-label';
      div.style.position = 'absolute';
      div.style.width = '100';
      div.style.height = '100';
      div.innerHTML = "hi there!";
      div.style.top = '-1000';
      div.style.left = '-1000';
      div.style.background = '#000000';
      div.style.padding = '5px';
      div.style.color = 'white';
      div.style.borderRadius = '5px';
      div.style.top = '80px';
      div.style.right = '30px'
      // ...
      return {
        element: div,
        parent: false,
        position: new THREE.Vector3(0, 0, 0),
        setHTML: function (html) {
          this.element.innerHTML = `
            <div>
              <p>
                ${html.description}
              </p>
            </div>
          `;
        },
        setParent: function (threejsobj) {
          this.parent = threejsobj;
        },
        updatePosition: function () {
          if (parent) {
            this.position.copy(this.parent.position);
          }
          // ...
          var coords2d = this.get2DCoords(this.position, camera);
          this.element.style.left = coords2d.x + 'px';
          this.element.style.top = coords2d.y + 'px';
        },
        get2DCoords: function (position, camera) {
          var vector = position.project(camera);
          vector.x = (vector.x + 1) / 2 * (window.innerWidth / 2);
          vector.y = -(vector.y - 1) / 2 * window.innerHeight;
          return vector;
        }
      };
    }

    // [WORKING]
    function updatePositionOfLine(line, info_container) {
      // ...
      var coord2d = info_container.get2DCoords(info_container.position, camera)
      console.debug('coord2d', coord2d)
      // console.debug('info_container', info_container)
      // ... remove previous line;
      scene.remove(oldLine)
      // ... construct-new-line;
      // points[1] = _convertLatLonToVec3(coord2d.y, coord2d.x)
      points[1] = info_container.position
      const geometry = new THREE.BufferGeometry().setFromPoints( points );
      const newLine = new THREE.Line(geometry, materialLine);
      // ... add-in-new-line;
      oldLine = newLine
      scene.add(newLine)
      // line.position.copy(info_container.position)
    }
  }

renderCompleteGlobe()
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.