Source: ui/widget/image-area.js

import { ManipulationHandles } from "./manipulation-handles.js";
import { BaseArea } from './base-area.js';

/**
 * An area somewhere in space, like a screen, displaying a texture.
 */
export class ImageArea extends BaseArea {
  /**
   * Creates the area with default values. 
   * By default, it's sized and positioned to be attached to the camera, and includes manipulation handles
   */
  constructor(scene, name="ImageArea-root") {
    super(scene, name);
    this.position = new BABYLON.Vector3(0, 0, .3);
    this.width = 2048;
    this.height = 1024;
    this.autoResize = true;
    this.visible = false;
    this.noiseTexture = null;
    this.callback = null; // CHECKME - onClick only used in test to start the video
    this.pointerIsDown = false;
  }

  /** Show the area, optionally also creates manipulation handles */  
  show () {
    if ( this.visible ) {
      return;
    }
    this.visible = true;
    this.group.billboardMode = this.billboardMode;
    this.group.position = this.position;
    this.ratio = this.width/this.height;

    this.material = new BABYLON.StandardMaterial("ImageMaterial", this.scene);
    this.material.emissiveColor = new BABYLON.Color3(0,1,0);
    this.material.disableLighting = true;
    this.material.backFaceCulling = false;
    this.noiseTexture = new BABYLON.NoiseProceduralTexture(this.name+"-perlin", 256, this.scene);
    this.material.diffuseTexture = this.noiseTexture;
    this.noiseTexture.octaves = 10;
    this.noiseTexture.persistence = 1.5;
    this.noiseTexture.animationSpeedFactor = 3;

    this.areaPlane = BABYLON.MeshBuilder.CreatePlane("ImageAreaPlane", {width:1,height:1}, this.scene);
    this.areaPlane.scaling.x = this.size*this.ratio;
    this.areaPlane.scaling.y = this.size;
    this.areaPlane.parent = this.group;
    this.areaPlane.material = this.material;
    this.areaPlane.visibility = 0.1;

    this.areaPlane.enablePointerMoveEvents = true;
    //this.areaPlane.pointerOverDisableMeshTesting = true; // no effect

    if (this.addHandles) {
      this.createHandles();
    }
    
    this.clickHandler = this.scene.onPointerObservable.add((pointerInfo) => {
      //console.log(pointerInfo.type+" "+pointerInfo.pickInfo.hit+" "+(this.areaPlane == pointerInfo.pickInfo.pickedMesh));
      if ( pointerInfo.type == BABYLON.PointerEventTypes.POINTERDOWN
        && pointerInfo.pickInfo.hit
        && this.areaPlane == pointerInfo.pickInfo.pickedMesh
      ) {
        //console.log("Clicked: x="+x+" y="+y+" coord "+pointerInfo.pickInfo.getTextureCoordinates() );
        let coords = pointerInfo.pickInfo.getTextureCoordinates();
        let y = Math.round(this.height*(1-coords.y));
        let x = Math.round(coords.x*this.width);
        this.click(x,y);
        this.pointerIsDown = true;
      } else if ( pointerInfo.type == BABYLON.PointerEventTypes.POINTERUP && this.pointerIsDown ) {
        this.pointerIsDown = false;
        this.pointerUp();
      } else if ( this.pointerIsDown 
        && pointerInfo.type == BABYLON.PointerEventTypes.POINTERMOVE 
        && pointerInfo.pickInfo.hit
        && this.areaPlane == pointerInfo.pickInfo.pickedMesh
      ) {
        let coords = pointerInfo.pickInfo.getTextureCoordinates();
        let y = Math.round(this.height*(1-coords.y));
        let x = Math.round(coords.x*this.width);
        this.pointerDrag(x,y);
      }
    });
    
  }
  
  /**
   * Internally used while replacing the texture
   * @private
   */
  texturesDispose() {
    if ( this.noiseTexture ) {
      this.noiseTexture.dispose();
      this.noiseTexture = null;
    }
    if ( this.texture ) {
      this.texture.dispose();
      this.texture = null;
    }
  }
  
  /**
   * Internally used after texture is set, sets emissiveColor and visibility
   */
  fullyVisible() {
    this.material.emissiveColor = new BABYLON.Color3(1,1,1);
    this.areaPlane.visibility = 1;
  }
  
  /**
   * Load the texture from the url
   */
  loadUrl(url) {
    let texture = new BABYLON.Texture(url, this.scene);
    this.texturesDispose();
    this.material.diffuseTexture = texture;
    this.texture = texture;
    this.fullyVisible();
    this.resizeArea(texture.getSize().width, texture.getSize().height);
  }

