import EventEmitter from 'src/helpers/EventEmitter'
import EventObserver from 'src/helpers/EventObserver'
import Raf from 'src/helpers/Raf'
import {clamp} from 'src/helpers/MathUtils'
import { TweenLite } from 'gsap'
import {BlendShaderFacade} from "./shaderFacades/BlendShaderFacade"
const THREE = require('three')
/**
* @typeDef {object} BaseBlend.EVENTS__TYPE
* @property {string} BLEND_UNIT - emitted on blend value change. event value:number (0 to 1)
*/
const EVENTS = {
BLEND_UNIT: 'BLEND_UNIT',
}
/**@typeDef {object} BaseBlend.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
*/
const FIT = Object.freeze({
COVER: 'cover',
CONTAIN: 'contain',
STRETCH: 'stretch',
})
export const createDefaultConfig = function(){
let defaultConfig = {
media: null,
fit: FIT.CONTAIN,
pollResize:true,
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,
//if using a radius effect in shader, auto-calculate max-radius based on hypotonuse (diagonal) of rendered viewport
autoMaxRadius:true,
}
return defaultConfig
}
//better auto-complete
export const BaseBlendConst = {
EVENTS,
FIT,
createDefaultConfig
}
/**
* @class BaseBlend - shader handling for Blend
*/
export class BaseBlend {
/** @property {...BaseBlend.EVENTS__TYPE}**/
get EVENTS() { return EVENTS }
/** @property {...BaseBlend.FIT__TYPE}**/
get FIT() { return FIT }
//CLASS const
/** @property {...BaseBlend.EVENTS__TYPE}**/
static get EVENTS() { return EVENTS }
/** @property {...BaseBlend.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 {boolean} [config.autoMaxRadius=true]
*/
constructor(config) {
Object.assign(this, EventEmitter )
Object.assign(this, EventObserver)
// CONFIG
this.config = Object.assign({}, createDefaultConfig() , config)
this.config.cellIndexLength = this.config.cellIndexLength || this.config.cellGrid.x * this.config.cellGrid.y
this.init();
}
init(){
this._initElement()
this._initVideo()
this._initRenderer()
this._initTexture()
this._initMaterial()
this._setCoreUniforms()
this._initMesh()
this.blend = 0
this._addEvents()
}
getMaterial(){
return this.shader.material
}
getMesh(){
return this.mesh
}
_initElement(){
this.el = document.createElement('canvas')
this.el.style.width = "100%";
this.el.style.height = "100%";
this.elementRect = {width:1000, height:1000}
}
_initVideo(){
let media = this.config.media
let isVideo = (media && media.nodeName && media.nodeName.toLowerCase() === "video")
let isMedia = (media && media.getDomElement)
if (!media || (!isVideo && !isMedia) ){
throw("BaseBlend requires a HTMLMediaElement, (resn)Media or (resn)MediaElemenTrack")
}
this.video = (isVideo) ? media : media.getDomElement()
this.media = (isMedia) ? media : null
this._setupVideoAspect()
this.listen(this.video, 'loadedmetadata', (e) => {this._setupVideoAspect()})
}
_setupVideoAspect(){
this.videoWidth = (this.video.videoWidth || 16) / this.config.cellGrid.x
this.videoHeight = (this.video.videoHeight || (9*2)) / this.config.cellGrid.y
this.videoAspect = this.videoWidth/this.videoHeight;
if(this.shader){this._resize()}
}
_initRenderer(){
let w = window.innerWidth
let h = window.innerHeight
if (!this.config.internalRenderer) {return}
this.renderer = new THREE.WebGLRenderer({
canvas: this.el,
alpha: false,
antialias: false,
autoClear:true,
gammaInput:false,
gammaOutput:false
})
this.renderer.setPixelRatio(window.devicePixelRatio || 1)
this.renderer.setClearColor(0x000000, 0)
this.camera = new THREE.OrthographicCamera(w/-2, w/2, h/-2, h/2, 0.001, 1000)
this.camera.position.z = 1
this.scene = new THREE.Scene()
}
_initTexture(){
this._iosVideoTextureFix()
this.texture = new THREE.VideoTexture(this.video)
this.texture.minFilter = this.texture.magFilter = THREE.LinearFilter
this.texture.format = THREE.RGBFormat
}
_iosVideoTextureFix(){
//ios needs to refresh the src to correctly display the videoTexture, otherwise shows black
// - unsure of underlying issue.
let ios = /(iOS|iPod|iPad|iPhone)/i.test(navigator.userAgent)
if (ios){
if (this.video._iosWebglTainted){
if(this.media){
//this.media.stop()
this.media.videoTrack.load(this.media.videoTrack._src).then(()=>{this.media.replay()})
}
if(!this.media){ this.video.src = this.video.src }
}
this.video._iosWebglTainted = true
}
}
_initMaterial() {
this.shader = new BlendShaderFacade({
vertexShader: this.config.vertexShader,
fragmentShader: this.config.fragmentShader,
uniforms:{}
})
}
_initMesh(){
let geometry = new THREE.PlaneGeometry(1, 1, 4, 4)
this.mesh = new THREE.Mesh(geometry, this.shader.material)
this.mesh.position.z = 0
if (this.config.internalRenderer){
this.scene.add(this.mesh)
}
}
_addEvents() {
if (this.config.internalRenderer){
this.listen(window,'resize', this._resize)
this._resize()
TweenLite.delayedCall(1, this._resize, null,null, true)
TweenLite.delayedCall(2, this._resize, null,null, true)
}
Raf.add(this._onFrame)
}
_removeEvents() {
Raf.remove(this._onFrame)
this.unlisten()
this.off()
}
_updateCanvasRect = () =>{
let rect = this.el.getBoundingClientRect();
this.elementRect.width = rect.width || 100
this.elementRect.height = rect.height || 100
}
_resize = () => {
this._updateCanvasRect();
let oW = this.elementRect.width;
let oH = this.elementRect.height;
this.camera.left = oW / -2
this.camera.right = oW / 2
this.camera.top = oH / 2
this.camera.bottom = oH / -2
this.camera.aspect = oW / oH
this.camera.updateProjectionMatrix()
//dont set css -let external style dictate
this.renderer.setSize(oW, oH, false)
this.calculateMeshScale()
}
calculateMeshScale(){
let oW = this.elementRect.width;
let oH = this.elementRect.height;
//note aspect-ratio-fit logic in fragmentShader
this.mesh.scale.x = oW
this.mesh.scale.y = oH
if(this.config.autoMaxRadius) this.calculateMaxRadius()
this.calculateVideoScale()
}
calculateMaxRadius(){
this.updateResolution()
var meshAspect = this.resolutionY/this.resolutionX;
let a = meshAspect
let b = 1
let hypotenuse = Math.sqrt((a*a) + (b*b))
this.maxRadius = hypotenuse; //diagonal of resolution-aspect
}
calculateVideoScale(){
this.updateResolution()
let oW = this.resolutionX //
let oH = this.resolutionY //
let oA = oW/oH
let vA = this.videoAspect
//video uv scale for shader
let cover = (this.config.fit === FIT.COVER)
let stretch = (this.config.fit === FIT.STRETCH)
if (stretch){
this.videoScaleX = 1
this.videoScaleY = 1
} else if((!cover && (oA < vA)) || (cover &&(oA > vA))){
this.videoScaleX = 1
this.videoScaleY = (1.0/oA)*vA
} else{
this.videoScaleX =oA/vA
this.videoScaleY = 1
}
}
//--ON FRAME --------------------------------------------------------------------------------------//
_onFrame = (delta) =>{
this.update()
}
update(){
this._updateResize()
this._updateMediaAudio()
this.updateResolution()
this._render()
}
_updateResize = () =>{
if(!this.config.pollResize ){ return}
let changed = (this._wiw !== window.innerWidth || this._wih !== window.innerHeight)
this._wiw = window.innerWidth
this._wih = window.innerHeight
if (changed){this._resize()}
}
updateResolution(){
//NOTE: geometry x,y init dimensions are 1,1, if otherwise multiply by initial geometry dimensions
let pixelAspect = window.devicePixelRatio || 1
this.resolutionX = this.mesh.scale.x * pixelAspect
this.resolutionY = this.mesh.scale.y * pixelAspect
}
_updateMediaAudio(){
if(!this.media || !this.media.blendAudioBetweenIndexes){return}
this.media.blendAudioBetweenIndexes(clamp(this.blend,0,1), this.cellIndex1, this.cellIndex2)
}
_render(){
if (this.config.internalRenderer){
this.renderer.render(this.scene, this.camera)
}
}
/*------MATERIAL SETUP + INTERFACING---------------------------------------------------------------------*/
_setCoreUniforms(){
this.shader.videoTexture = this.texture
this.shader.cellGridX = this.config.cellGrid.x
this.shader.cellGridY = this.config.cellGrid.y
this.shader.uvTrim = this.config.cellUVTrim
}
/** @property {number} blend - sets blend value in shader */
set blend(val) {
let changed = (this.shader.blend !== val)
this.shader.blend = val
if(changed) {this.emit(EVENTS.BLEND_UNIT, val);}
}
get blend() { return this.shader.blend }
/** @property {number} touchX - sets touchX value in shader. touchX,touchY are used as a radial effect center */
set touchX(val) { this.shader.material.uniforms.touch.value.x = val || 0 }
get touchX() { return this.shader.touchX }
/** @property {number} touchY - sets touchX value in shader. touchX,touchY are used as a radial effect center */
set touchY(val) { this.shader.touchY = val || 0 }
get touchY() { return this.shader.touchY }
/** @property {integer} cellIndex1 - sets cellIndex1 value in shader. cellIndex1 is the video-spritesheet cellIndex blended FROM*/
set cellIndex1(val) { this.shader.cellIndexX = clamp(val, 0, this.config.cellIndexLength-1); }
get cellIndex1() { return this.shader.cellIndexX }
/** @property {integer} cellIndex2 - sets cellIndex2 value in shader. cellIndex2 is the video-spritesheet cellIndex blended TO*/
set cellIndex2(val) { this.shader.cellIndexY = clamp(val, 0, this.config.cellIndexLength-1); }
get cellIndex2() { return this.shader.cellIndexY }
/** @property {number} maxRadius - sets maxRadius value in shader.
* maxRadius is a viewport-unit setting for the maximum blend radius
* @see config.autoMaxRadius - set to true, will auto-calculate/update this value to the minimum full-coverage size
* */
set maxRadius(val) { this.shader.maxRadius = val || 0 }
get maxRadius() { return this.shader.maxRadius}
/** @property {number} radiusBleed - sets radiusBleed value in shader. changes the radial effect bleed width */
set radiusBleed(val) { this.shader.radiusBleed = val || 0 }
get radiusBleed() { return this.shader.radiusBleed}
/** @property {number} uvTrim - sets uvTrim value in shader.
* @see {@link config.cellUVTrim} - set initially by this config value
* */
set uvTrim(val) { this.shader.uvTrim = val || 0 }
get uvTrim() { return this.shader.uvTrim}
/** @property {number} resolutionX - sets resolutionX value in shader.
* auto-calculated/updated based on canvas size
*/
set resolutionX(val) { this.shader.resolutionX = val || 0 }
get resolutionX() { return this.shader.resolutionX }
/** @property {number} resolutionY - sets resolutionY value in shader.
* auto-calculated/updated based on canvas size
*/
set resolutionY(val) { this.shader.resolutionY = val || 0 }
get resolutionY() { return this.shader.resolutionY }
/** @property {number} videoScaleX - sets videoScaleX value in shader.
* auto-calculated/updated based on canvas size/video-aspect in accordance to config.fit
*/
set videoScaleX(val) { this.shader.videoScaleX = val || 0 }
get videoScaleX() { return this.shader.videoScaleX }
/** @property {number} videoScaleY - sets videoScaleY value in shader.
* auto-calculated/updated based on canvas size/video-aspect in accordance to config.fit
*/
set videoScaleY(val) { this.shader.videoScaleY = val || 0 }
get videoScaleY() { return this.shader.videoScaleY }
/*----END : MATERIAL SETUP + INTERFACING---------------------------------------------------------------------*/
_clearScene(scene){
for (let i = scene.children.length - 1; i >= 0; i--) {
const object = scene.children[i]
object.geometry.dispose()
object.material.dispose()
scene.remove(object)
}
}
/** as advertised */
destroy() {
this.destroyed = true;
this._removeEvents()
if(this.config.internalRenderer){
this.renderer.forceContextLoss()
this._clearScene(this.scene)
}
this.shader.destroy()
this.texture.dispose()
this.texture.image = null
this.renderer = null
this.camera = null
this.scene = null
this.mesh = null
this.texture = null
this.media = null
this.video = null
this.shader = null
delete this.renderer
delete this.camera
delete this.scene
delete this.mesh
delete this.texture
}
}