pxlNav/core/Utils.js


/**
 * @namespace pxlUtils
 * @description Utility functions
 */

import {
  Vector2,
  Vector3,
  ImageLoader,
  Texture,
  VideoTexture,
  CanvasTexture,
  LinearFilter,
  AlphaFormat,
  RedFormat,
  RGBAFormat,
  RGFormat,
  RGBFormat,
  LuminanceFormat,
  DepthFormat,
  MeshBasicMaterial
} from "../../libs/three/three.module.min.js";
/*LinearSRGBColorSpace,
SRGBColorSpace,
CubeUVRefractionMapping,*/


export class Utils{
  constructor(assetRoot="images/assets/", mobile ){
    this.assetRoot=assetRoot;
    this.mobile=mobile;
    this.pxlTimer=null;
    this.verboseLoading=false;
    this.texLoader=new ImageLoader();
    this.textLoaderArray=[];
    // Texture formats, use as needed
    // ImageLoader's defauls; images load as RGBAFormat by default, and JPG as RGBAFormat
    this.channelFormats=[ AlphaFormat, RedFormat, RGFormat, RGBFormat, RGBAFormat, LuminanceFormat, DepthFormat ];
  }

  get curMS(){
      return this.pxlTimer.curMS;
  }

  setDependencies( pxlNav ){
    this.pxlTimer=pxlNav.pxlTimer;
  }
  
  updateUrl(url,state={},title=""){
      if (window.history.replaceState) {
          window.history.replaceState(state, title, url);
      }else{
          window.history.pushState(state, title, url);
      }
  }
  
  copyRoomUrl(){
      let url=window.location;
      let tmpFocus=document.activeElement;
      let tmpText = document.createElement("textarea");
      tmpText.value = url;

      document.body.appendChild(tmpText);
      tmpText.focus();
      tmpText.select();

      let status=false;
      try{
          let callback = document.execCommand('copy');
          status = callback ? 'successful' : 'unsuccessful';
      }catch(err){}

      document.body.removeChild(tmpText);
      tmpFocus.focus();  
      
      return status;
  }
    
  checkInt(val){
    return (val%1)==0;
  }

  degToRad(deg){
    return deg*(Math.PI/180);
  }

  toHundreths(val){ // int(val*100)*.01 returns an erronious float on semi ussual basis...
    if(!val) return 0;

    if( Number.isInteger(val) ){
      return val;
    }else{
      let sp=(val+"").split(".");
      let ret=parseFloat(sp[0]+"."+sp[1].substr(0,2));
      return ret;
    }
  }

  toTenths(val){ // int(val*100)*.01 returns an erronious float on semi ussual basis...
    if(!val) return 0;

    if( Number.isInteger(val) ){
      return val;
    }else{
      let sp=(val+"").split(".");
      let ret=parseFloat(sp[0]+"."+sp[1].substr(0,1));
      return ret;
    }
  }
  
  getDateTime(){
    let d=new Date();
    let date=(d.getFullYear()+"").padStart(2,'0')+"-"+((d.getMonth()+1)+"").padStart(2,'0')+"-"+(d.getDate()+"").padStart(2,'0');
    let time=(d.getHours()+"").padStart(2,'0')+":"+(d.getMinutes()+"").padStart(2,'0')+":"+(d.getSeconds()+"").padStart(2,'0');
    return [date, time];
  }
  
  // -- -- -- //

  cleanStrict( messageString ){
    let strip=document.createElement( "div" );
    strip.innerHTML=messageString;
    strip=strip.innerText;
    let matcher=strip.match(/([a-zA-Z0-9])\w+/g);

    if(matcher){
      strip=matcher.join(" ");
    }
    return strip;
  }
    
  cleanBasic( messageString ){
    let strip=document.createElement( "div" );
    strip.innerHTML=messageString;
    strip=strip.innerText;
    let matcher=strip.match(/([a-zA-Z0-9\s\w-+()\[\]])+/g);

    if(matcher){
      strip=matcher.join("");
    }
    return strip;
  }
    
  cleanString( messageString ){
    let strip=document.createElement( "div" );
    strip.innerHTML=messageString;
    strip=strip.innerText;
    return strip;
  }