  /**
   * Load texture from the data buffer, e.g. blob
   */
  loadData(data, name="bufferedTexture") {
    console.log("Loading texture, size "+data.size);
    this.texturesDispose();
    let texture = BABYLON.Texture.LoadFromDataString(name,data,this.scene);
    this.material.diffuseTexture = texture;
    this.texture = texture;
    this.fullyVisible();
    this.resizeArea(texture.getSize().width, texture.getSize().height);
  }

  /** Load video texture from the url, and by default also creates and plays the spatial sound. */
  loadVideo(url, playSound=true) {
    let texture = new BABYLON.VideoTexture(null, url, this.scene);
    this.texturesDispose();
    this.material.diffuseTexture = texture;
    this.texture = texture;
    this.fullyVisible();
    console.log("Loaded video "+url+" playing sound: "+playSound);
    if ( playSound ) {
      this.sound = new BABYLON.Sound(
        "videoTextureSound",
        texture.video,
        this.scene, null, {
          //loop: true,
          autoplay: true,
          spatialSound: true,
          //streaming: false,
          distanceModel: "linear",
          maxDistance: 10,
          panningModel: "equalpower" // or "HRTF"
        });
      this.sound.attachToMesh(this.areaPlane);
      this.attachVolumeControl();
    }
    console.log(texture.video.videoWidth+"x"+texture.video.videoHeight);
    // resize the plane
    texture.video.onresize = (event) => {
      this.resizeArea(texture.video.videoWidth,texture.video.videoHeight);
    }
  }
  
  /**
   * Load a MediaStream, and resize the plane
   */
  loadStream(mediaStream) {
    console.log("Attaching media stream", mediaStream);
    BABYLON.VideoTexture.CreateFromStreamAsync(this.scene, mediaStream).then( (texture) => {
      this.texturesDispose();
      this.texture = texture;
      this.material.diffuseTexture = texture;
      this.material.diffuseTexture.vScale = -1
      this.fullyVisible();
    });

    let mediaTrackSettings = mediaStream.getVideoTracks()[0].getSettings();
    //console.log('Playing video track', mediaTrackSettings);
    /*
    // local:
    frameRate: 30
    height: 2160
    width: 3840
    // remote:
    aspectRatio: 1.7774436090225565
    deviceId: "96a38882-6269-454e-8009-3d3960b343ba"
    frameRate: 30
    height: 1330
    resizeMode: "none"
    width: 2364
    */
    // now resize the area
    this.resizeArea(mediaTrackSettings.width, mediaTrackSettings.height);
  }
  
  /** Internally used to resize the plane once video/image resolution is known */
  resizeArea(width, height) {
    if ( this.autoResize && width && height ) {
      //console.log("ImageArea resizing to "+width+"x"+height);
      this.width = width;
      this.height = height;
      this.ratio = this.width/this.height;
      this.areaPlane.scaling.x = this.size*this.ratio;
      this.areaPlane.scaling.y = this.size;
      if ( this.addHandles ) {
        if ( this.handles ) {
          this.handles.dispose();
        }
        this.createHandles();
      }
    }
  }
  
  /**
   * Creates manipulation handles. Left and right handle resize, and top and bottom move it.
   */
  createHandles() {
    this.handles = new ManipulationHandles(this.areaPlane, this.size*this.ratio, this.size, this.scene);
    this.handles.material = new BABYLON.StandardMaterial("TextAreaMaterial", this.scene);
    this.handles.material.alpha = 0.75;
    this.handles.material.diffuseColor = new BABYLON.Color3(.2,.2,.3);
    this.handles.canMinimize = this.canMinimize;
    this.handles.show();
    this.attachVolumeControl();
  }

  attachVolumeControl() {
    if ( this.handles && this.sound && !this.handles.onMinMax ) {
      this.handles.onMinMax = minimized => {
        console.log("Minimized: "+minimized);
        if ( minimized ) {
          this.sound.setVolume(0,1);
        } else {
          this.sound.setVolume(1,1);
        }
      }
    }
  }

  /** Called on pointer event, passed texture coordinates. Executes callback */
  async click(x,y) {
    if ( this.callback ) {
      this.callback(this,x,y);
    }
  }

  /** Called on pointer event */
  pointerUp() {
  }

  /** Called on pointer event, passed texture coordinates */  
  pointerDrag(x, y) {
  }
  
  /**
   * Set click event handler here
   * @param callback executed on pointer click, passed Control argument
   */
  onClick(callback) {
    this.callback = callback;   
  }

  /** Clean up. */
  dispose() {
    super.dispose();
    if ( this.clickHandler) {
      this.scene.onPointerObservable.remove(this.clickHandler);
    }
    if ( this.sound ) {
      this.sound.dispose();
    }
    this.texturesDispose();
  }

}