Source: Blend.js

import Touch from './helpers/Touch'
import { TweenLite } from 'gsap'
import {getUVFromViewPortCoord} from "./helpers/MouseUVUtil"
const THREE = require('three')

import {
  BaseBlend ,
  BaseBlendConst
} from "./BaseBlend"





/**
 * @typeDef {object} Blend.EVENTS__TYPE
 * @property {string} BLEND_UNIT -  emitted on blend value change. event value:number (0 to 1)
 * @property {string} BLEND_TOUCH - emitted on  touch or mousedown.  event value: none
 * @property {string} BLEND_TRANSITION_COMPLETE - emitted on blend value =1. can use to switch video cell targets. event value: none
 */
export const EVENTS = {
  BLEND_UNIT: BaseBlendConst.EVENTS.BLEND_UNIT,
  BLEND_TOUCH: 'BLEND_TOUCH',
  BLEND_TRANSITION_COMPLETE:'BLEND_TRANSITION_COMPLETE',
}


/**@typeDef {object} Blend.FIT__TYPE
 *  @property {string} COVER  - fill frame, maintain aspect ratio
 *  @property {string} CONTAIN - fit within frame, maintain aspect ratio
 *  @property {string} STRETCH - stretch to frame, dont maintain aspect ratio
 */
export const FIT = {
  COVER: 'cover',
  CONTAIN: 'contain',
  STRETCH: 'stretch',
}



/**@typeDef {object} Blend.TOUCH_MODE__TYPE
 * @property {string} NONE - for external-only control of touch/blend events
 * @property {string} HOLD - transition to index2 on hold - transition to index1 on release
 * @property {string} HOLD_THRESHOLD - trigger transition to index2 on hold duration, then index1 = index2. if threshold crossed - no release trigger
 * @property {string} TOGGLE - trigger transition to index2 on press, then index1 = index2. no release trigger
 */
export const TOUCH_MODE = {
  NONE:"none",
  HOLD:"hold",
  HOLD_THRESHOLD:"hold_threshold",
  TOGGLE:"toggle"
}


export const createDefaultConfig = function(){

  return {
    media: null,

    fit: FIT.CONTAIN, // contain | cover | stretch

    cellGrid: {
      x: 1,
      y: 2,
    },
    cellIndexLength: null, //set if spritesheet max-cell-index is less than the full cell grid
    cellUVTrim:0.985, // trim video cell texture lookups to avoid cell-bleed

    vertexShader:null, //eg ./shaders/base.vert
    fragmentShader:null, //eg ./shaders/circleFade.frag

    //internalRenderer: if false : wont create renderer, scene, camera.
    internalRenderer:true,

    touchMode:TOUCH_MODE.HOLD,
    touchEasing:0.5, //1 = no easing, 0.1 = take 30ish frames to move to new value
    holdThreshold:0.25,//TOUCH_MODE.HOLD_THRESHOLD only - blend unit minimum to switch to next cellIndex

    transitionPress:{
        delay:0.0,
        duration:1.0,
        ease:"Sine.easeIn"
    },
    transitionRelease:{
        delay:0.0,
        duration:0.8,
        ease:"Sine.easeOut"
    },

    blendEasing:0.3, //1 = no easing, 0.1 = take 30ish frames to move to new value
    blendBiasFunc: (n)=> n, //null,//(n)=>{return n}, //bias the blend shader input (curve)
    bleedBiasFunc: (n)=> window.Power1.easeOut.getRatio(n), //bias the radialBleed shader input (curve)
    autoMaxRadius:true, // true: will automatically calculate the required maxRadius to cover the video for a radial based effect

    mouseRayCasting: false, // if custom-transforming the video mesh, or adding mesh to another scene, set true to position mouse correctly
  }

}

//better auto-complete
export const BlendConst = {
  EVENTS,
  FIT,
  TOUCH_MODE,
  createDefaultConfig
}


