Source: ui/widget/mini-map.js

import { VRSPACEUI } from "../vrspace-ui.js";
import { World } from '../../world/world.js';

/**
 * MiniMap sets up a camera looking from above at current avatar position,
 * takes a screenshot every now and then, and displays it.
 */
export class MiniMap {
  constructor(scene) {
    /** Delay in ms, defines frame rate, defaults to 25fps (20ms)*/
    this.delay = 1000 / 25;
    /** Movement resolution to track, defaut 0.5 */
    this.resolution = 0.5;
    /** Height above the avatar use to capture the scene */
    this.height = 100;

    // internal constants
    this.scene = scene;
    this.engine = scene.getEngine();
    this.camera = new BABYLON.ArcRotateCamera("MiniMapCamera", -Math.PI / 2, 0, this.height, new BABYLON.Vector3(0, 0, 0), this.scene);

    // taking screenshot changes active camera, tell HUD to ignore the change    
    VRSPACEUI.hud.ignoreCamera = this.camera;

    this.surface = BABYLON.MeshBuilder.CreateDisc("MiniMap", { radius: .05 }, this.scene);
    this.surface.parent = VRSPACEUI.hud.root;
    this.surface.material = new BABYLON.StandardMaterial("screenshotMaterial");
    this.surface.material.disableLighting = true;

    this.center = BABYLON.MeshBuilder.CreateSphere("MiniMapCenter", { diameter: 0.002 }, this.scene);
    this.center.material = new BABYLON.StandardMaterial("MiniMapCenterMaterial");
    this.center.material.emissiveColor = BABYLON.Color3.Yellow();
    this.center.material.disableLighting = true;
    this.center.parent = this.surface;

    this.top = BABYLON.MeshBuilder.CreateSphere("MiniMapTop", { diameter: 0.002 }, this.scene);
    this.top.material = new BABYLON.StandardMaterial("MiniMapTopMaterial");
    this.top.material.emissiveColor = BABYLON.Color3.Yellow();
    this.top.material.disableLighting = true;
    this.top.parent = this.surface;
    this.top.position = new BABYLON.Vector3(0, 0.05, 0);

    // state variables
    this.timestamp = Date.now();
    this.capturing = false;
    this.lastPos = new BABYLON.Vector3(0, 0, 0);

    // handlers
    this.movementHandler = () => this.movement();
    this.scene.registerBeforeRender(this.movementHandler);

    this.windowResized = () => this.setPosition();
    window.addEventListener("resize", this.windowResized);

    this.setPosition();
  }

  setPosition() {
    this.surface.position = new BABYLON.Vector3(VRSPACEUI.hud.scaling() * .2 * this.engine.getAspectRatio(this.scene.activeCamera) - 0.05, -2 * VRSPACEUI.hud.vertical(), 0);
  }

  shot() {
    if (!this.capturing) {
      this.capturing = true;

      // top-down view bookkeeping:
      let pos = VRSPACEUI.hud.root.parent.position;
      if (this.scene.activeCamera.getClassName() == 'ArcRotateCamera' && World.lastInstance && World.lastInstance.camera1p) {
        pos = World.lastInstance.camera1p.position;
      }
      this.camera.position = new BABYLON.Vector3(pos.x, pos.y + this.height, pos.z);
      this.camera.target = new BABYLON.Vector3(pos.x, pos.y, pos.z);
      // mandatory, as it gets recalculated:
      this.camera.alpha = -Math.PI * .5;

      // same thing with both methods
      //BABYLON.Tools.CreateScreenshot(this.engine, this.camera, {precision: 1.0, height:1024, width:1024}, (data) => {
      BABYLON.Tools.CreateScreenshotUsingRenderTarget(this.engine, this.camera, { precision: 1.0, height: 1024, width: 1024 }, (data) => {
        const screenshotTexture = new BABYLON.Texture(data, this.engine, false, false);
        screenshotTexture.onLoadObservable.add((texture) => {
          this.surface.material.emissiveTexture = texture;
          this.capturing = false;
        });

      });
    }
  }

  hasMoved() {
    let pos;
    if (this.scene.activeCamera.getClassName() == 'UniversalCamera') {
      pos = this.scene.activeCamera.position;
    } else if (this.scene.activeCamera.getClassName() == 'WebXRCamera') {
      pos = this.scene.activeCamera.position;
    } else if (this.scene.activeCamera.getClassName() == 'ArcRotateCamera') {
      if (World.lastInstance && World.lastInstance.camera1p) {
        // relies on AvatarController keeping camera1p position in sync
        pos = World.lastInstance.camera1p.position;
      } else {
        // CHECKME fallback
        // return this.scene.activeCamera.hasMoved;
      }
    }
    let dist = this.lastPos.subtract(pos).length();
    if (dist > this.resolution) {
      this.lastPos = this.scene.activeCamera.position.clone();
      return true;
    }
  }

  movement() {
    if (this.timestamp + this.delay > Date.now()) {
      return;
    }
    this.timestamp = Date.now();
    if (this.scene.activeCamera.getClassName() == 'UniversalCamera') {
      //this.camera.alpha = -Math.PI/2-this.scene.activeCamera.rotation.y;
      this.surface.rotation.z = this.scene.activeCamera.rotation.y;
    } else if (this.scene.activeCamera.getClassName() == 'ArcRotateCamera') {
      //this.camera.alpha = this.scene.activeCamera.alpha;
      this.surface.rotation.z = 1.5 * Math.PI - this.scene.activeCamera.alpha;
    } else if (this.scene.activeCamera.getClassName() == 'WebXRCamera') {
      let rot = this.scene.activeCamera.rotationQuaternion.toEulerAngles();
      //this.camera.alpha = -Math.PI/2-rot.y;
      this.surface.rotation.z = rot.y;
    }
    if (this.hasMoved()) {
      this.shot();
    }
  }

  dispose() {
    this.scene.unregisterBeforeRender(this.movementHandler);
    this.surface.dispose();
    this.camera.dispose();
    window.removeEventListener("resize", this.windowResized);
  }
}