  // Round to nearest
  toNearestStr( val, precision=2 ){
    let retVal = val.toFixed(precision);
    return retVal;
  }

  // Round array elements to nearest
  arrayToStr( arr, precision=2 ){
    let retArr = [];
    arr.forEach( (val)=>{
      retArr.push( this.toNearestStr(val, precision) );
    });
    return retArr;
  }

  // Flatten array to joined string
  flattenArrayToStr( arr, precision=2, delimiter="," ){
    return this.arrayToStr( arr, precision ).join(delimiter);
  }

  // -- -- -- //
  
  randomFloat(min,max){
    return Math.random()*(max-min)+min;
  }

  // -- -- -- //

  screenToNDC( x, y, width, height ){
    let ndcX = ( x / width ) * 2 - 1;
    let ndcY = - ( y / height ) * 2 + 1;
    return new Vector2( ndcX, ndcY );
  }
  
  // -- -- -- //

  componentToHex(c) {
    var hex = c.toString(16);
    return hex.padStart(2,'0');
  }
  rgbToHex(r, g, b) {
    return "#" + this.componentToHex(Math.min(255, Math.max(0,Math.round(r)))) + this.componentToHex(Math.min(255, Math.max(0,Math.round(g)))) + this.componentToHex(Math.min(255, Math.max(0,Math.round(b))));
  }
  
  hexToRgb( hex ) {
    let buffer=hex[0];
    if(buffer=="#"){
      hex=hex.substr( 1, 6 );
    }else{
      hex=hex.substr( 0, 6 );
    }
    let r,g,b;
    if(hex.length==3){
      r=hex[0]+hex[0];
      g=hex[1]+hex[1];
      b=hex[2]+hex[2];
    }else{
      r=hex[0]+hex[1];
      g=hex[2]+hex[3];
      b=hex[4]+hex[5];
    }
    r=parseInt(r,16);
    g=parseInt(g,16);
    b=parseInt(b,16);
    return [r,g,b];
  }
  
  // -- -- -- //

  stringToRgb( string, boost=null, zoFit=false ){
    let stringColor=[255,0,0];
    if( string ){
      let sLength=string.length;
      let charCode="";
      for(let x=0; x<sLength; ++x){
        charCode+=string[ (sLength-1-x) ].charCodeAt(0).toString(16);
      }

      let ccLength=charCode.length;
      if(ccLength>6){
        let offset=1;
        if(string=="tussin"){
          offset=0;
        }else if(string=="fexofenadine"){
          offset=-1;
        }
        let reader=Math.max(0,parseInt((ccLength-6)/2+offset));
        charCode=charCode.substr(reader,6);
      }
      
      stringColor=this.hexToRgb( charCode );
    }
    
    if( boost != null){
      let maxCd=Math.max(...stringColor);
      let minCd=Math.min(...stringColor);
      let boostCd=maxCd*boost;
      stringColor[0]= parseInt( Math.min(255, ((stringColor[0]-minCd) / (maxCd-minCd))*255+boostCd) );
      stringColor[1]= parseInt( Math.min(255, ((stringColor[1]-minCd) / (maxCd-minCd))*255+boostCd) );
      stringColor[2]= parseInt( Math.min(255, ((stringColor[2]-minCd) / (maxCd-minCd))*255+boostCd) );
      /*
      stringColor[0]= Math.max(0, Math.min(255, (stringColor[0]-100)*boost+100));
      stringColor[1]= Math.max(0, Math.min(255, (stringColor[1]-100)*boost+100));
      stringColor[2]= Math.max(0, Math.min(255, (stringColor[2]-100)*boost+100));
      */
    }
    
    if( zoFit==true ){
      stringColor[0]=stringColor[0]/255;
      stringColor[1]=stringColor[1]/255;
      stringColor[2]=stringColor[2]/255;
    }
    
    return stringColor;
  }
    
  // -- -- -- //