/**
 * @extends BaseBlend
 * @class
 */
  export class Blend extends BaseBlend {


      /** @property {...Blend.EVENTS__TYPE}**/
     get EVENTS() { return EVENTS }

     /** @property {...Blend.TOUCH_MODE__TYPE}**/
     get TOUCH_MODE() { return TOUCH_MODE }

     /** @property {...Blend.FIT__TYPE}**/
     get FIT() { return FIT }

     /** @property {...Blend.EVENTS__TYPE}**/
     static get EVENTS() { return EVENTS }

     /** @property {...Blend.TOUCH_MODE__TYPE}**/
     static get TOUCH_MODE() { return TOUCH_MODE }

      /** @property {...Blend.FIT__TYPE}**/
     static get FIT() { return FIT }

    /**
     *
     * @param {Object} config - configuration , see createDefaultConfig() for default values
     * @param {(HTMLVideoElement|Media|MediaElementTrack)} config.media - must pass one of: (src/services/MediaService/Media, src/services/MediaService/tracks/MediaElementTrack, HTMLVideoElement)
     *
     * @param {string} [config.fit=contain]  - video maintain-aspect-crop behaviour [contain | cover | stretch], see const FIT
     *
     * @param {object} [config.cellGrid={x:1, y:2}] - the x:column, y:row totals of the video spritesheet
     * @param {number} config.cellGrid.x - the column total of the video spritesheet (x axis)
     * @param {number} config.cellGrid.y - the row total of the video spritesheet (y axis)
     *
     * @param {integer} [config.cellIndexLength] - set if spritesheet max-cell-index is less than the full celltotal(w*h)
     * @param {number} [config.cellUVTrim=0.985] - trim video cell texture lookups to avoid cell-bleed (notrim=1, trim to 25% borderwidth = 0.5
     *
     * @param {string} [config.vertexShader=./shaders/base.vert] - eg ./shaders/base.vert
     * @param {string} [config.fragmentShader=./shaders/circleFade.frag] - eg ./shaders/circleFade.frag
     * @param {boolean} [config.internalRenderer=true] - if false, wont create renderer, scene, camera.
     * Intended to then use mesh/material in an externally defined scene.
     * Side effects: fit should probably then be full uv coverage (fit:FIT.STRETCH)

     * @param {string} [config.touchMode=hold] - (none|hold|hold_threshold|toggle) see const TOUCH_MODE
     * @param {number} [config.touchEasing=0.5]
     * @param {number} [config.holdThreshold=0.25]
     *
     * @param {object} [config.transitionPress={delay:0.0,duration:1.0,ease:"Sine.easeIn"}] - TweenMax props for transition of blend on press
     * @param {number} config.transitionPress.delay - Seconds
     * @param {number} config.transitionPress.duration - Seconds
     * @param {String} config.transitionPress.ease
     *
     * @param {object} [config.transitionRelease={delay:0.0,duration:0.8,ease:"Sine.easeOut"}] - TweenMax props for transition of blend on release
     * @param {number} config.transitionRelease.delay - Seconds
     * @param {number} config.transitionRelease.duration - Seconds
     * @param {String} config.transitionRelease.ease
     *
     *
     * @param {number} [config.blendEasing=0.3]
     * @param {function} [config.blendBiasFunc=(n)=> n]
     * @param {function} [config.bleedBiasFunc=(n)=> window.Power1.easeOut.getRatio(n) ]
     * @param {boolean} [config.autoMaxRadius=true]
     * @param {boolean} [config.mouseRayCasting=false]
     */
    constructor(config) {
      config = Object.assign({},createDefaultConfig(),config)
      super(config)
    }

     init(){
       this.blendTarget = 0
       this._blendEased = 0
       super.init()
     }


  /** raf, extended from BaseBlend*/
    update(){
      this.updateTouch()
      this.updateBlend(this.blendTarget)
      super.update()
    }


    /** called per frame, updates any touch behaviour
    * @param {number} target - blend value to bias/ease to
    */
    updateBlend(target){
      let c = this.config;
      let blend = target;
      this._blendEased += (blend - this._blendEased) * c.blendEasing
      blend = this._blendEased;

      this.blend = (c.blendBiasFunc) ? c.blendBiasFunc(blend) : blend
      this.radiusBleed = (c.bleedBiasFunc) ? c.bleedBiasFunc(blend) : blend
    }




  //----TOUCH HANDLING - ( separate touch handler class?? ) ------------------------------------------------//

  /** called per frame, updates any touch behaviour */
    updateTouch(){
     if (this.config.touchMode === TOUCH_MODE.NONE){return}
     let touch = this._getTouch()

     let released = (this._prevTouched && !touch.touching)
     let pressed = (!this._prevTouched && touch.touching)
     if (pressed){this.emit(EVENTS.BLEND_TOUCH)}
     this._prevTouched = touch.touching;

     this.touchX += (touch.x - this.touchX) * this.config.touchEasing
     this.touchY += (touch.y - this.touchY) * this.config.touchEasing

     if (this.config.touchMode === TOUCH_MODE.HOLD){ this._updateTouch_HOLD(pressed ,released) }
     if (this.config.touchMode === TOUCH_MODE.TOGGLE){this._updateTouch_TOGGLE( pressed ,released) }
     if (this.config.touchMode === TOUCH_MODE.HOLD_THRESHOLD){  this._updateTouch_HOLD_THRESHOLD(pressed ,released) }
    }

    setTouchImmediate(){
     let touch = this._getTouch()
     this.touchX = touch.x
     this.touchY = touch.y
    }



    /* --------------END TOUCH HANDLING------------------------------------------------------------*/


    /* --------------TRANSITION UTILS--------------------------------------------------------------*/



  /**
   * @method
   * @param {function} [onComplete]
   * @param {object} [tweenProps] - TweenMax props for transition of blend
   * @param {number} tweenProps.delay - Seconds
   * @param {number} tweenProps.duration - Seconds
   * @param {String} tweenProps.ease
   */
    toggleBlend = (onComplete, tweenProps ) => {
      //tweenProps = {duration delay ease}
      this._blendTweenTarget = (this._blendTweenTarget === 1) ? 0 : 1
      this.tweenBlend(this._blendTweenTarget, onComplete, tweenProps );
    }

  /**
   * @method
   * @param {integer} fromIndex
   * @param {integer} toIndex
   * @param {integer} nextIndex2
   * @param {function} [onComplete]
   * @param {object} [tweenProps] - TweenMax props for transition of blend
   * if not provided - uses config tweenProps
   * @param {number} tweenProps.delay - Seconds
   * @param {number} tweenProps.duration - Seconds
   * @param {String} tweenProps.ease
   */
    transitionToIndex = (fromIndex, toIndex, nextIndex2, onComplete, tweenProps ) => {
      //tweenProps = {duration delay ease}
        this.cellIndex1 = fromIndex
        this.cellIndex2 = toIndex

        this.tweenBlend(1, ()=>{
          this.blend = this.blendTarget = this._blendEased = 0
          this.cellIndex1 = toIndex
          this.cellIndex2 = nextIndex2
          if (onComplete){onComplete()}

         }, tweenProps )
    }

  /**
   * tweens to a given blend value
   * @method
   * @param {number blend = (0 to 1)
   * @param {function } [onComplete]
   * @param {object} [tweenProps] - TweenMax props for transition of blend
   * if not provided - uses config tweenProps
   * @param {number} tweenProps.delay - Seconds
   * @param {number} tweenProps.duration - Seconds
   * @param {String} tweenProps.ease
   */
    tweenBlend =(blend, onComplete, tweenProps ) => {
      let c = (blend === 0) ? this.config.transitionRelease : this.config.transitionPress
      tweenProps = tweenProps || c;

      this._blendTweenTarget = blend
      let props = {
        blendTarget:blend,
        delay:tweenProps.delay,
        ease:tweenProps.ease,
        onComplete:() =>{
          if (onComplete){onComplete()}
          this._tweeningBlend = false
          if(blend === 1){this.emit(EVENTS.BLEND_TRANSITION_COMPLETE)}
        }
      }

      this._tweeningBlend = true;
      TweenLite.killTweensOf(this, {blendTarget:true})
      TweenLite.to(this, tweenProps.duration, props)
    }


  /* --------------END TRANSITION UTILS------------------------------------------------------------*/

    destroy(){
      this.touchUtil.destroy()
      this.touchUtil = null
      TweenLite.killTweensOf(this)
      super.destroy()
    }


    /*--------------INTERNAL------------------------------------------------------------*/

  _addEvents() {
    this.touchUtil = new Touch(this.el)
    super._addEvents()
  }

  _removeEvents() {
    super._removeEvents()
  }

  _getTouch(){
    this.__touch = this.__touch || {x:-1, y:-1, touching:false}
    let rayCasting = this.config.mouseRayCasting
    if (rayCasting){

      let uv = getUVFromViewPortCoord(this.touchUtil.x,this.touchUtil.y, this.camera, [this.mesh])
      if (uv){
        this.__touch.x = uv.x
        this.__touch.y = 1.0-uv.y
      }
      this.__touch.touching = (this.touchUtil.touching && uv)

    }else{
      this.__touch.x = this.touchUtil.x
      this.__touch.y = this.touchUtil.y
      this.__touch.touching = this.touchUtil.touching
    }

    return this.__touch
  }


  _updateTouch_HOLD( pressed ,released){
    if (pressed){
      this.setTouchImmediate()
      this.tweenBlend(1)
    }
    if (released){
      this.tweenBlend(0)
    }
  }

  _updateTouch_TOGGLE( pressed ,released){
    if (pressed && !this._tweeningBlend){
      this.setTouchImmediate()
      let next = (this.cellIndex2 +1) % this.config.cellIndexLength;
      this.transitionToIndex(this.cellIndex1, this.cellIndex2, next );
    }
  }

  _updateTouch_HOLD_THRESHOLD(pressed ,released){
    if(this._canReleaseBlend === undefined){
      this._canReleaseBlend = false
      this._canPressBlend = true
    }

    if (pressed && this._canPressBlend){
      this.setTouchImmediate()
      this._canPressBlend = false
      this._canReleaseBlend = true
      let nextTargetIndex2 = (this.cellIndex2 +1) % this.config.cellIndexLength;
      this.transitionToIndex(this.cellIndex1, this.cellIndex2, nextTargetIndex2, ()=>{this._canPressBlend = true})

    } else if (this._canReleaseBlend && (0.2 < this.blend) ){
      this._canReleaseBlend = false
      this._canPressBlend = false

    } else if (this._canReleaseBlend && released){
      this.tweenBlend(0)
      this._canReleaseBlend = false
      this._canPressBlend = true
    }
  }

  _killTweens = () => {
    TweenLite.killTweensOf(this, {blendTarget:true})
  }

}