  // Convert Color/Vector3 to sRGB Color Space
  colorTosRGB( color ){
    // Check if the colorue is a color object
    if( typeof color == "object" ){
      // Color Object
      if( color.hasOwnProperty && color.hasOwnProperty("r") ){
        color.r = this.tosRGB(color.r);
        color.g = this.tosRGB(color.g);
        color.b = this.tosRGB(color.b);
      }
      if( color.hasOwnProperty && color.hasOwnProperty("x") ){
        color.x = this.tosRGB(color.x);
        color.y = this.tosRGB(color.y);
        color.z = this.tosRGB(color.z);
      }
      return color;
    }
    return color;
  }

  // Convert Linear to sRGB
  tosRGB( val ){
    // Convert the value per channel
    if( val <= 0.0031308 ){
      val *= 12.92;
    }else{
      val = 1.055 * Math.pow(val, this.oneTwoPFour) - 0.055;
    }
    return val;
  }

  // -- -- --

  // Convert Color/Vector3 to Linear Color Space
  colorToLinear( color ){
    // Check if the colorue is a color object
    if( typeof color == "object" ){
      // Color Object
      if( color.hasOwnProperty && color.hasOwnProperty("r") ){
        color.r = this.toLinear(color.r);
        color.g = this.toLinear(color.g);
        color.b = this.toLinear(color.b);
      }
      if( color.hasOwnProperty && color.hasOwnProperty("x") ){
        color.x = this.toLinear(color.x);
        color.y = this.toLinear(color.y);
        color.z = this.toLinear(color.z);
      }
      return color;
    }
    return color;
  }
  // Convert sRGB to Linear
  toLinear( val ){
    if( val <= 0.04045 ){
      val *= this.twelvePNineTwoDiv;
    }else{
      val = Math.pow((val + 0.055) * this.onePOFiveFiveDiv, 2.4);
    }
    return val;
  }


  // -- -- --

  gammaCorrectColor( color, gammaIn="2.2", gammaOut="1.8" ){
    // Check if the colorue is a color object
    if( typeof color == "object" ){
      // Color Object
      if( color.hasOwnProperty && color.hasOwnProperty("r") ){
        color.r = this.gammaCorrect(color.r, gammaIn, gammaOut);
        color.g = this.gammaCorrect(color.g, gammaIn, gammaOut);
        color.b = this.gammaCorrect(color.b, gammaIn, gammaOut);
      }
      if( color.hasOwnProperty && color.hasOwnProperty("x") ){
        color.x = this.gammaCorrect(color.x, gammaIn, gammaOut);
        color.y = this.gammaCorrect(color.y, gammaIn, gammaOut);
        color.z = this.gammaCorrect(color.z, gammaIn, gammaOut);
      }
      return color;
    }
    return color;
  }

  gammaCorrection( channel, gammaIn="2.2", gammaOut="1.8" ){
    // Linearize the color
    let linearChannel = Math.pow( channel, gammaIn );

    // Apply the gamma correction
    let channelShift = Math.pow( linearChannel, 1.0 / gammaOut );

    return channelShift;
  }

  // -- -- --



  // TODO : Prep & re-implement THREE.GammaFactor -> pxlNav.pxlDevice.GammaFactor
  // TODO : pxlDevice OS detect needs to be implement for color conversion between known OS color spaces
  convertColor( color, space=COLOR_SHIFT.KEEP ){
    if( space == COLOR_SHIFT.KEEP ){
      return color;
    }

    // Notes on OS Gamma levels --
    //   Linear Gamma is 1.0
    //   Windows default Gamma is 2.2
    //   Mac, Linux, & Androids default Gamma is 1.8
    //   Other Unix systems are 2.3-2.5

    // Notes on Color Spaces --
    //   sRGB & Rec.709 are the same
    //     sRGB is more in line with how the human eye percieves color
    //   Linear is an uncorrected color space, not data is lost between programs or devices
    //     Linear Colors should then be converted to sRGB for display,
    //       But not converted if the color is for Data needs

    let retColor = color.clone();
    switch (space) {
      // Assuming color adjustments have been made with shader math --
      case COLOR_SHIFT.sRGB_TO_LINEAR:
        retColor = this.colorTosRGB(retColor);
        break;
      case COLOR_SHIFT.LINEAR_TO_sRGB:
        retColor = this.colorToLinear(retColor);
        break;

      // TODO : These need to be checked, added for completeness, not tested
      case COLOR_SHIFT.WINDOWS_TO_UNIX:
        retColor = this.gammaCorrectColor(retColor, "2.2", "1.8");
        break;
      case COLOR_SHIFT.UNIX_TO_WINDOWS:
        retColor = this.gammaCorrectColor(retColor, "1.8", "2.2");
        break;
      case COLOR_SHIFT.LINEAR_TO_WINDOWS:
        retColor = this.gammaCorrectColor(retColor, "1.0", "2.2");
        break;
      case COLOR_SHIFT.WINDOWS_TO_LINEAR:
        retColor = this.gammaCorrectColor(retColor, "2.2", "1.0");
        break;
      case COLOR_SHIFT.LINEAR_TO_UNIX:
        retColor = this.gammaCorrectColor(retColor, "1.0", "1.8");
        break;
      case COLOR_SHIFT.UNIX_TO_LINEAR:
        retColor = this.gammaCorrectColor(retColor, "1.8", "1.0");
        break;
      default:
        break;
    }

    return retColor;
  }


  // -- -- -- //

  randomizeArray(inputArr){
    let tmpArr=[...inputArr];
    let retArr=[];
    while( tmpArr.length > 0){
      let rand=tmpArr.length==1 ? 0 : parseInt(Math.random()*21*tmpArr.length)%tmpArr.length;
      retArr.push( tmpArr.splice( rand, 1 )[0] );
    }
    return retArr;
  }
    
  getRandom( list, seed=1.14 ){
    let randEl= Math.floor( Math.random( seed ) * list.length);
    return list[ randEl ];
  }
    
  applyTransformList(curObj,transList){
    var rotate=transList["r"];
    curObj.rotateX(rotate[0]);
    curObj.rotateY(rotate[1]);
    curObj.rotateZ(rotate[2]);
    if(typeof(transList["rOrder"]) !== "undefined" ){
      curObj.rotation.order=transList["rOrder"];
    }
    var pos=transList["t"];
    curObj.position.set(pos[0],pos[1],pos[2]);
    var scale=transList["s"];
    curObj.scale.set(scale[0],scale[1],scale[2]);
    
    curObj.matrixAutoUpdate=false;
    curObj.updateMatrix();
  }
  
    vec2(x=null,y=null){
        return new Vector2(x,y);
    }
    vec3(x=0,y=0,z=0){
        return new Vector3(x,y,z);
    }
    
  //this.channelFormats=[ AlphaFormat, RedFormat, RGFormat, RGBFormat, RGBAFormat, LuminanceFormat, DepthFormat ];
  loadTexture(imgPath,channels=null,mods={}){
    // ## Check how textLoaderArray textures are being handled after being disposed

    // No path, default to the asset root
    if( !imgPath.includes( "/") ){
      imgPath = this.assetRoot + imgPath;
    }

    if(typeof(this.textLoaderArray[imgPath]) != "undefined"){
      texture=this.textLoaderArray[imgPath];
    }else{
      //var texLoader=new ImageLoader(verboseLoading);
      var texture=new Texture();
      let modKeys = Object.keys(mods);
      this.texLoader.load(imgPath,
        (tex)=>{
          if(channels!=null){
            texture.format = this.channelFormats[ channels ];
          }
          texture.image=tex;
          texture.needsUpdate=true;

          // Apply texture mods passed from the user
          if(modKeys.length>0){
            modKeys.forEach((x)=>{
              texture[x]=mods[x];
            });
          }
        },
        undefined,
        (err)=>{
          console.error(" TextureLoader :: Error loading texture :: "+imgPath);
          console.error(err);
        }
      );
      this.textLoaderArray[imgPath]=texture;
    }
    return texture;
  }

  getVideoTexture( videoObject ){
    let videoTexture=new VideoTexture(videoObject);
    videoTexture.minFilter=LinearFilter;
    videoTexture.magFilter=LinearFilter; // faster, lower samples, NearestFilter
    videoTexture.format=RGBFormat;
    
    return videoTexture;
  }
  
  getCanvasTexture( canvas ){
    const texture = new CanvasTexture(canvas);
      
    const material = new MeshBasicMaterial({
      map: texture,
    });
    return {texture, material};
  }
}