Submit
Path:
~
/
home
/
getwphos
/
public_html
/
ppine
/
wp-content
/
plugins
/
trx_addons
/
addons
/
image-effects
/
curtains
/
File Content:
curtains.js
/*** Little WebGL helper to apply images, videos or canvases as textures of planes Author: Martin Laxenaire https://www.martin-laxenaire.fr/ Version: 6.1.1 https://www.curtainsjs.com/ ***/ 'use strict'; /*** CURTAINS CLASS ***/ /*** This is our main class to call to init our curtains Basically sets up all necessary intern variables based on params and runs the init method params: @containerID (string): the container ID that will hold our canvas returns: @this: our Curtains element ***/ function Curtains(params) { this.planes = []; this.renderTargets = []; this.shaderPasses = []; // textures this._imageCache = []; this._drawStacks = { "opaque": { length: 0, programs: [], order: [], }, "transparent": { length: 0, programs: [], order: [], }, "renderPasses": [], "scenePasses": [], }; this._drawingEnabled = true; this._forceRender = false; // handle old version init param if(typeof params === "string") { console.warn("Since v4.0 you should use an object to pass your container and other parameters. Please refer to the docs: https://www.curtainsjs.com/documentation.html"); var container = params; params = { container: container }; } // set container if(!params.container) { var container = document.createElement("div"); container.setAttribute("id", "curtains-canvas"); document.body.appendChild(container); this.container = container; } else { if(typeof params.container === "string") { this.container = document.getElementById(params.container); } else if(params.container instanceof Element) { this.container = params.container; } } // if we should use auto resize (default to true) this._autoResize = params.autoResize; if(this._autoResize === null || this._autoResize === undefined) { this._autoResize = true; } // if we should use auto render (default to true) this._autoRender = params.autoRender; if(this._autoRender === null || this._autoRender === undefined) { this._autoRender = true; } // if we should watch the scroll (default to true) this._watchScroll = params.watchScroll; if(this._watchScroll === null || this._watchScroll === undefined) { this._watchScroll = true; } // pixel ratio and rendering scale this.pixelRatio = params.pixelRatio || window.devicePixelRatio || 1; params.renderingScale = isNaN(params.renderingScale) ? 1 : parseFloat(params.renderingScale); this._renderingScale = Math.max(0.25, Math.min(1, params.renderingScale)); // webgl context parameters this.premultipliedAlpha = params.premultipliedAlpha || false; this.alpha = params.alpha; if(this.alpha === null || this.alpha === undefined) { this.alpha = true; } this.antialias = params.antialias; if(this.antialias === null || this.antialias === undefined) { this.antialias = true; } this.productionMode = params.production || false; if(!this.container) { if(!this.productionMode) console.warn("You must specify a valid container ID"); // call the error callback if provided if(this._onErrorCallback) { this._onErrorCallback() } return; } this._init(); } /*** Init by creating a canvas and webgl context, set the size and handle events Then prepare immediately for drawing as all planes will be created asynchronously ***/ Curtains.prototype._init = function() { this.glCanvas = document.createElement("canvas"); // set our webgl context var glAttributes = { alpha: this.alpha, premultipliedAlpha: this.premultipliedAlpha, antialias: this.antialias, }; this.gl = this.glCanvas.getContext("webgl2", glAttributes); this._isWebGL2 = !!this.gl; if(!this.gl) { this.gl = this.glCanvas.getContext("webgl", glAttributes) || this.glCanvas.getContext("experimental-webgl", glAttributes); } // WebGL context could not be created if(!this.gl) { if(!this.productionMode) console.warn("WebGL context could not be created"); if(this._onErrorCallback) { this._onErrorCallback() } return; } // get webgl extensions this._getExtensions(); // managing our webgl draw states this._glState = { // programs currentProgramID: null, programs: [], // last buffer sizes drawn (avoid redundant buffer bindings) currentBuffersID: 0, setDepth: null, // current frame buffer ID frameBufferID: null, // current scene pass ID scenePassIndex: null, // face culling cullFace: null, // textures flip Y flipY: null, }; // handling context this._contextLostHandler = this._contextLost.bind(this); this.glCanvas.addEventListener("webglcontextlost", this._contextLostHandler, false); this._contextRestoredHandler = this._contextRestored.bind(this); this.glCanvas.addEventListener("webglcontextrestored", this._contextRestoredHandler, false); // handling scroll event this._scrollManager = { handler: this._scroll.bind(this, true), shouldWatch: this._watchScroll, // init values even if we won't necessarily use them xOffset: window.pageXOffset, yOffset: window.pageYOffset, lastXDelta: 0, lastYDelta: 0, }; if(this._watchScroll) { window.addEventListener("scroll", this._scrollManager.handler, {passive: true}); } // this will set the size as well this.setPixelRatio(this.pixelRatio, false); // handling window resize event this._resizeHandler = null; if(this._autoResize) { this._resizeHandler = this.resize.bind(this, true); window.addEventListener("resize", this._resizeHandler, false); } // we can start rendering now this._readyToDraw(); }; /*** Get all available WebGL extensions based on WebGL used version Called on init and on context restoration ***/ Curtains.prototype._getExtensions = function() { this._extensions = []; if(this._isWebGL2) { this._extensions['EXT_color_buffer_float'] = this.gl.getExtension('EXT_color_buffer_float'); this._extensions['OES_texture_float_linear'] = this.gl.getExtension('OES_texture_float_linear'); this._extensions['WEBGL_lose_context'] = this.gl.getExtension('WEBGL_lose_context'); } else { this._extensions['OES_vertex_array_object'] = this.gl.getExtension('OES_vertex_array_object'); this._extensions['OES_texture_float'] = this.gl.getExtension('OES_texture_float'); this._extensions['OES_texture_float_linear'] = this.gl.getExtension('OES_texture_float_linear'); this._extensions['OES_texture_half_float'] = this.gl.getExtension('OES_texture_half_float'); this._extensions['OES_texture_half_float_linear'] = this.gl.getExtension('OES_texture_half_float_linear'); this._extensions['OES_element_index_uint'] = this.gl.getExtension('OES_element_index_uint'); this._extensions['OES_standard_derivatives'] = this.gl.getExtension('OES_standard_derivatives'); this._extensions['EXT_sRGB'] = this.gl.getExtension('EXT_sRGB'); this._extensions['WEBGL_depth_texture'] = this.gl.getExtension('WEBGL_depth_texture'); this._extensions['WEBGL_draw_buffers'] = this.gl.getExtension('WEBGL_draw_buffers'); this._extensions['WEBGL_lose_context'] = this.gl.getExtension('WEBGL_lose_context'); } }; /*** SIZING ***/ /*** Set the pixel ratio property and update everything by calling resize method ***/ Curtains.prototype.setPixelRatio = function(pixelRatio, triggerCallback) { this.pixelRatio = parseFloat(Math.max(pixelRatio, 1)) || 1; // apply new pixel ratio to all our elements but don't trigger onAfterResize callback this.resize(triggerCallback); }; /*** Set our container and canvas sizes ***/ Curtains.prototype._setSize = function() { // get our container bounding client rectangle var containerBoundingRect = this.container.getBoundingClientRect(); // use the bounding rect values this._boundingRect = { width: containerBoundingRect.width * this.pixelRatio, height: containerBoundingRect.height * this.pixelRatio, top: containerBoundingRect.top * this.pixelRatio, left: containerBoundingRect.left * this.pixelRatio, }; // iOS Safari > 8+ has a known bug due to navigation bar appearing/disappearing // this causes wrong bounding client rect calculations, especially negative top value when it shouldn't // to fix this we'll use a dirty but useful workaround // first we check if we're on iOS Safari var isSafari = !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/); var iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; if(isSafari && iOS) { // if we are on iOS Safari we'll need a custom function to retrieve our container absolute top position function getTopOffset(el) { var topOffset = 0; while(el && !isNaN(el.offsetTop)) { topOffset += el.offsetTop - el.scrollTop; el = el.offsetParent; } return topOffset; } // use it to update our top value this._boundingRect.top = getTopOffset(this.container) * this.pixelRatio; } this.glCanvas.style.width = Math.floor(this._boundingRect.width / this.pixelRatio) + "px"; this.glCanvas.style.height = Math.floor(this._boundingRect.height / this.pixelRatio) + "px"; this.glCanvas.width = Math.floor(this._boundingRect.width * this._renderingScale); this.glCanvas.height = Math.floor(this._boundingRect.height * this._renderingScale); this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight); // update scroll values ass well if(this._scrollManager.shouldWatch) { this._scrollManager.xOffset = window.pageXOffset; this._scrollManager.yOffset = window.pageYOffset; } }; /*** Useful to get our container bounding rectangle without triggering a reflow/layout returns : @boundingRectangle (obj): an object containing our container bounding rectangle (width, height, top and left properties) ***/ Curtains.prototype.getBoundingRect = function() { return this._boundingRect; }; /*** Resize our container and all the planes params: @triggerCallback (boolean): Whether we should trigger onAfterResize callback ***/ Curtains.prototype.resize = function(triggerCallback) { this._setSize(); // resize the planes only if they are fully initiated for(var i = 0; i < this.planes.length; i++) { if(this.planes[i]._canDraw) { this.planes[i].planeResize(); } } // resize the shader passes only if they are fully initiated for(var i = 0; i < this.shaderPasses.length; i++) { if(this.shaderPasses[i]._canDraw) { this.shaderPasses[i].planeResize(); } } // resize the render targets for(var i = 0; i < this.renderTargets.length; i++) { this.renderTargets[i].resize(); } // be sure we'll update the scene even if drawing is disabled this.needRender(); var self = this; setTimeout(function() { if(self._onAfterResizeCallback && triggerCallback) { self._onAfterResizeCallback(); } }, 0); }; /*** SCROLLING ***/ /*** Handles the different values associated with a scroll event (scroll and delta values) If no plane watch the scroll then those values won't be retrieved to avoid unnecessary reflow calls If at least a plane is watching, update all watching planes positions based on the scroll values And force render for at least one frame to actually update the scene ***/ Curtains.prototype._scroll = function() { // get our scroll values var scrollValues = { x: window.pageXOffset, y: window.pageYOffset, }; // update scroll manager values this.updateScrollValues(scrollValues.x, scrollValues.y); // shouldWatch should be true if at least one plane watches the scroll if(this._scrollManager.shouldWatch) { for(var i = 0; i < this.planes.length; i++) { // if our plane is watching the scroll, update its position if(this.planes[i].watchScroll) { this.planes[i].updateScrollPosition(); } } // be sure we'll update the scene even if drawing is disabled this.needRender(); } if(this._onScrollCallback) { this._onScrollCallback(); } }; /*** Updates the scroll manager X and Y scroll values as well as last X and Y deltas Internally called by the scroll handler if at least one plane is watching the scroll Could be called externally as well if the user wants to handle the scroll by himself params: @x (float): scroll value along X axis @y (float): scroll value along Y axis ***/ Curtains.prototype.updateScrollValues = function(x, y) { // get our scroll delta values var lastScrollXValue = this._scrollManager.xOffset; this._scrollManager.xOffset = x; this._scrollManager.lastXDelta = lastScrollXValue - this._scrollManager.xOffset; var lastScrollYValue = this._scrollManager.yOffset; this._scrollManager.yOffset = y; this._scrollManager.lastYDelta = lastScrollYValue - this._scrollManager.yOffset; }; /*** Returns last delta scroll values returns: @delta (obj): an object containing X and Y last delta values ***/ Curtains.prototype.getScrollDeltas = function() { return { x: this._scrollManager.lastXDelta, y: this._scrollManager.lastYDelta, }; }; /*** Returns last window scroll values returns: @scrollValue (obj): an object containing X and Y last scroll values ***/ Curtains.prototype.getScrollValues = function() { return { x: this._scrollManager.xOffset, y: this._scrollManager.yOffset, }; }; /*** ENABLING / DISABLING DRAWING ***/ /*** Enables the render loop ***/ Curtains.prototype.enableDrawing = function() { this._drawingEnabled = true; }; /*** Disables the render loop ***/ Curtains.prototype.disableDrawing = function() { this._drawingEnabled = false; }; /*** Forces the rendering of the next frame, even if disabled ***/ Curtains.prototype.needRender = function() { this._forceRender = true; }; /*** HANDLING CONTEXT ***/ /*** Called when the WebGL context is lost ***/ Curtains.prototype._contextLost = function(event) { event.preventDefault(); this._glState = { currentProgramID: null, programs: [], // last buffer sizes drawn (avoid redundant buffer bindings) currentBuffersID: 0, setDepth: null, // current frame buffer ID frameBufferID: null, // current scene pass ID scenePassIndex: null, // face culling cullFace: null, // textures flip Y flipY: null, }; // cancel requestAnimationFrame if(this._animationFrameID) { window.cancelAnimationFrame(this._animationFrameID); } var self = this; setTimeout(function() { if(self._onContextLostCallback) { self._onContextLostCallback(); } }, 0); }; /*** Call this method to restore your context ***/ Curtains.prototype.restoreContext = function() { if(this.gl && this._extensions['WEBGL_lose_context']) { this._extensions['WEBGL_lose_context'].restoreContext(); } else if(!this.productionMode) { if(!this.gl) { console.warn("Could not restore context because the context is not defined"); } else if(!this._extensions['WEBGL_lose_context']) { console.warn("Could not restore context because the restore context extension is not defined"); } } }; /*** Called when the WebGL context is restored ***/ Curtains.prototype._contextRestored = function() { var isDrawingEnabled = this._drawingEnabled; this._drawingEnabled = false; this._getExtensions(); // set blend func this._setBlendFunc(); // enable depth by default this._setDepth(true); // reset draw stacks this._drawStacks = { "opaque": { length: 0, programs: [], order: [], }, "transparent": { length: 0, programs: [], order: [], }, "renderPasses": [], "scenePasses": [], }; this._imageCache = []; // we need to reset everything : planes programs, shaders, buffers and textures ! for(var i = 0; i < this.renderTargets.length; i++) { this.renderTargets[i]._restoreContext(); } for(var i = 0; i < this.planes.length; i++) { this.planes[i]._restoreContext(); } // same goes for shader passes for(var i = 0; i < this.shaderPasses.length; i++) { this.shaderPasses[i]._restoreContext(); } // callback if(this._onContextRestoredCallback) { this._onContextRestoredCallback(); } // start drawing again // reset drawing flag to original value this._drawingEnabled = isDrawingEnabled; // force next frame render whatever our drawing flag value this.needRender(); // requestAnimationFrame again if needed if(this._autoRender) { this._animate(); } }; /*** Dispose everything ***/ Curtains.prototype.dispose = function() { this._isDestroying = true; // be sure to delete all planes while(this.planes.length > 0) { this.removePlane(this.planes[0]); } // we need to delete the shader passes also while(this.shaderPasses.length > 0) { this.removeShaderPass(this.shaderPasses[0]); } // finally we need to delete the render targets while(this.renderTargets.length > 0) { this.removeRenderTarget(this.renderTargets[0]); } // delete all programs from manager for(var i = 0; i < this._glState.programs.length; i++) { var program = this._glState.programs[i]; this.gl.deleteProgram(program.program); } this._glState = { currentProgramID: null, programs: [], // last buffer sizes drawn (avoid redundant buffer bindings) currentBuffersID: 0, setDepth: null, // current frame buffer ID frameBufferID: null, // current scene pass ID scenePassIndex: null, // face culling cullFace: null, // textures flip Y flipY: null, }; // wait for all planes to be deleted before stopping everything var self = this; var deleteInterval = setInterval(function() { if(self.planes.length === 0 && self.shaderPasses.length === 0 && self.renderTargets.length === 0) { // clear interval clearInterval(deleteInterval); // clear the buffer to clean scene self._clear(); // cancel animation frame if(self._animationFrameID) { window.cancelAnimationFrame(self._animationFrameID); } // remove event listeners if(this._resizeHandler) { window.removeEventListener("resize", self._resizeHandler, false); } if(this._watchScroll) { window.removeEventListener("scroll", this._scrollManager.handler, {passive: true}); } // ThemeREX fix: Incorrect event names in the original script //self.glCanvas.removeEventListener("webgllost", self._contextLostHandler, false); //self.glCanvas.removeEventListener("webglrestored", self._contextRestoredHandler, false); self.glCanvas.removeEventListener("webglcontextlost", self._contextLostHandler, false); self.glCanvas.removeEventListener("webglcontextrestored", self._contextRestoredHandler, false); // lose context if(self.gl && self._extensions['WEBGL_lose_context']) { self._extensions['WEBGL_lose_context'].loseContext(); } // clear canvas state self.glCanvas.width = self.glCanvas.width; self.gl = null; // remove canvas from DOM self.container.removeChild(self.glCanvas); self.container = null; self.glCanvas = null; } }, 100); }; /*** WEBGL PROGRAMS ***/ /*** Compile our WebGL shaders based on our written shaders params: @shaderCode (string): shader code @shaderType (shaderType): WebGL shader type (vertex or fragment) returns: @shader (compiled shader): our compiled shader ***/ Curtains.prototype._createShader = function(shaderCode, shaderType) { var shader = this.gl.createShader(shaderType); this.gl.shaderSource(shader, shaderCode); this.gl.compileShader(shader); // check shader compilation status only when not in production mode if(!this.productionMode) { if(!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { // shader debugging log as seen in THREE.js WebGLProgram source code var shaderTypeString = shaderType === this.gl.VERTEX_SHADER ? "vertex shader" : "fragment shader"; var shaderSource = this.gl.getShaderSource(shader); var shaderLines = shaderSource.split('\n'); for(var i = 0; i < shaderLines.length; i ++) { shaderLines[i] = (i + 1) + ': ' + shaderLines[i]; } shaderLines = shaderLines.join("\n"); console.warn("Errors occurred while compiling the", shaderTypeString, ":\n", this.gl.getShaderInfoLog(shader)); console.error(shaderLines); return null; } } return shader; }; /*** Compare two shaders strings to detect whether they are equal or not params: @firstShader (string): shader code @secondShader (string): shader code returns: @shader (bool): whether both shaders are equal or not ***/ Curtains.prototype._isEqualShader = function(firstShader, secondShader) { var isEqualShader = false; if(firstShader.localeCompare(secondShader) === 0) { isEqualShader = true; } return isEqualShader; }; /*** Checks whether the program has already been registered before creating it params: @vs (string): vertex shader code @fs (string): fragment shader code @plane (Plane or ShaderPass object): our plane to set up returns: @program (object): our program object, false if ceation failed ***/ Curtains.prototype._setupProgram = function(vs, fs, plane) { var existingProgram = {}; // check if the program exists // a program already exists if both vertex and fragment shaders are the same for(var i = 0; i < this._glState.programs.length; i++) { if(this._isEqualShader(this._glState.programs[i].vsCode, vs) && this._isEqualShader(this._glState.programs[i].fsCode, fs)) { existingProgram = this._glState.programs[i]; // no need to go further break; } } // we found an existing program if(existingProgram.program) { // if we've decided to share existing programs, just return the existing one if(plane.shareProgram) { return existingProgram; } else { // we need to create a new program but we don't have to re compile the shaders var shaders = this._useExistingShaders(existingProgram); return this._createProgram(shaders, plane._type); } } else { // compile the new shaders and create a new program var shaders = this._useNewShaders(vs, fs); if(!shaders) { return false; } else { return this._createProgram(shaders, plane._type); } } }; /*** Use already compiled shaders params: @program (object): an object containing amongst others our compiled shaders and their codes returns: @shadersObject (object): an object containing the shaders and their codes ***/ Curtains.prototype._useExistingShaders = function(program) { return { vs: { vertexShader: program.vertexShader, vsCode: program.vsCode, }, fs: { fragmentShader: program.fragmentShader, fsCode: program.fsCode, } }; }; /*** Compiles and creates new shaders params: @vs (string): vertex shader code @fs (string): fragment shader code returns: @shadersObject (object): an object containing the shaders and their codes ***/ Curtains.prototype._useNewShaders = function(vs, fs) { var isProgramValid = true; var vertexShader = this._createShader(vs, this.gl.VERTEX_SHADER); var fragmentShader = this._createShader(fs, this.gl.FRAGMENT_SHADER); if(!vertexShader || !fragmentShader) { if(!this.productionMode) console.warn("Unable to find or compile the vertex or fragment shader"); isProgramValid = false; } if(isProgramValid) { return { vs: { vertexShader: vertexShader, vsCode: vs, }, fs: { fragmentShader: fragmentShader, fsCode: fs, } }; } else { return isProgramValid; } }; /*** Used internally to set up program based on the created shaders and attach them to the program Checks whether the program has already been registered before creating it params: @shadersObject (object): an object containing the shaders and their codes @type (string): type of the plane that will use that program. Could be either "Plane" or "ShaderPass" returns: @program (object): our program object, false if ceation failed ***/ Curtains.prototype._createProgram = function(shadersObject, type) { var gl = this.gl; var isProgramValid = true; // we need to create a new shader program var webglProgram = gl.createProgram(); // if shaders are valid, go on if(isProgramValid) { gl.attachShader(webglProgram, shadersObject.vs.vertexShader); gl.attachShader(webglProgram, shadersObject.fs.fragmentShader); gl.linkProgram(webglProgram); // check the shader program creation status only when not in production mode if(!this.productionMode) { if(!gl.getProgramParameter(webglProgram, gl.LINK_STATUS)) { console.warn("Unable to initialize the shader program."); isProgramValid = false; } } // free the shaders handles gl.deleteShader(shadersObject.vs.vertexShader); gl.deleteShader(shadersObject.fs.fragmentShader); } // everything is ok we can go on if(isProgramValid) { // our program object var program = { id: this._glState.programs.length, vsCode: shadersObject.vs.vsCode, vertexShader: shadersObject.vs.vertexShader, fsCode: shadersObject.fs.fsCode, fragmentShader: shadersObject.fs.fragmentShader, program: webglProgram, type: type, }; // create a new entry in our draw stack array if it's a regular plane if(type === "Plane") { this._drawStacks["opaque"]["programs"]["program-" + program.id] = []; this._drawStacks["transparent"]["programs"]["program-" + program.id] = []; } // add it to our program manager programs list this._glState.programs.push(program); return program; } else { return isProgramValid; } }; /*** Tell WebGL to use the specified program if it's not already in use params: @program (object): a program object ***/ Curtains.prototype._useProgram = function(program) { if(this._glState.currentProgramID === null || this._glState.currentProgramID !== program.id) { this.gl.useProgram(program.program); this._glState.currentProgramID = program.id; } }; /*** Create a Plane element and load its images params: @planesHtmlElement (html element): the html element that we will use for our plane @params (obj): plane params: - vertexShaderID (string, optionnal): the vertex shader ID. If not specified, will look for a data attribute data-vs-id on the plane HTML element. Will throw an error if nothing specified - fragmentShaderID (string, optionnal): the fragment shader ID. If not specified, will look for a data attribute data-fs-id on the plane HTML element. Will throw an error if nothing specified - widthSegments (optionnal): plane definition along the X axis (1 by default) - heightSegments (optionnal): plane definition along the Y axis (1 by default) - mimicCSS (boolean, optionnal): define if the plane should mimic it's html element position (true by default) DEPRECATED - alwaysDraw (boolean, optionnal): define if the plane should always be drawn or it should be drawn only if its within the canvas (false by default) - autoloadSources (boolean, optionnal): define if the sources should be load on init automatically (true by default) - crossOrigin (string, optionnal): define the crossOrigin process to load images if any - fov (int, optionnal): define the perspective field of view (default to 75) - uniforms (obj, otpionnal): the uniforms that will be passed to the shaders (if no uniforms specified there wont be any interaction with the plane) returns : @plane: our newly created plane object ***/ Curtains.prototype.addPlane = function(planeHtmlElement, params) { // if the WebGL context couldn't be created, return null if(!this.gl) { if(!this.productionMode) console.warn("Unable to create a plane. The WebGl context couldn't be created"); if(this._onErrorCallback) { this._onErrorCallback() } return null; } else { if(!planeHtmlElement || planeHtmlElement.length === 0) { if(!this.productionMode) console.warn("The html element you specified does not currently exists in the DOM"); if(this._onErrorCallback) { this._onErrorCallback() } return false; } // init the plane var plane = new Curtains.Plane(this, planeHtmlElement, params); if(!plane._usedProgram) { plane = false; } else { this.planes.push(plane); } return plane; } }; /*** Completly remove a Plane element (delete from draw stack, delete buffers and textures, empties object, remove) params: @plane (plane element): the plane element to remove ***/ Curtains.prototype.removePlane = function(plane) { // first we want to stop drawing it plane._canDraw = false; var stackType = plane._transparent ? "transparent" : "opaque"; // now free the webgl part plane && plane._dispose(); // remove from our planes array var planeIndex; for(var i = 0; i < this.planes.length; i++) { if(plane.uuid === this.planes[i].uuid) { planeIndex = i; } } // erase the plane plane = null; this.planes[planeIndex] = null; this.planes.splice(planeIndex, 1); // now rebuild the drawStacks // start by clearing all the program drawstacks for(var i = 0; i < this._glState.programs.length; i++) { this._drawStacks["opaque"]["programs"]["program-" + this._glState.programs[i].id] = []; this._drawStacks["transparent"]["programs"]["program-" + this._glState.programs[i].id] = []; } this._drawStacks["opaque"].length = 0; this._drawStacks["transparent"].length = 0; // rebuild them with the new plane indexes for(var i = 0; i < this.planes.length; i++) { var plane = this.planes[i]; plane.index = i; var planeStackType = plane._transparent ? "transparent" : "opaque"; if(planeStackType === "transparent") { this._drawStacks[planeStackType]["programs"]["program-" + plane._usedProgram.id].unshift(plane.index); } else { this._drawStacks[planeStackType]["programs"]["program-" + plane._usedProgram.id].push(plane.index); } this._drawStacks[planeStackType].length++; } // look for an empty program drawstack array and remove it from the program order stack for(var i = 0; i < this._drawStacks[stackType]["order"].length; i++) { var programID = this._drawStacks[stackType]["order"][i]; if(this._drawStacks[stackType]["programs"]["program-" + programID].length === 0) { this._drawStacks[stackType]["order"].splice(i, 1); } } // clear the buffer to clean scene if(this.gl) this._clear(); // reset buffers to force binding them again this._glState.currentBuffersID = 0; }; /*** This function will stack planes by opaqueness/transparency, program ID and then indexes Stack order drawing process: - draw opaque then transparent planes - for each of those two stacks, iterate through the existing programs (following the "order" array) and draw their respective planes This is done to improve speed, notably when using shared programs, and reduce GL calls ***/ Curtains.prototype._stackPlane = function(plane) { var stackType = plane._transparent ? "transparent" : "opaque"; var drawStack = this._drawStacks[stackType]; if(stackType === "transparent") { drawStack["programs"]["program-" + plane._usedProgram.id].unshift(plane.index); // push to the order array only if it's not already in there if(!drawStack["order"].includes(plane._usedProgram.id)) { drawStack["order"].unshift(plane._usedProgram.id); } } else { drawStack["programs"]["program-" + plane._usedProgram.id].push(plane.index); // push to the order array only if it's not already in there if(!drawStack["order"].includes(plane._usedProgram.id)) { drawStack["order"].push(plane._usedProgram.id); } } drawStack.length++; }; /*** POST PROCESSING ***/ /*** RENDER TARGETS ***/ /*** Create a new RenderTarget element params: @params (obj): plane params: - depth (bool, optionnal): if the render target should use a depth buffer in order to preserve depth (default to false) returns : @renderTarget: our newly created RenderTarget object ***/ Curtains.prototype.addRenderTarget = function(params) { // if the WebGL context couldn't be created, return null if(!this.gl) { if(!this.productionMode) console.warn("Unable to create a render target. The WebGl context couldn't be created"); if(this._onErrorCallback) { this._onErrorCallback() } return null; } else { // init the render target var renderTarget = new Curtains.RenderTarget(this, params); return renderTarget; } }; /*** Completely remove a RenderTarget element params: @renderTarget (RenderTarget element): the render target element to remove ***/ Curtains.prototype.removeRenderTarget = function(renderTarget) { // check if it is attached to a shader pass if(renderTarget._shaderPass) { if(!this.productionMode) { console.warn("You're trying to remove a render target attached to a shader pass. You should remove that shader pass instead:", renderTarget._shaderPass); } return; } // loop through all planes that might use that render target and reset it for(var i = 0; i < this.planes.length; i++) { if(this.planes[i].target && this.planes[i].target.uuid === renderTarget.uuid) { this.planes[i].target = null; } } // remove from our render targets array var fboIndex; for(var i = 0; i < this.renderTargets.length; i++) { if(renderTarget.uuid === this.renderTargets[i].uuid) { fboIndex = i; } } // finally erase the plane this.renderTargets[fboIndex] = null; this.renderTargets.splice(fboIndex, 1); // now free the webgl part renderTarget && renderTarget._dispose(); renderTarget = null; // clear the buffer to clean scene if(this.gl) this._clear(); // reset buffers to force binding them again this._glState.currentBuffersID = 0; }; /*** SHADER PASSES ***/ /*** Create a new ShaderPass element params: @params (obj): plane params: - vertexShaderID (string, optionnal): the vertex shader ID. If not specified, will look for a data attribute data-vs-id on the plane HTML element. Will throw an error if nothing specified - fragmentShaderID (string, optionnal): the fragment shader ID. If not specified, will look for a data attribute data-fs-id on the plane HTML element. Will throw an error if nothing specified - crossOrigin (string, optionnal): define the crossOrigin process to load images if any - uniforms (obj, otpionnal): the uniforms that will be passed to the shaders (if no uniforms specified there wont be any interaction with the plane) returns : @shaderPass: our newly created ShaderPass object ***/ Curtains.prototype.addShaderPass = function(params) { // if the WebGL context couldn't be created, return null if(!this.gl) { if(!this.productionMode) console.warn("Unable to create a shader pass. The WebGl context couldn't be created"); if(this._onErrorCallback) { this._onErrorCallback() } return null; } else { // init the shader pass var shaderPass = new Curtains.ShaderPass(this, params); if(!shaderPass._usedProgram) { shaderPass = false; } else { if(params.renderTarget) { this._drawStacks.renderPasses.push(shaderPass.index); } else { this._drawStacks.scenePasses.push(shaderPass.index); } this.shaderPasses.push(shaderPass); } return shaderPass; } }; /*** Completly remove a ShaderPass element does almost the same thing as the removePlane method but handles only shaderPasses array, not drawStack params: @plane (plane element): the plane element to remove ***/ Curtains.prototype.removeShaderPass = function(plane) { // first we want to stop drawing it plane._canDraw = false; if(plane.target) { plane.target._shaderPass = null; this.removeRenderTarget(plane.target); plane.target = null; } // remove from shaderPasses our array var planeIndex; for(var i = 0; i < this.shaderPasses.length; i++) { if(plane.uuid === this.shaderPasses[i].uuid) { planeIndex = i; } } // finally erase the plane this.shaderPasses.splice(planeIndex, 1); // now rebuild the drawStacks // start by clearing all drawstacks this._drawStacks.scenePasses = []; this._drawStacks.renderPasses = []; // restack our planes with new indexes for(var i = 0; i < this.shaderPasses.length; i++) { this.shaderPasses[i].index = i; if(this.shaderPasses[i]._isScenePass) { this._drawStacks.scenePasses.push(this.shaderPasses[i].index); } else { this._drawStacks.renderPasses.push(this.shaderPasses[i].index); } } // reset the scenePassIndex if needed if(this._drawStacks.scenePasses.length === 0) { this._glState.scenePassIndex = null; } // now free the webgl part plane && plane._dispose(); plane = null; // clear the buffer to clean scene if(this.gl) this._clear(); // reset buffers to force binding them again this._glState.currentBuffersID = 0; }; /*** CLEAR SCENE ***/ Curtains.prototype._clear = function() { this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); }; /*** FBO ***/ /*** Called to bind or unbind a FBO params: @frameBuffer (frameBuffer): if frameBuffer is not null, bind it, unbind it otherwise @cancelClear (bool / undefined): if we should cancel clearing the frame buffer (typically on init & resize) ***/ Curtains.prototype._bindFrameBuffer = function(frameBuffer, cancelClear) { var bufferId = null; if(frameBuffer) { bufferId = frameBuffer.index; // new frame buffer, bind it if(bufferId !== this._glState.frameBufferID) { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, frameBuffer._frameBuffer); this.gl.viewport(0, 0, frameBuffer._size.width, frameBuffer._size.height); // if we should clear the buffer content if(frameBuffer._shouldClear && !cancelClear) { this._clear(); } } } else if(this._glState.frameBufferID !== null) { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight); } this._glState.frameBufferID = bufferId; }; /*** DEPTH ***/ /*** Called to set whether the renderer will handle depth test or not Depth test is enabled by default params: @setDepth (boolean): if we should enable or disable the depth test ***/ Curtains.prototype._setDepth = function(setDepth) { if(setDepth && !this._glState.depthTest) { this._glState.depthTest = setDepth; // enable depth test this.gl.enable(this.gl.DEPTH_TEST); } else if(!setDepth && this._glState.depthTest) { this._glState.depthTest = setDepth; // disable depth test this.gl.disable(this.gl.DEPTH_TEST); } }; /*** BLEND FUNC ***/ /*** Called to set the blending function (transparency) ***/ Curtains.prototype._setBlendFunc = function() { // allows transparency // based on how three.js solves this var gl = this.gl; gl.enable(gl.BLEND); if(this.premultipliedAlpha) { gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); } else { gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); } }; /*** FACE CULLING ***/ /*** Called to set whether we should cull a plane face or not params: @cullFace (boolean): what face we should cull ***/ Curtains.prototype._setFaceCulling = function(cullFace) { var gl = this.gl; if(this._glState.cullFace !== cullFace) { this._glState.cullFace = cullFace; if(cullFace === "none") { gl.disable(gl.CULL_FACE); } else { // default to back face culling var faceCulling = cullFace === "front" ? gl.FRONT : gl.BACK; gl.enable(gl.CULL_FACE); gl.cullFace(faceCulling); } } }; /*** UTILS ***/ /*** Returns a universally unique identifier ***/ Curtains.prototype._generateUUID = function() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16).toUpperCase(); }); }; /*** MATRICES MATHS ***/ /*** Simple matrix multiplication helper params: @a (array): first matrix @b (array): second matrix returns: @out: matrix after multiplication ***/ Curtains.prototype._multiplyMatrix = function(a, b) { var out = new Float32Array(16); out[0] = b[0]*a[0] + b[1]*a[4] + b[2]*a[8] + b[3]*a[12]; out[1] = b[0]*a[1] + b[1]*a[5] + b[2]*a[9] + b[3]*a[13]; out[2] = b[0]*a[2] + b[1]*a[6] + b[2]*a[10] + b[3]*a[14]; out[3] = b[0]*a[3] + b[1]*a[7] + b[2]*a[11] + b[3]*a[15]; out[4] = b[4]*a[0] + b[5]*a[4] + b[6]*a[8] + b[7]*a[12]; out[5] = b[4]*a[1] + b[5]*a[5] + b[6]*a[9] + b[7]*a[13]; out[6] = b[4]*a[2] + b[5]*a[6] + b[6]*a[10] + b[7]*a[14]; out[7] = b[4]*a[3] + b[5]*a[7] + b[6]*a[11] + b[7]*a[15]; out[8] = b[8]*a[0] + b[9]*a[4] + b[10]*a[8] + b[11]*a[12]; out[9] = b[8]*a[1] + b[9]*a[5] + b[10]*a[9] + b[11]*a[13]; out[10] = b[8]*a[2] + b[9]*a[6] + b[10]*a[10] + b[11]*a[14]; out[11] = b[8]*a[3] + b[9]*a[7] + b[10]*a[11] + b[11]*a[15]; out[12] = b[12]*a[0] + b[13]*a[4] + b[14]*a[8] + b[15]*a[12]; out[13] = b[12]*a[1] + b[13]*a[5] + b[14]*a[9] + b[15]*a[13]; out[14] = b[12]*a[2] + b[13]*a[6] + b[14]*a[10] + b[15]*a[14]; out[15] = b[12]*a[3] + b[13]*a[7] + b[14]*a[11] + b[15]*a[15]; return out; }; /*** Simple matrix scaling helper params : @matrix (array): initial matrix @scaleX (float): scale along X axis @scaleY (float): scale along Y axis @scaleZ (float): scale along Z axis returns : @scaledMatrix: matrix after scaling ***/ Curtains.prototype._scaleMatrix = function(matrix, scaleX, scaleY, scaleZ) { var scaledMatrix = new Float32Array(16); scaledMatrix[0] = scaleX * matrix[0 * 4 + 0]; scaledMatrix[1] = scaleX * matrix[0 * 4 + 1]; scaledMatrix[2] = scaleX * matrix[0 * 4 + 2]; scaledMatrix[3] = scaleX * matrix[0 * 4 + 3]; scaledMatrix[4] = scaleY * matrix[1 * 4 + 0]; scaledMatrix[5] = scaleY * matrix[1 * 4 + 1]; scaledMatrix[6] = scaleY * matrix[1 * 4 + 2]; scaledMatrix[7] = scaleY * matrix[1 * 4 + 3]; scaledMatrix[8] = scaleZ * matrix[2 * 4 + 0]; scaledMatrix[9] = scaleZ * matrix[2 * 4 + 1]; scaledMatrix[10] = scaleZ * matrix[2 * 4 + 2]; scaledMatrix[11] = scaleZ * matrix[2 * 4 + 3]; if(matrix !== scaledMatrix) { scaledMatrix[12] = matrix[12]; scaledMatrix[13] = matrix[13]; scaledMatrix[14] = matrix[14]; scaledMatrix[15] = matrix[15]; } return scaledMatrix; }; /*** Creates a matrix from a quaternion rotation, vector translation and vector scale, rotating and scaling around the given origin Equivalent for applying translation, rotation and scale matrices but much faster Source code from: http://glmatrix.net/docs/mat4.js.html params : @translation (array): translation vector: [X, Y, Z] @quaternion (array): rotation quaternion @scale (array): scale vector: [X, Y, Z] @origin (array): origin vector around which to scale and rotate: [X, Y, Z] returns : @matrix: matrix after transformations ***/ Curtains.prototype._composeMatrixFromOrigin = function(translation, quaternion, scale, origin) { var matrix = new Float32Array(16); // Quaternion math var x = quaternion[0], y = quaternion[1], z = quaternion[2], w = quaternion[3]; var x2 = x + x; var y2 = y + y; var z2 = z + z; var xx = x * x2; var xy = x * y2; var xz = x * z2; var yy = y * y2; var yz = y * z2; var zz = z * z2; var wx = w * x2; var wy = w * y2; var wz = w * z2; var sx = scale.x; var sy = scale.y; var sz = 1; // scale along Z is always equal to 1 var ox = origin.x; var oy = origin.y; var oz = origin.z; var out0 = (1 - (yy + zz)) * sx; var out1 = (xy + wz) * sx; var out2 = (xz - wy) * sx; var out4 = (xy - wz) * sy; var out5 = (1 - (xx + zz)) * sy; var out6 = (yz + wx) * sy; var out8 = (xz + wy) * sz; var out9 = (yz - wx) * sz; var out10 = (1 - (xx + yy)) * sz; matrix[0] = out0; matrix[1] = out1; matrix[2] = out2; matrix[3] = 0; matrix[4] = out4; matrix[5] = out5; matrix[6] = out6; matrix[7] = 0; matrix[8] = out8; matrix[9] = out9; matrix[10] = out10; matrix[11] = 0; matrix[12] = translation.x + ox - (out0 * ox + out4 * oy + out8 * oz); matrix[13] = translation.y + oy - (out1 * ox + out5 * oy + out9 * oz); matrix[14] = translation.z + oz - (out2 * ox + out6 * oy + out10 * oz); matrix[15] = 1; return matrix; }; /*** Apply a matrix 4 to a point (vec3) Useful to convert a point position from plane local world to webgl space using projection view matrix for example Source code from: http://glmatrix.net/docs/vec3.js.html params : @point (array): point to which we apply the matrix @matrix (array): 4x4 matrix used returns : @point: point after matrix application ***/ Curtains.prototype._applyMatrixToPoint = function(point, matrix) { var transformedPoint = []; var x = point[0], y = point[1], z = point[2]; transformedPoint[0] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]; transformedPoint[1] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]; transformedPoint[2] = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]; var w = matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15]; w = w || 1; transformedPoint[0] /= w; transformedPoint[1] /= w; transformedPoint[2] /= w; return transformedPoint; }; /*** DRAW EVERYTHING ***/ /*** This is called when everything is set up and ready to draw It will launch our requestAnimationFrame loop ***/ Curtains.prototype._readyToDraw = function() { // we are ready to go this.container.appendChild(this.glCanvas); // set blend func this._setBlendFunc(); // enable depth by default this._setDepth(true); console.log("curtains.js - v6.1"); this._animationFrameID = null; if(this._autoRender) { this._animate(); } }; /*** This just handles our drawing animation frame ***/ Curtains.prototype._animate = function() { this.render(); this._animationFrameID = window.requestAnimationFrame(this._animate.bind(this)); }; /*** Loop through one of our stack (opaque or transparent objects) and draw its planes ***/ Curtains.prototype._drawPlaneStack = function(stackType) { for(var i = 0; i < this._drawStacks[stackType]["order"].length; i++) { var programID = this._drawStacks[stackType]["order"][i]; var program = this._drawStacks[stackType]["programs"]["program-" + programID]; for(var j = 0; j < program.length; j++) { var plane = this.planes[program[j]]; // be sure the plane exists if(plane) { // draw the plane plane._drawPlane(); } } } }; /*** This is our draw call, ie what has to be called at each frame our our requestAnimationFrame loop draw our planes and shader passes ***/ Curtains.prototype.render = function() { // If _forceRender is true, force rendering this frame even if drawing is not enabled. // If not, only render if enabled. if(!this._drawingEnabled && !this._forceRender) return; // reset _forceRender if(this._forceRender) { this._forceRender = false; } // Curtains onRender callback if(this._onRenderCallback) { this._onRenderCallback(); } // clear scene first this._clear(); // enable first frame buffer for shader passes if(this._drawStacks.scenePasses.length > 0 && this._drawStacks.renderPasses.length === 0) { this._glState.scenePassIndex = 0; this._bindFrameBuffer(this.shaderPasses[this._drawStacks.scenePasses[0]].target); } // loop on our stacked planes this._drawPlaneStack("opaque"); // draw transparent planes if needed if(this._drawStacks["transparent"].length) { // clear our depth buffer to display transparent objects this.gl.clearDepth(1.0); this.gl.clear(this.gl.DEPTH_BUFFER_BIT); this._drawPlaneStack("transparent"); } // now render the shader passes // if we got one or multiple scene passes after the render passes, bind the first scene pass here if(this._drawStacks.scenePasses.length > 0 && this._drawStacks.renderPasses.length > 0) { this._glState.scenePassIndex = 0; this._bindFrameBuffer(this.shaderPasses[this._drawStacks.scenePasses[0]].target); } // first the render passes for(var i = 0; i < this._drawStacks.renderPasses.length; i++) { var renderPass = this.shaderPasses[this._drawStacks.renderPasses[i]]; renderPass._drawPlane(); } // then the scene passes if(this._drawStacks.scenePasses.length > 0) { for(var i = 0; i < this._drawStacks.scenePasses.length; i++) { var scenePass = this.shaderPasses[this._drawStacks.scenePasses[i]]; scenePass._drawPlane(); } } }; /*** EVENTS ***/ /*** This is called each time our container has been resized params : @callback (function) : a function to execute returns : @this: our Curtains element to handle chaining ***/ Curtains.prototype.onAfterResize = function(callback) { if(callback) { this._onAfterResizeCallback = callback; } return this; }; /*** This is called when an error has been detected during init params: @callback (function): a function to execute returns: @this: our Curtains element to handle chaining ***/ Curtains.prototype.onError = function(callback) { if(callback) { this._onErrorCallback = callback; } return this; }; /*** This is called once our context has been lost params: @callback (function): a function to execute returns: @this: our Curtains element to handle chaining ***/ Curtains.prototype.onContextLost = function(callback) { if(callback) { this._onContextLostCallback = callback; } return this; }; /*** This is called once our context has been restored params: @callback (function): a function to execute returns: @this: our Curtains element to handle chaining ***/ Curtains.prototype.onContextRestored = function(callback) { if(callback) { this._onContextRestoredCallback = callback; } return this; }; /*** This is called once at each request animation frame call params: @callback (function): a function to execute returns: @this: our Curtains element to handle chaining ***/ Curtains.prototype.onRender = function(callback) { if(callback) { this._onRenderCallback = callback; } return this; }; /*** This is called each time window is scrolled and if our scrollManager is active params : @callback (function) : a function to execute returns : @this: our Curtains element to handle chaining ***/ Curtains.prototype.onScroll = function(callback) { if(callback) { this._onScrollCallback = callback; } return this; }; /*** BASEPLANE CLASS ***/ /*** Here we create our BasePlane object (note that we are using the Curtains namespace to avoid polluting the global scope) We will create a plane object containing the program, shaders, as well as other useful data Once our shaders are linked to a program, we create their matrices and set up their default attributes params: @curtainWrapper: our curtain object that wraps all the planes @plane (html element): html div that contains 0 or more media elements. @params (obj): see addPlanes method of the wrapper returns: @this: our BasePlane element ***/ Curtains.BasePlane = function(curtainWrapper, plane, params) { this._type = this._type || "BasicPlane"; this._curtains = curtainWrapper; this.htmlElement = plane; this.uuid = this._curtains._generateUUID(); this._initBasePlane(params); }; /*** Init our plane object and its properties params: @params (obj): see addPlanes method of the wrapper returns: @this: our BasePlane element or false if it could not have been created ***/ Curtains.BasePlane.prototype._initBasePlane = function(params) { // if params are not defined if(!params) params = {}; this._canDraw = false; // whether to share programs or not (could enhance performance if a lot of planes use the same shaders) this.shareProgram = params.shareProgram || false; // define if we should update the plane's matrices when called in the draw loop this._updatePerspectiveMatrix = false; this._updateMVMatrix = false; this._definition = { width: parseInt(params.widthSegments) || 1, height: parseInt(params.heightSegments) || 1, }; // unique plane buffers dimensions based on width and height // used to avoid unnecessary buffer bindings during draw loop this._definition.buffersID = this._definition.width * this._definition.height + this._definition.width; // depth test this._depthTest = params.depthTest; if(this._depthTest === null || this._depthTest === undefined) { this._depthTest = true; } // face culling this.cullFace = params.cullFace; if( this.cullFace !== "back" && this.cullFace !== "front" && this.cullFace !== "none" ) { this.cullFace = "back"; } // we will store our active textures in an array this._activeTextures = []; // set up init uniforms if(!params.uniforms) { params.uniforms = {}; } this.uniforms = {}; // create our uniforms objects if(params.uniforms) { for(var key in params.uniforms) { var uniform = params.uniforms[key]; // fill our uniform object this.uniforms[key] = { name: uniform.name, type: uniform.type, value: uniform.value, lastValue: uniform.value, }; } } // first we prepare the shaders to be set up var shaders = this._setupShaders(params); // then we set up the program as compiling can be quite slow this._usedProgram = this._curtains._setupProgram(shaders.vertexShaderCode, shaders.fragmentShaderCode, this); // our object that will handle all medias loading process this._loadingManager = { sourcesLoaded: 0, initSourcesToLoad: 0, // will change if there's any texture to load on init complete: false, }; this.images = []; this.videos = []; this.canvases = []; this.textures = []; this.crossOrigin = params.crossOrigin || "anonymous"; // allow the user to add custom data to the plane this.userData = {}; // if program and shaders are valid, go on if(this._usedProgram) { // should draw is set to true by default, we'll check it later this._shouldDraw = true; // let the user decide whether the plane should be drawn this.visible = true; // set plane attributes this._setAttributes(); // set plane sizes this._setDocumentSizes(); // set our uniforms this._setUniforms(); // set plane definitions, vertices, uvs and stuff this._initializeBuffers(); this._canDraw = true; return this; } else { return false; } }; /*** Get a default vertex shader that does nothing but show the plane ***/ Curtains.BasePlane.prototype._getDefaultVS = function() { if(!this._curtains.productionMode) console.warn("No vertex shader provided, will use a default one"); return "precision mediump float;\nattribute vec3 aVertexPosition;attribute vec2 aTextureCoord;uniform mat4 uMVMatrix;uniform mat4 uPMatrix;varying vec3 vVertexPosition;varying vec2 vTextureCoord;void main() {vTextureCoord = aTextureCoord;vVertexPosition = aVertexPosition;gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);}"; }; /*** Get a default fragment shader that does nothing but draw black pixels ***/ Curtains.BasePlane.prototype._getDefaultFS = function() { return "precision mediump float;\nvarying vec3 vVertexPosition;varying vec2 vTextureCoord;void main( void ) {gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);}"; }; /*** SHADERS CREATIONS ***/ /*** Used internally to set up shaders params: @params (obj): see addPlanes method of the wrapper ***/ Curtains.BasePlane.prototype._setupShaders = function(params) { // handling shaders var vsId = params.vertexShaderID || this.htmlElement.getAttribute("data-vs-id"); var fsId = params.fragmentShaderID || this.htmlElement.getAttribute("data-fs-id"); var vsIdHTML, fsIdHTML; if(!params.vertexShader) { if(!vsId || !document.getElementById(vsId)) { vsIdHTML = this._getDefaultVS(); } else { vsIdHTML = document.getElementById(vsId).innerHTML; } } if(!params.fragmentShader) { if(!fsId || !document.getElementById(fsId)) { if(!this._curtains.productionMode) console.warn("No fragment shader provided, will use a default one"); fsIdHTML = this._getDefaultFS(); } else { fsIdHTML = document.getElementById(fsId).innerHTML; } } return { vertexShaderCode: params.vertexShader || vsIdHTML, fragmentShaderCode: params.fragmentShader || fsIdHTML, } }; /*** PLANE ATTRIBUTES & UNIFORMS ***/ /*** UNIFORMS ***/ /*** This is a little helper to set uniforms based on their types params : @uniformType (string): the uniform type @uniformLocation (WebGLUniformLocation obj): location of the current program uniform @uniformValue (float/integer or array of float/integer): value to set ***/ Curtains.BasePlane.prototype._handleUniformSetting = function(uniformType, uniformLocation, uniformValue) { var gl = this._curtains.gl; switch(uniformType) { case "1i": gl.uniform1i(uniformLocation, uniformValue); break; case "1iv": gl.uniform1iv(uniformLocation, uniformValue); break; case "1f": gl.uniform1f(uniformLocation, uniformValue); break; case "1fv": gl.uniform1fv(uniformLocation, uniformValue); break; case "2i": gl.uniform2i(uniformLocation, uniformValue[0], uniformValue[1]); break; case "2iv": gl.uniform2iv(uniformLocation, uniformValue); break; case "2f": gl.uniform2f(uniformLocation, uniformValue[0], uniformValue[1]); break; case "2fv": gl.uniform2fv(uniformLocation, uniformValue); break; case "3i": gl.uniform3i(uniformLocation, uniformValue[0], uniformValue[1], uniformValue[2]); break; case "3iv": gl.uniform3iv(uniformLocation, uniformValue); break; case "3f": gl.uniform3f(uniformLocation, uniformValue[0], uniformValue[1], uniformValue[2]); break; case "3fv": gl.uniform3fv(uniformLocation, uniformValue); break; case "4i": gl.uniform4i(uniformLocation, uniformValue[0], uniformValue[1], uniformValue[2], uniformValue[3]); break; case "4iv": gl.uniform4iv(uniformLocation, uniformValue); break; case "4f": gl.uniform4f(uniformLocation, uniformValue[0], uniformValue[1], uniformValue[2], uniformValue[3]); break; case "4fv": gl.uniform4fv(uniformLocation, uniformValue); break; case "mat2": gl.uniformMatrix2fv(uniformLocation, false, uniformValue); break; case "mat3": gl.uniformMatrix3fv(uniformLocation, false, uniformValue); break; case "mat4": gl.uniformMatrix4fv(uniformLocation, false, uniformValue); break; default: if(!this._curtains.productionMode) console.warn("This uniform type is not handled : ", uniformType); } }; /*** This sets our shaders uniforms ***/ Curtains.BasePlane.prototype._setUniforms = function() { var curtains = this._curtains; var gl = curtains.gl; // ensure we are using the right program curtains._useProgram(this._usedProgram); // check for program active textures var numUniforms = gl.getProgramParameter(this._usedProgram.program, gl.ACTIVE_UNIFORMS); for(var i = 0; i < numUniforms; i++) { var activeUniform = gl.getActiveUniform(this._usedProgram.program, i); // if it's a texture add it to our activeTextures array if(activeUniform.type === gl.SAMPLER_2D) { this._activeTextures.push(activeUniform); } } // set our uniforms if we got some if(this.uniforms) { for(var key in this.uniforms) { var uniform = this.uniforms[key]; // set our uniform location uniform.location = gl.getUniformLocation(this._usedProgram.program, uniform.name); if(!uniform.type) { if(Array.isArray(uniform.value)) { if(uniform.value.length === 4) { uniform.type = "4f"; if(!curtains.productionMode) console.warn("No uniform type declared for " + uniform.name + ", applied a 4f (array of 4 floats) uniform type"); } else if(uniform.value.length === 3) { uniform.type = "3f"; if(!curtains.productionMode) console.warn("No uniform type declared for " + uniform.name + ", applied a 3f (array of 3 floats) uniform type"); } else if(uniform.value.length === 2) { uniform.type = "2f"; if(!curtains.productionMode) console.warn("No uniform type declared for " + uniform.name + ", applied a 2f (array of 2 floats) uniform type"); } } else if(uniform.value.constructor === Float32Array) { if(uniform.value.length === 16) { uniform.type = "mat4"; if(!curtains.productionMode) console.warn("No uniform type declared for " + uniform.name + ", applied a mat4 (4x4 matrix array) uniform type"); } else if(uniform.value.length === 9) { uniform.type = "mat3"; if(!curtains.productionMode) console.warn("No uniform type declared for " + uniform.name + ", applied a mat3 (3x3 matrix array) uniform type"); } else if(uniform.value.length === 4) { uniform.type = "mat2"; if(!curtains.productionMode) console.warn("No uniform type declared for " + uniform.name + ", applied a mat2 (2x2 matrix array) uniform type"); } } else { uniform.type = "1f"; if(!curtains.productionMode) console.warn("No uniform type declared for " + uniform.name + ", applied a 1f (float) uniform type"); } } // set the uniforms this._handleUniformSetting(uniform.type, uniform.location, uniform.value); } } }; /*** This updates all uniforms of a plane that were set by the user It is called at each draw call ***/ Curtains.BasePlane.prototype._updateUniforms = function() { if(this.uniforms) { for(var key in this.uniforms) { var uniform = this.uniforms[key]; if(!this.shareProgram) { if(!uniform.value.length && uniform.value !== uniform.lastValue) { // update our uniforms this._handleUniformSetting(uniform.type, uniform.location, uniform.value); } else if(JSON.stringify(uniform.value) !== JSON.stringify(uniform.lastValue)) { // compare two arrays // update our uniforms this._handleUniformSetting(uniform.type, uniform.location, uniform.value); } uniform.lastValue = uniform.value; } else { // update our uniforms this._handleUniformSetting(uniform.type, uniform.location, uniform.value); } } } }; /*** ATTRIBUTES ***/ /*** This set our plane vertex shader attributes BE CAREFUL : if an attribute is set here, it MUST be DECLARED and USED inside our plane vertex shader ***/ Curtains.BasePlane.prototype._setAttributes = function() { // set default attributes if(!this._attributes) this._attributes = {}; this._attributes.vertexPosition = { name: "aVertexPosition", location: this._curtains.gl.getAttribLocation(this._usedProgram.program, "aVertexPosition"), }; this._attributes.textureCoord = { name: "aTextureCoord", location: this._curtains.gl.getAttribLocation(this._usedProgram.program, "aTextureCoord"), }; }; /*** PLANE VERTICES AND BUFFERS ***/ /*** This method is used internally to create our vertices coordinates and texture UVs we first create our UVs on a grid from [0, 0, 0] to [1, 1, 0] then we use the UVs to create our vertices coords ***/ Curtains.BasePlane.prototype._setPlaneVertices = function() { // geometry vertices this._geometry = { vertices: [], }; // now the texture UVs coordinates this._material = { uvs: [], }; for(var y = 0; y < this._definition.height; ++y) { var v = y / this._definition.height; for(var x = 0; x < this._definition.width; ++x) { var u = x / this._definition.width; // uvs and vertices // our uvs are ranging from 0 to 1, our vertices range from -1 to 1 // first triangle this._material.uvs.push(u); this._material.uvs.push(v); this._material.uvs.push(0); this._geometry.vertices.push((u - 0.5) * 2); this._geometry.vertices.push((v - 0.5) * 2); this._geometry.vertices.push(0); this._material.uvs.push(u + (1 / this._definition.width)); this._material.uvs.push(v); this._material.uvs.push(0); this._geometry.vertices.push(((u + (1 / this._definition.width)) - 0.5) * 2); this._geometry.vertices.push((v - 0.5) * 2); this._geometry.vertices.push(0); this._material.uvs.push(u); this._material.uvs.push(v + (1 / this._definition.height)); this._material.uvs.push(0); this._geometry.vertices.push((u - 0.5) * 2); this._geometry.vertices.push(((v + (1 / this._definition.height)) - 0.5) * 2); this._geometry.vertices.push(0); // second triangle this._material.uvs.push(u); this._material.uvs.push(v + (1 / this._definition.height)); this._material.uvs.push(0); this._geometry.vertices.push((u - 0.5) * 2); this._geometry.vertices.push(((v + (1 / this._definition.height)) - 0.5) * 2); this._geometry.vertices.push(0); this._material.uvs.push(u + (1 / this._definition.width)); this._material.uvs.push(v); this._material.uvs.push(0); this._geometry.vertices.push(((u + (1 / this._definition.width)) - 0.5) * 2); this._geometry.vertices.push((v - 0.5) * 2); this._geometry.vertices.push(0); this._material.uvs.push(u + (1 / this._definition.width)); this._material.uvs.push(v + (1 / this._definition.height)); this._material.uvs.push(0); this._geometry.vertices.push(((u + (1 / this._definition.width)) - 0.5) * 2); this._geometry.vertices.push(((v + (1 / this._definition.height)) - 0.5) * 2); this._geometry.vertices.push(0); } } }; /*** This method creates our vertex and texture coord buffers ***/ Curtains.BasePlane.prototype._initializeBuffers = function() { var gl = this._curtains.gl; // if this our first time we need to create our geometry and material objects if(!this._geometry && !this._material) { this._setPlaneVertices(); } if(!this._attributes) return; // now we'll create vertices and uvs attributes this._geometry.bufferInfos = { id: gl.createBuffer(), itemSize: 3, numberOfItems: this._geometry.vertices.length / 3, // divided by item size }; this._material.bufferInfos = { id: gl.createBuffer(), itemSize: 3, numberOfItems: this._material.uvs.length / 3, // divided by item size }; // use vertex array objects if available if(this._curtains._isWebGL2) { this._vao = gl.createVertexArray(); gl.bindVertexArray(this._vao); } else if(this._curtains._extensions['OES_vertex_array_object']) { this._vao = this._curtains._extensions['OES_vertex_array_object'].createVertexArrayOES(); this._curtains._extensions['OES_vertex_array_object'].bindVertexArrayOES(this._vao); } // bind both attributes buffers gl.enableVertexAttribArray(this._attributes.vertexPosition.location); gl.bindBuffer(gl.ARRAY_BUFFER, this._geometry.bufferInfos.id); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this._geometry.vertices), gl.STATIC_DRAW); // Set where the vertexPosition attribute gets its data, gl.vertexAttribPointer(this._attributes.vertexPosition.location, this._geometry.bufferInfos.itemSize, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(this._attributes.textureCoord.location); gl.bindBuffer(gl.ARRAY_BUFFER, this._material.bufferInfos.id); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this._material.uvs), gl.STATIC_DRAW); gl.vertexAttribPointer(this._attributes.textureCoord.location, this._material.bufferInfos.itemSize, gl.FLOAT, false, 0, 0); // update current buffers ID this._curtains._glState.currentBuffersID = this._definition.buffersID; }; /*** Used internally handle context restoration ***/ Curtains.BasePlane.prototype._restoreContext = function() { var curtains = this._curtains; this._canDraw = false; if(this._matrices) { this._matrices = null; } this._attributes = null; this._geometry.bufferInfos = null; this._material.bufferInfos = null; // reset the used program based on our previous shaders code strings this._usedProgram = curtains._setupProgram(this._usedProgram.vsCode, this._usedProgram.fsCode, this); if(this._usedProgram) { // reset attributes this._setAttributes(); // reset plane uniforms this._activeTextures = []; this._setUniforms(); // reinitialize buffers this._initializeBuffers(); // handle attached render targets if(this._type === "ShaderPass") { // recreate the render target and its texture if(this._isScenePass) { this.target._frameBuffer = null; this.target._depthBuffer = null; // remove its render target curtains.renderTargets.splice(this.target.index, 1); // remove its render target texture as well this.textures.splice(0, 1); this._createFrameBuffer(); curtains._drawStacks.scenePasses.push(this.index); } else { // set the render target var target = curtains.renderTargets[this.target.index]; this.setRenderTarget(target); this.target._shaderPass = target; // re init render texture from render target texture this.textures[0]._canDraw = false; this.textures[0]._setTextureUniforms(); this.textures[0].setFromTexture(target.textures[0]); curtains._drawStacks.renderPasses.push(this.index); } } else if(this.target) { // reset its render target if needed this.setRenderTarget(curtains.renderTargets[this.target.index]); } // reset textures // we have reinitiated our ShaderPass render target texture above, so skip it for(var i = this._type === "ShaderPass" ? 1 : 0; i < this.textures.length; i++) { this.textures[i]._restoreContext(); } // if this is a Plane object we need to reset its matrices, perspective and position if(this._type === "Plane") { this._initMatrices(); // set our initial perspective matrix this.setPerspective(this._fov, this._nearPlane, this._farPlane); this._applyWorldPositions(); // add the plane to our draw stack again as they have been emptied curtains._stackPlane(this); } this._canDraw = true; } }; /*** PLANE SIZES AND TEXTURES HANDLING ***/ /*** Set our plane dimensions and positions relative to document Triggers reflow! ***/ Curtains.BasePlane.prototype._setDocumentSizes = function() { // set our basic initial infos var planeBoundingRect = this.htmlElement.getBoundingClientRect(); // just in case the html element is missing from the DOM, set its container values instead if(planeBoundingRect.width === 0 && planeBoundingRect.height === 0) { planeBoundingRect = this._curtains._boundingRect; } if(!this._boundingRect) this._boundingRect = {}; // set plane dimensions in document space this._boundingRect.document = { width: planeBoundingRect.width * this._curtains.pixelRatio, height: planeBoundingRect.height * this._curtains.pixelRatio, top: planeBoundingRect.top * this._curtains.pixelRatio, left: planeBoundingRect.left * this._curtains.pixelRatio, }; }; /*** BOUNDING BOXES GETTERS ***/ /*** Useful to get our plane HTML element bounding rectangle without triggering a reflow/layout returns : @boundingRectangle (obj): an object containing our plane HTML element bounding rectangle (width, height, top, bottom, right and left properties) ***/ Curtains.BasePlane.prototype.getBoundingRect = function() { return { width: this._boundingRect.document.width, height: this._boundingRect.document.height, top: this._boundingRect.document.top, left: this._boundingRect.document.left, // right = left + width, bottom = top + height right: this._boundingRect.document.left + this._boundingRect.document.width, bottom: this._boundingRect.document.top + this._boundingRect.document.height, }; }; /*** Get intersection points between a plane and the camera near plane When a plane gets clipped by the camera near plane, the clipped corner projected coords returned by _applyMatrixToPoint() are erronate We need to find the intersection points using another approach Here I chose to use non clipped corners projected coords and a really small vector parallel to the plane's side We're adding that vector again and again to our corner projected coords until the Z coordinate matches the near plane: we got our intersection params: @corners (array): our original corners vertices coordinates @mvpCorners (array): the projected corners of our plane @clippedCorners (array): index of the corners that are clipped returns: @mvpCorners (array): the corrected projected corners of our plane ***/ Curtains.BasePlane.prototype._getNearPlaneIntersections = function(corners, mvpCorners, clippedCorners) { // rebuild the clipped corners based on non clipped ones // find the intersection by adding a vector starting from a corner till we reach the near plane function getIntersection(refPoint, secondPoint) { // direction vector to add var vector = [ secondPoint[0] - refPoint[0], secondPoint[1] - refPoint[1], secondPoint[2] - refPoint[2], ]; // copy our corner refpoint var intersection = refPoint.slice(); // iterate till we reach near plane while(intersection[2] > -1) { intersection[0] += vector[0]; intersection[1] += vector[1]; intersection[2] += vector[2]; } return intersection; } if(clippedCorners.length === 1) { // we will have 5 corners to check so we'll need to push a new entry in our mvpCorners array if(clippedCorners[0] === 0) { // top left is culled // get intersection iterating from top right mvpCorners[0] = getIntersection(mvpCorners[1], this._curtains._applyMatrixToPoint([0.95, 1, 0], this._matrices.mVPMatrix)); // get intersection iterating from bottom left mvpCorners.push(getIntersection(mvpCorners[3], this._curtains._applyMatrixToPoint([-1, -0.95, 0], this._matrices.mVPMatrix))); } else if(clippedCorners[0] === 1) { // top right is culled // get intersection iterating from top left mvpCorners[1] = getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([-0.95, 1, 0], this._matrices.mVPMatrix)); // get intersection iterating from bottom right mvpCorners.push(getIntersection(mvpCorners[2], this._curtains._applyMatrixToPoint([1, -0.95, 0], this._matrices.mVPMatrix))); } else if(clippedCorners[0] === 2) { // bottom right is culled // get intersection iterating from bottom left mvpCorners[2] = getIntersection(mvpCorners[3], this._curtains._applyMatrixToPoint([-0.95, -1, 0], this._matrices.mVPMatrix)); // get intersection iterating from top right mvpCorners.push(getIntersection(mvpCorners[1], this._curtains._applyMatrixToPoint([1, 0.95, 0], this._matrices.mVPMatrix))); } else if(clippedCorners[0] === 3) { // bottom left is culled // get intersection iterating from bottom right mvpCorners[3] = getIntersection(mvpCorners[2], this._curtains._applyMatrixToPoint([0.95, -1, 0], this._matrices.mVPMatrix)); // get intersection iterating from top left mvpCorners.push(getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([-1, 0.95, 0], this._matrices.mVPMatrix))); } } else if(clippedCorners.length === 2) { if(clippedCorners[0] === 0 && clippedCorners[1] === 1) { // top part of the plane is culled by near plane // find intersection using bottom corners mvpCorners[0] = getIntersection(mvpCorners[3], this._curtains._applyMatrixToPoint([-1, -0.95, 0], this._matrices.mVPMatrix)); mvpCorners[1] = getIntersection(mvpCorners[2], this._curtains._applyMatrixToPoint([1, -0.95, 0], this._matrices.mVPMatrix)); } else if(clippedCorners[0] === 1 && clippedCorners[1] === 2) { // right part of the plane is culled by near plane // find intersection using left corners mvpCorners[1] = getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([-0.95, 1, 0], this._matrices.mVPMatrix)); mvpCorners[2] = getIntersection(mvpCorners[3], this._curtains._applyMatrixToPoint([-0.95, -1, 0], this._matrices.mVPMatrix)); } else if(clippedCorners[0] === 2 && clippedCorners[1] === 3) { // bottom part of the plane is culled by near plane // find intersection using top corners mvpCorners[2] = getIntersection(mvpCorners[1], this._curtains._applyMatrixToPoint([1, 0.95, 0], this._matrices.mVPMatrix)); mvpCorners[3] = getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([-1, 0.95, 0], this._matrices.mVPMatrix)); } else if(clippedCorners[0] === 0 && clippedCorners[1] === 3) { // left part of the plane is culled by near plane // find intersection using right corners mvpCorners[0] = getIntersection(mvpCorners[1], this._curtains._applyMatrixToPoint([0.95, 1, 0], this._matrices.mVPMatrix)); mvpCorners[3] = getIntersection(mvpCorners[2], this._curtains._applyMatrixToPoint([0.95, -1, 0], this._matrices.mVPMatrix)); } } else if(clippedCorners.length === 3) { // get the corner that is not clipped var nonClippedCorner = 0; for(var i = 0; i < corners.length; i++) { if(!clippedCorners.includes(i)) { nonClippedCorner = i; } } // we will have just 3 corners so reset our mvpCorners array with just the visible corner mvpCorners = [ mvpCorners[nonClippedCorner] ]; if(nonClippedCorner === 0) { // from top left corner to right mvpCorners.push(getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([-0.95, 1, 0], this._matrices.mVPMatrix))); // from top left corner to bottom mvpCorners.push(getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([-1, 0.95, 0], this._matrices.mVPMatrix))); } else if(nonClippedCorner === 1) { // from top right corner to left mvpCorners.push(getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([0.95, 1, 0], this._matrices.mVPMatrix))); // from top right corner to bottom mvpCorners.push(getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([1, 0.95, 0], this._matrices.mVPMatrix))); } else if(nonClippedCorner === 2) { // from bottom right corner to left mvpCorners.push(getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([0.95, -1, 0], this._matrices.mVPMatrix))); // from bottom right corner to top mvpCorners.push(getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([1, -0.95, 0], this._matrices.mVPMatrix))); } else if(nonClippedCorner === 3) { // from bottom left corner to right mvpCorners.push(getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([-0.95, -1, 0], this._matrices.mVPMatrix))); // from bottom left corner to top mvpCorners.push(getIntersection(mvpCorners[0], this._curtains._applyMatrixToPoint([-1, -0.95, 0], this._matrices.mVPMatrix))); } } else { // all 4 corners are culled! artificially apply wrong coords to force plane culling for(var i = 0; i < corners.length; i++) { mvpCorners[i][0] = 10000; mvpCorners[i][1] = 10000; } } return mvpCorners; }; /*** Useful to get our WebGL plane bounding box in the world space Takes all transformations into account Used internally for frustum culling returns : @boundingRectangle (obj): an object containing our plane WebGL element 4 corners coordinates: top left corner is [-1, 1] and bottom right corner is [1, -1] ***/ Curtains.BasePlane.prototype._getWorldCoords = function() { var corners = [ [-1, 1, 0], // plane's top left corner [1, 1, 0], // plane's top right corner [1, -1, 0], // plane's bottom right corner [-1, -1, 0], // plane's bottom left corner ]; // corners with model view projection matrix applied var mvpCorners = []; // eventual clipped corners var clippedCorners = []; // we are going to get our plane's four corners relative to our model view projection matrix for(var i = 0; i < corners.length; i++) { var mvpCorner = this._curtains._applyMatrixToPoint(corners[i], this._matrices.mVPMatrix); mvpCorners.push(mvpCorner); // Z position is > 1 or < -1 means the corner is clipped if(Math.abs(mvpCorner[2]) > 1) { clippedCorners.push(i); } } // near plane is clipping, get intersections between plane and near plane if(clippedCorners.length) { mvpCorners = this._getNearPlaneIntersections(corners, mvpCorners, clippedCorners); } // we need to check for the X and Y min and max values // use arbitrary integers that will be overrided anyway var minX = Infinity; var maxX = -Infinity; var minY = Infinity; var maxY = -Infinity; for(var i = 0; i < mvpCorners.length; i++) { var corner = mvpCorners[i]; if(corner[0] < minX) { minX = corner[0]; } if(corner[0] > maxX) { maxX = corner[0]; } if(corner[1] < minY) { minY = corner[1]; } if(corner[1] > maxY) { maxY = corner[1]; } } return { top: maxY, right: maxX, bottom: minY, left: minX, }; }; /*** Useful to get our WebGL plane bounding box relative to the document Takes all transformations into account Used internally for frustum culling returns : @boundingRectangle (obj): an object containing our plane WebGL element bounding rectangle (width, height, top, bottom, right and left properties) ***/ Curtains.BasePlane.prototype.getWebGLBoundingRect = function() { // check that our view projection matrix is defined if(this._matrices.mVPMatrix) { // get our world space bouding rect var worldBBox = this._getWorldCoords(); // normalize worldBBox to (0 -> 1) screen coordinates with [0, 0] being the top left corner and [1, 1] being the bottom right var screenBBox = { top: 1 - (worldBBox.top + 1) / 2, right: (worldBBox.right + 1) / 2, bottom: 1 - (worldBBox.bottom + 1) / 2, left: (worldBBox.left + 1) / 2, }; screenBBox.width = screenBBox.right - screenBBox.left; screenBBox.height = screenBBox.bottom - screenBBox.top; // return our values ranging from 0 to 1 multiplied by our canvas sizes + canvas top and left offsets return { width: screenBBox.width * this._curtains._boundingRect.width, height: screenBBox.height * this._curtains._boundingRect.height, top: screenBBox.top * this._curtains._boundingRect.height + this._curtains._boundingRect.top, left: screenBBox.left * this._curtains._boundingRect.width + this._curtains._boundingRect.left, // add left and width to get right property right: screenBBox.left * this._curtains._boundingRect.width + this._curtains._boundingRect.left + screenBBox.width * this._curtains._boundingRect.width, // add top and height to get bottom property bottom: screenBBox.top * this._curtains._boundingRect.height + this._curtains._boundingRect.top + screenBBox.height * this._curtains._boundingRect.height, }; } else { return this._boundingRect.document; } }; /*** Returns our plane WebGL bounding rectangle in document coordinates including additional drawCheckMargins returns : @boundingRectangle (obj): an object containing our plane WebGL element bounding rectangle including the draw check margins (top, bottom, right and left properties) ***/ Curtains.BasePlane.prototype._getWebGLDrawRect = function() { var boundingRect = this.getWebGLBoundingRect(); return { top: boundingRect.top - this.drawCheckMargins.top, right: boundingRect.right + this.drawCheckMargins.right, bottom: boundingRect.bottom + this.drawCheckMargins.bottom, left: boundingRect.left - this.drawCheckMargins.left, }; }; /*** Handles each plane resizing used internally when our container is resized ***/ Curtains.BasePlane.prototype.planeResize = function() { // reset plane dimensions this._setDocumentSizes(); // if this is a Plane object we need to update its perspective and positions if(this._type === "Plane") { // reset perspective this.setPerspective(this._fov, this._nearPlane, this._farPlane); // apply new position this._applyWorldPositions(); } // resize all textures for(var i = 0; i < this.textures.length; i++) { this.textures[i].resize(); } // handle our after resize event var self = this; setTimeout(function() { if(self._onAfterResizeCallback) { self._onAfterResizeCallback(); } }, 0); }; /*** IMAGES, VIDEOS AND CANVASES LOADING ***/ /*** This method creates a new Texture associated to the plane params : @type (string) : texture type, either image, video or canvas returns : @t: our newly created texture ***/ Curtains.BasePlane.prototype.createTexture = function(params) { if(typeof params === "string") { params = { sampler: params, }; if(!this._curtains.productionMode) { console.warn("Since v5.1 you should use an object to pass your sampler name with the createTexture() method. Please refer to the docs: https://www.curtainsjs.com/documentation.html (texture concerned: ", params.sampler, ")"); } } if(!params) params = {}; var texture = new Curtains.Texture(this, { index: this.textures.length, sampler: params.sampler || null, fromTexture: params.fromTexture || null, isFBOTexture: params.isFBOTexture || false, // used internally }); // add our texture to the textures array this.textures.push(texture); return texture; }; /*** Check whether a plane has loaded all its initial sources and fires the onReady callback params : @sourcesArray (array) : array of html images, videos or canvases elements ***/ Curtains.BasePlane.prototype._isPlaneReady = function() { if(!this._loadingManager.complete && this._loadingManager.sourcesLoaded >= this._loadingManager.initSourcesToLoad) { this._loadingManager.complete = true; // force next frame rendering this._curtains.needRender(); var self = this; setTimeout(function() { if(self._onReadyCallback) { self._onReadyCallback(); } }, 0); } }; /*** This method handles the sources loading process params : @sourcesArray (array) : array of html images, videos or canvases elements ***/ Curtains.BasePlane.prototype.loadSources = function(sourcesArray) { for(var i = 0; i < sourcesArray.length; i++) { this.loadSource(sourcesArray[i]); } }; /*** This method loads one source It checks what type of source it is then use the right loader params : @source (html element) : html image, video or canvas element ***/ Curtains.BasePlane.prototype.loadSource = function(source) { if(source.tagName.toUpperCase() === "IMG") { this.loadImage(source); } else if(source.tagName.toUpperCase() === "VIDEO") { this.loadVideo(source); } else if(source.tagName.toUpperCase() === "CANVAS") { this.loadCanvas(source); } else if(!this._curtains.productionMode) { console.warn("this HTML tag could not be converted into a texture:", source.tagName); } }; /*** Handles media loading errors params : @source (html element) : html image, video or canvas element @error (object) : loading error ***/ Curtains.BasePlane.prototype._sourceLoadError = function(source, error) { if(!this._curtains.productionMode) { console.warn("There has been an error:", error, "while loading this source:", source); } }; /*** Check if this source is already assigned to a cached texture params : @source (html element) : html image, video or canvas element (only images for now) ***/ Curtains.BasePlane.prototype._getTextureFromCache = function(source) { var cachedTexture = false; if(this._curtains._imageCache.length > 0) { for(var i = 0; i < this._curtains._imageCache.length; i++) { var cacheTextureItem = this._curtains._imageCache[i]; if(cacheTextureItem.source) { if(cacheTextureItem.type === "image" && cacheTextureItem.source.src === source.src) { cachedTexture = cacheTextureItem; } } } } return cachedTexture; }; /*** This method loads an image Creates a new texture object right away and once the image is loaded it uses it as our WebGL texture params : @source (image) : html image element ***/ Curtains.BasePlane.prototype.loadImage = function(source) { var image = source; image.crossOrigin = this.crossOrigin || "anonymous"; image.sampler = source.getAttribute("data-sampler") || null; // check for cache var cachedTexture = this._getTextureFromCache(source); if(cachedTexture) { this.createTexture({ sampler: image.sampler, fromTexture: cachedTexture, }); this.images.push(cachedTexture.source); // fire parent plane onReady callback if needed this._isPlaneReady(); return; } // create a new texture that will use our image later var texture = this.createTexture({ sampler: image.sampler, }); // handle our loaded data event inside the texture and tell our plane when the video is ready to play texture._onSourceLoadedHandler = texture._onSourceLoaded.bind(texture, image); // If the image is in the cache of the browser, // the 'load' event might have been triggered // before we registered the event handler. if(image.complete) { texture._onSourceLoaded(image); } else if(image.decode) { var self = this; image.decode().then(texture._onSourceLoadedHandler).catch(function() { // fallback to classic load & error events image.addEventListener('load', texture._onSourceLoadedHandler, false); image.addEventListener('error', self._sourceLoadError.bind(self, image), false); }); } else { image.addEventListener('load', texture._onSourceLoadedHandler, false); image.addEventListener('error', this._sourceLoadError.bind(this, image), false); } // add the image to our array this.images.push(image); }; /*** This method loads a video Creates a new texture object right away and once the video has enough data it uses it as our WebGL texture params : @source (video) : html video element ***/ Curtains.BasePlane.prototype.loadVideo = function(source) { var video = source; video.preload = true; video.muted = true; video.loop = true; video.sampler = source.getAttribute("data-sampler") || null; video.crossOrigin = this.crossOrigin || "anonymous"; // create a new texture that will use our video later var texture = this.createTexture({ sampler: video.sampler }); // handle our loaded data event inside the texture and tell our plane when the video is ready to play texture._onSourceLoadedHandler = texture._onVideoLoadedData.bind(texture, video); video.addEventListener('canplaythrough', texture._onSourceLoadedHandler, false); video.addEventListener('error', this._sourceLoadError.bind(this, video), false); // If the video is in the cache of the browser, // the 'canplaythrough' event might have been triggered // before we registered the event handler. if(video.readyState >= video.HAVE_FUTURE_DATA) { texture._onSourceLoaded(video); } // start loading our video video.load(); this.videos.push(video); }; /*** This method loads a canvas Creates a new texture object right away and uses the canvas as our WebGL texture params : @source (canvas) : html canvas element ***/ Curtains.BasePlane.prototype.loadCanvas = function(source) { var canvas = source; canvas.sampler = source.getAttribute("data-sampler") || null; var texture = this.createTexture({ sampler: canvas.sampler }); this.canvases.push(canvas); texture._onSourceLoaded(canvas); }; /*** LOAD ARRAYS ***/ /*** Loads an array of images params : @imagesArray (array) : array of html image elements returns : @this: our plane to handle chaining ***/ Curtains.BasePlane.prototype.loadImages = function(imagesArray) { for(var i = 0; i < imagesArray.length; i++) { this.loadImage(imagesArray[i]); } }; /*** Loads an array of videos params : @videosArray (array) : array of html video elements returns : @this: our plane to handle chaining ***/ Curtains.BasePlane.prototype.loadVideos = function(videosArray) { for(var i = 0; i < videosArray.length; i++) { this.loadVideo(videosArray[i]); } }; /*** Loads an array of canvases params : @canvasesArray (array) : array of html canvas elements returns : @this: our plane to handle chaining ***/ Curtains.BasePlane.prototype.loadCanvases = function(canvasesArray) { for(var i = 0; i < canvasesArray.length; i++) { this.loadCanvas(canvasesArray[i]); } }; /*** This has to be called in order to play the planes videos We need this because on mobile devices we can't start playing a video without a user action Once the video has started playing we set an interval and update a new frame to our our texture at a 30FPS rate ***/ Curtains.BasePlane.prototype.playVideos = function() { for(var i = 0; i < this.textures.length; i++) { var texture = this.textures[i]; if(texture.type === "video") { var playPromise = texture.source.play(); // In browsers that don’t yet support this functionality, // playPromise won’t be defined. var self = this; if(playPromise !== undefined) { playPromise.catch(function(error) { if(!self._curtains.productionMode) console.warn("Could not play the video : ", error); }); } } } }; /*** INTERACTION ***/ /*** This function takes the mouse position relative to the document and returns it relative to our plane It ranges from -1 to 1 on both axis params : @xPosition (float): position to convert on X axis @yPosition (float): position to convert on Y axis returns : @mousePosition: the mouse position relative to our plane in WebGL space coordinates ***/ Curtains.BasePlane.prototype.mouseToPlaneCoords = function(xMousePosition, yMousePosition) { // remember our ShaderPass objects don't have a scale property var scale = this.scale ? this.scale : {x: 1, y: 1}; // we need to adjust our plane document bounding rect to it's webgl scale var scaleAdjustment = { x: (this._boundingRect.document.width - this._boundingRect.document.width * scale.x) / 2, y: (this._boundingRect.document.height - this._boundingRect.document.height * scale.y) / 2, }; // also we need to divide by pixel ratio var planeBoundingRect = { width: (this._boundingRect.document.width * scale.x) / this._curtains.pixelRatio, height: (this._boundingRect.document.height * scale.y) / this._curtains.pixelRatio, top: (this._boundingRect.document.top + scaleAdjustment.y) / this._curtains.pixelRatio, left: (this._boundingRect.document.left + scaleAdjustment.x) / this._curtains.pixelRatio, }; // mouse position conversion from document to plane space var mousePosition = { x: (((xMousePosition - planeBoundingRect.left) / planeBoundingRect.width) * 2) - 1, y: 1 - (((yMousePosition - planeBoundingRect.top) / planeBoundingRect.height) * 2) }; return mousePosition; }; /*** Used inside our draw call to set the correct plane buffers before drawing it ***/ Curtains.BasePlane.prototype._bindPlaneBuffers = function() { var curtains = this._curtains; var gl = curtains.gl; if(this._vao) { if(curtains._isWebGL2) { curtains.gl.bindVertexArray(this._vao); } else { curtains._extensions['OES_vertex_array_object'].bindVertexArrayOES(this._vao); } } else { // Set the vertices buffer gl.enableVertexAttribArray(this._attributes.vertexPosition.location); gl.bindBuffer(gl.ARRAY_BUFFER, this._geometry.bufferInfos.id); gl.vertexAttribPointer(this._attributes.vertexPosition.location, this._geometry.bufferInfos.itemSize, gl.FLOAT, false, 0, 0); // Set where the texture coord attribute gets its data, gl.enableVertexAttribArray(this._attributes.textureCoord.location); gl.bindBuffer(gl.ARRAY_BUFFER, this._material.bufferInfos.id); gl.vertexAttribPointer(this._attributes.textureCoord.location, this._material.bufferInfos.itemSize, gl.FLOAT, false, 0, 0); } // update current buffers ID curtains._glState.currentBuffersID = this._definition.buffersID; }; /*** This is used to set the WebGL context active texture and bind it params : @texture (texture object) : Our texture object containing our WebGL texture and its index ***/ Curtains.BasePlane.prototype._bindPlaneTexture = function(texture) { var gl = this._curtains.gl; if(texture._canDraw) { // tell WebGL we want to affect the texture at the plane's index unit gl.activeTexture(gl.TEXTURE0 + texture.index); // bind the texture to the plane's index unit gl.bindTexture(gl.TEXTURE_2D, texture._sampler.texture); } }; /*** This function adds a render target to a plane params : @renderTarger (RenderTarget): the render target to add to that plane ***/ Curtains.BasePlane.prototype.setRenderTarget = function(renderTarget) { if(!renderTarget || !renderTarget._type || renderTarget._type !== "RenderTarget") { if(!this._curtains.productionMode) { console.warn("Could not set the render target because the argument passed is not a RenderTarget class object", renderTarget); } return; } this.target = renderTarget; }; /*** DRAW THE PLANE ***/ /*** We draw the plane, ie bind the buffers, set the active textures and draw it If the plane type is a ShaderPass we also need to bind the right frame buffers ***/ Curtains.BasePlane.prototype._drawPlane = function() { var curtains = this._curtains; var gl = curtains.gl; // check if our plane is ready to draw if(this._canDraw) { // even if our plane should not be drawn we still execute its onRender callback and update its uniforms if(this._onRenderCallback) { this._onRenderCallback(); } // to improve webgl pipeline performace, we might want to update each texture that needs an update here // see https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#texImagetexSubImage_uploads_particularly_with_videos_can_cause_pipeline_flushes if(this._type === "ShaderPass") { if(this._isScenePass) { // if this is a scene pass, check if theres one more coming next and eventually bind it if(curtains._glState.scenePassIndex + 1 < curtains._drawStacks.scenePasses.length) { curtains._bindFrameBuffer(curtains.shaderPasses[curtains._drawStacks.scenePasses[curtains._glState.scenePassIndex + 1]].target); curtains._glState.scenePassIndex++; } else { curtains._bindFrameBuffer(null); } } else if(curtains._glState.scenePassIndex === null) { // we are rendering a bunch of planes inside a render target, unbind it curtains._bindFrameBuffer(null); } } else { // if we should render to a render target if(this.target) { curtains._bindFrameBuffer(this.target); } else if(curtains._glState.scenePassIndex === null) { curtains._bindFrameBuffer(null); } // update our perspective matrix this._setPerspectiveMatrix(); // update our mv matrix this._setMVMatrix(); } // now check if we really need to draw it and its textures if((this.alwaysDraw || this._shouldDraw) && this.visible) { // enable/disable depth test curtains._setDepth(this._depthTest); // face culling curtains._setFaceCulling(this.cullFace); // ensure we're using the right program curtains._useProgram(this._usedProgram); // update all uniforms set up by the user this._updateUniforms(); // bind plane attributes buffers // if we're rendering on a frame buffer object, force buffers bindings to avoid bugs if(curtains._glState.currentBuffersID !== this._definition.buffersID || this.target) { this._bindPlaneBuffers(); } // draw all our plane textures for(var i = 0; i < this.textures.length; i++) { // draw (bind and maybe update) our texture this.textures[i]._drawTexture(); } // the draw call! gl.drawArrays(gl.TRIANGLES, 0, this._geometry.bufferInfos.numberOfItems); // callback after draw if(this._onAfterRenderCallback) { this._onAfterRenderCallback(); } } } }; /*** This deletes all our plane webgl bindings and its textures ***/ Curtains.BasePlane.prototype._dispose = function() { var gl = this._curtains.gl; if(gl) { // delete buffers // each time we check for existing properties to avoid errors if(this._vao) { if(this._curtains._isWebGL2) { gl.deleteVertexArray(this._vao); } else { this._curtains._extensions['OES_vertex_array_object'].deleteVertexArrayOES(this._vao); } } if(this._geometry) { gl.bindBuffer(gl.ARRAY_BUFFER, this._geometry.bufferInfos.id); gl.bufferData(gl.ARRAY_BUFFER, 1, gl.STATIC_DRAW); gl.deleteBuffer(this._geometry.bufferInfos.id); this._geometry = null; } if(this._material) { gl.bindBuffer(gl.ARRAY_BUFFER, this._material.bufferInfos.id); gl.bufferData(gl.ARRAY_BUFFER, 1, gl.STATIC_DRAW); gl.deleteBuffer(this._material.bufferInfos.id); this._material = null; } if(this.target && this._type === "ShaderPass") { this._curtains.removeRenderTarget(this.target); // remove the first texture since it has been deleted with the render target this.textures.shift(); } // unbind and delete the textures for(var i = 0; i < this.textures.length; i++) { this.textures[i]._dispose(); } this.textures = null; } }; /*** BASE PLANE EVENTS ***/ /*** This is called each time a plane has been resized params : @callback (function) : a function to execute returns : @this: our plane to handle chaining ***/ Curtains.BasePlane.prototype.onAfterResize = function(callback) { if(callback) { this._onAfterResizeCallback = callback; } return this; }; /*** This is called each time a plane's image has been loaded. Useful to handle a loader params : @callback (function) : a function to execute returns : @this: our plane to handle chaining ***/ Curtains.BasePlane.prototype.onLoading = function(callback) { if(callback) { this._onPlaneLoadingCallback = callback; } return this; }; /*** This is called when a plane is ready to be drawn params : @callback (function) : a function to execute returns : @this: our plane to handle chaining ***/ Curtains.BasePlane.prototype.onReady = function(callback) { if(callback) { this._onReadyCallback = callback; } return this; }; /*** This is called at each requestAnimationFrame call params : @callback (function) : a function to execute returns : @this: our plane to handle chaining ***/ Curtains.BasePlane.prototype.onRender = function(callback) { if(callback) { this._onRenderCallback = callback; } return this; }; /*** This is called at each requestAnimationFrame call for each plane after the draw call params : @callback (function) : a function to execute returns : @this: our plane to handle chaining ***/ Curtains.BasePlane.prototype.onAfterRender = function(callback) { if(callback) { this._onAfterRenderCallback = callback; } return this; }; /*** PLANE CLASS ***/ /*** Here we create our Plane object (note that we are using the Curtains namespace to avoid polluting the global scope) It will inherits from ou BasePlane class that handles all the WebGL part Plane class will add: - sizing and positioning and everything that relates to the DOM like draw checks and reenter/leave events - projection and view matrices and everything that is related like perspective, scale, rotation... - sources auto loading and onReady callback - depth related things params : @curtainWrapper : our curtain object that wraps all the planes @plane (html element) : html div that contains 0 or more media elements. @params (obj) : see addPlanes method of the wrapper returns : @this: our Plane element ***/ Curtains.Plane = function(curtainWrapper, plane, params) { this._type = "Plane"; // inherit Curtains.BasePlane.call(this, curtainWrapper, plane, params); this.index = this._curtains.planes.length; this._canDraw = false; // used for FBOs this.target = null; // if params is not defined if(!params) params = {}; this._setInitParams(params); // if program is valid, go on if(this._usedProgram) { // add our plane to the draw stack this._curtains._stackPlane(this); // init our plane this._initPositions(); this._initSources(); } else { if(this._curtains._onErrorCallback) { // if it's not valid call the curtains error callback this._curtains._onErrorCallback(); } } }; Curtains.Plane.prototype = Object.create(Curtains.BasePlane.prototype); Curtains.Plane.prototype.constructor = Curtains.Plane; /*** Set plane's initial params params : @params (obj) : see addPlanes method of the Curtains class ***/ Curtains.Plane.prototype._setInitParams = function(params) { // if our plane should always be drawn or if it should be drawn only when inside the viewport (frustum culling) this.alwaysDraw = params.alwaysDraw || false; // if the plane has transparency this._transparent = params.transparent || false; // draw check margins in pixels // positive numbers means it can be displayed even when outside the viewport // negative numbers means it can be hidden even when inside the viewport var drawCheckMargins = { top: 0, right: 0, bottom: 0, left: 0, }; if(params.drawCheckMargins) { drawCheckMargins = params.drawCheckMargins; } this.drawCheckMargins = drawCheckMargins; this._initTransformValues(); // if we decide to load all sources on init or let the user do it manually this.autoloadSources = params.autoloadSources; if(this.autoloadSources === null || this.autoloadSources === undefined) { this.autoloadSources = true; } // set default fov this._fov = params.fov || 50; this._nearPlane = 0.1; this._farPlane = 150; // if we should watch scroll if(params.watchScroll === null || params.watchScroll === undefined) { this.watchScroll = this._curtains._watchScroll; } else { this.watchScroll = params.watchScroll || false; } // start listening for scroll if(this.watchScroll) { this._curtains._scrollManager.shouldWatch = true; } }; /*** Set/reset plane's transformation values: rotation, scale, translation, transform origin ***/ Curtains.Plane.prototype._initTransformValues = function() { this.rotation = { x: 0, y: 0, z: 0, }; // initial quaternion this.quaternion = new Float32Array([0, 0, 0, 1]); this.relativeTranslation = { x: 0, y: 0, z: 0, }; // will be our translation in webgl coordinates this._translation = { x: 0, y: 0, z: 0 }; this.scale = { x: 1, y: 1, }; // set plane transform origin to center this.transformOrigin = { x: 0.5, y: 0.5, z: 0, }; }; /*** Init our plane position: set its matrices, its position and perspective ***/ Curtains.Plane.prototype._initPositions = function() { // set its matrices this._initMatrices(); // set our initial perspective matrix this.setPerspective(this._fov, this._nearPlane, this._farPlane); // apply our css positions this._applyWorldPositions(); }; /*** Load our initial sources if needed and calls onReady callback ***/ Curtains.Plane.prototype._initSources = function() { // finally load every sources already in our plane html element // load plane sources if(this.autoloadSources) { // load images var imagesArray = []; for(var i = 0; i < this.htmlElement.getElementsByTagName("img").length; i++) { imagesArray.push(this.htmlElement.getElementsByTagName("img")[i]); } if(imagesArray.length > 0) { this.loadSources(imagesArray); } // load videos var videosArray = []; for(var i = 0; i < this.htmlElement.getElementsByTagName("video").length; i++) { videosArray.push(this.htmlElement.getElementsByTagName("video")[i]); } if(videosArray.length > 0) { this.loadSources(videosArray); } // load canvases var canvasesArray = []; for(var i = 0; i < this.htmlElement.getElementsByTagName("canvas").length; i++) { canvasesArray.push(this.htmlElement.getElementsByTagName("canvas")[i]); } if(canvasesArray.length > 0) { this.loadSources(canvasesArray); } this._loadingManager.initSourcesToLoad = imagesArray.length + videosArray.length + canvasesArray.length; } if(this._loadingManager.initSourcesToLoad === 0) { // onReady callback this._isPlaneReady(); if(!this._curtains.productionMode) { // if there's no images, no videos, no canvas, send a warning console.warn("This plane does not contain any image, video or canvas element. You may want to add some later with the loadSource() or loadSources() method."); } } this._canDraw = true; // be sure we'll update the scene even if drawing is disabled this._curtains.needRender(); // everything is ready, check if we should draw the plane if(!this.alwaysDraw) { this._shouldDrawCheck(); } }; /*** Init our plane model view and projection matrices and set their uniform locations ***/ Curtains.Plane.prototype._initMatrices = function() { var gl = this._curtains.gl; // projection and model view matrix // create our modelview and projection matrix this._matrices = { mvMatrix: { name: "uMVMatrix", matrix: new Float32Array([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]), location: gl.getUniformLocation(this._usedProgram.program, "uMVMatrix"), }, pMatrix: { name: "uPMatrix", matrix: new Float32Array([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]), // will be set after location: gl.getUniformLocation(this._usedProgram.program, "uPMatrix"), } }; }; /*** Reset our plane transformation values and HTML element if specified (and valid) params : @htmlElement (HTML element, optionnal) : if provided, new HTML element to use as a reference for sizes and position syncing. ***/ Curtains.Plane.prototype.resetPlane = function(htmlElement) { this._initTransformValues(); if(htmlElement !== null && !!htmlElement) { this.htmlElement = htmlElement; this.updatePosition(); } else if(!htmlElement && !this._curtains.productionMode) { console.warn("You are trying to reset a plane with a HTML element that does not exist. The old HTML element will be kept instead."); } }; /*** Set our plane dimensions and positions relative to clip spaces ***/ Curtains.Plane.prototype._setWorldSizes = function() { var curtains = this._curtains; // dimensions and positions of our plane in the document and clip spaces // don't forget translations in webgl space are referring to the center of our plane and canvas var planeCenter = { x: (this._boundingRect.document.width / 2) + this._boundingRect.document.left, y: (this._boundingRect.document.height / 2) + this._boundingRect.document.top, }; var curtainsCenter = { x: (curtains._boundingRect.width / 2) + curtains._boundingRect.left, y: (curtains._boundingRect.height / 2) + curtains._boundingRect.top, }; // our plane clip space informations this._boundingRect.world = { width: this._boundingRect.document.width / curtains._boundingRect.width, height: this._boundingRect.document.height / curtains._boundingRect.height, top: (curtainsCenter.y - planeCenter.y) / curtains._boundingRect.height, left: (planeCenter.x - curtainsCenter.x) / curtains._boundingRect.height, }; // since our vertices values range from -1 to 1 // we need to scale them under the hood relatively to our canvas // to display an accurately sized plane this._boundingRect.world.scale = { x: (this._curtains._boundingRect.width / this._curtains._boundingRect.height) * this._boundingRect.world.width / 2, y: this._boundingRect.world.height / 2, }; }; /*** PLANES PERSPECTIVES, SCALES AND ROTATIONS ***/ /*** This will set our perspective matrix and update our perspective matrix uniform used internally at each draw call if needed ***/ Curtains.Plane.prototype._setPerspectiveMatrix = function() { if(this._updatePerspectiveMatrix) { var aspect = this._curtains._boundingRect.width / this._curtains._boundingRect.height; var top = this._nearPlane * Math.tan((Math.PI / 180) * 0.5 * this._fov); var height = 2 * top; var width = aspect * height; var left = -0.5 * width; var right = left + width; var bottom = top - height; var x = 2 * this._nearPlane / (right - left); var y = 2 * this._nearPlane / (top - bottom); var a = (right + left) / (right - left); var b = (top + bottom) / (top - bottom); var c = -(this._farPlane + this._nearPlane) / (this._farPlane - this._nearPlane); var d = -2 * this._farPlane * this._nearPlane / (this._farPlane - this._nearPlane); this._matrices.pMatrix.matrix = new Float32Array([ x, 0, 0, 0, 0, y, 0, 0, a, b, c, -1, 0, 0, d, 0 ]); } // update our matrix uniform only if we share programs or if we actually have updated its values if(this.shareProgram || !this.shareProgram && this._updatePerspectiveMatrix) { this._curtains._useProgram(this._usedProgram); this._curtains.gl.uniformMatrix4fv(this._matrices.pMatrix.location, false, this._matrices.pMatrix.matrix); } this._updatePerspectiveMatrix = false; }; /*** This will set our perspective matrix new parameters (fov, near plane and far plane) used internally but can be used externally as well to change fov for example params : @fov (float): the field of view @near (float): the nearest point where object are displayed @far (float): the farthest point where object are displayed ***/ Curtains.Plane.prototype.setPerspective = function(fov, near, far) { var fieldOfView = isNaN(fov) ? this._fov : parseFloat(fov); // clamp between 1 and 179 fieldOfView = Math.max(1, Math.min(fieldOfView, 179)); if(fieldOfView !== this._fov) { this._fov = fieldOfView; } // update the camera position anyway this._cameraZPosition = Math.tan((Math.PI / 180) * 0.5 * this._fov) * 2.0; // corresponding CSS perspective property value depending on canvas size and fov values // based on https://stackoverflow.com/questions/22421439/convert-field-of-view-value-to-css3d-perspective-value this._CSSPerspective = Math.pow(Math.pow(this._curtains._boundingRect.width / (2 * this._curtains.pixelRatio), 2) + Math.pow(this._curtains._boundingRect.height / (2 * this._curtains.pixelRatio), 2), 0.5) / Math.tan((this._fov / 2) * Math.PI / 180); // near plane this._nearPlane = isNaN(near) ? this._nearPlane : parseFloat(near); this._nearPlane = Math.max(this._nearPlane, 0.01); // far plane this._farPlane = isNaN(far) ? this._farPlane : parseFloat(far); this._farPlane = Math.max(this._farPlane, 50); // update the plane perspective matrix this._updatePerspectiveMatrix = true; // update the mvMatrix as well cause we need to update z translation based on new fov this._updateMVMatrix = true; }; /*** This will set our model view matrix used internally at each draw call if needed It will calculate our matrix based on its plane translation, rotation and scale ***/ Curtains.Plane.prototype._setMVMatrix = function() { if(this._updateMVMatrix) { // translation // along the Z axis it's based on the relativeTranslation.z, CSSPerspective and cameraZPosition values // we're computing it here because it will change when our fov changes this._translation.z = this.relativeTranslation.z / this._CSSPerspective; var translation = { x: this._translation.x, y: this._translation.y, z: -((1 - this._translation.z) / this._cameraZPosition), }; var adjustedOrigin = { x: this.transformOrigin.x * 2 - 1, // between -1 and 1 y: -(this.transformOrigin.y * 2 - 1), // between -1 and 1 }; var origin = { x: adjustedOrigin.x * this._boundingRect.world.scale.x, y: adjustedOrigin.y * this._boundingRect.world.scale.y, z: this.transformOrigin.z }; var matrixFromOrigin = this._curtains._composeMatrixFromOrigin(translation, this.quaternion, this.scale, origin); var scaleMatrix = new Float32Array([ this._boundingRect.world.scale.x, 0.0, 0.0, 0.0, 0.0, this._boundingRect.world.scale.y, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ]); this._matrices.mvMatrix.matrix = this._curtains._multiplyMatrix(matrixFromOrigin, scaleMatrix); // this is the result of our projection matrix * our mv matrix, useful for bounding box calculations and frustum culling this._matrices.mVPMatrix = this._curtains._multiplyMatrix(this._matrices.pMatrix.matrix, this._matrices.mvMatrix.matrix); // check if we should draw the plane but only if everything has been initialized if(!this.alwaysDraw) { this._shouldDrawCheck(); } } // update our matrix uniform only if we share programs or if we actually have updated its values if(this.shareProgram || !this.shareProgram && this._updateMVMatrix) { this._curtains._useProgram(this._usedProgram); this._curtains.gl.uniformMatrix4fv(this._matrices.mvMatrix.location, false, this._matrices.mvMatrix.matrix); } // reset our flag this._updateMVMatrix = false; }; /*** This will set our plane scale used internally but can be used externally as well params : @scaleX (float): scale to apply on X axis @scaleY (float): scale to apply on Y axis ***/ Curtains.Plane.prototype.setScale = function(scaleX, scaleY) { scaleX = isNaN(scaleX) ? this.scale.x : parseFloat(scaleX); scaleY = isNaN(scaleY) ? this.scale.y : parseFloat(scaleY); scaleX = Math.max(scaleX, 0.001); scaleY = Math.max(scaleY, 0.001); // only apply if values changed if(scaleX !== this.scale.x || scaleY !== this.scale.y) { this.scale = { x: scaleX, y: scaleY }; // adjust textures size for(var i = 0; i < this.textures.length; i++) { this.textures[i].resize(); } // we should update the plane mvMatrix this._updateMVMatrix = true; } }; /*** This will set our plane rotation used internally but can be used externally as well params : @angleX (float): rotation to apply on X axis (in radians) @angleY (float): rotation to apply on Y axis (in radians) @angleZ (float): rotation to apply on Z axis (in radians) ***/ Curtains.Plane.prototype.setRotation = function(angleX, angleY, angleZ) { angleX = isNaN(angleX) ? this.rotation.x : parseFloat(angleX); angleY = isNaN(angleY) ? this.rotation.y : parseFloat(angleY); angleZ = isNaN(angleZ) ? this.rotation.z : parseFloat(angleZ); // only apply if values changed if(angleX !== this.rotation.x || angleY !== this.rotation.y || angleZ !== this.rotation.z) { this.rotation = { x: angleX, y: angleY, z: angleZ }; this._setQuaternion(); // we should update the plane mvMatrix this._updateMVMatrix = true; } }; /*** Sets our plane rotation quaternion using Euler angles and XYZ as axis order ***/ Curtains.Plane.prototype._setQuaternion = function() { var ax = this.rotation.x * 0.5; var ay = this.rotation.y * 0.5; var az = this.rotation.z * 0.5; var sinx = Math.sin(ax); var cosx = Math.cos(ax); var siny = Math.sin(ay); var cosy = Math.cos(ay); var sinz = Math.sin(az); var cosz = Math.cos(az); // XYZ order this.quaternion[0] = sinx * cosy * cosz + cosx * siny * sinz; this.quaternion[1] = cosx * siny * cosz - sinx * cosy * sinz; this.quaternion[2] = cosx * cosy * sinz + sinx * siny * cosz; this.quaternion[3] = cosx * cosy * cosz - sinx * siny * sinz; }; /*** This will set our plane transform origin (0, 0, 0) means plane's top left corner (1, 1, 0) means plane's bottom right corner (0.5, 0.5, -1) means behind plane's center params : @xOrigin (float): coordinate of transformation origin along width @yOrigin (float): coordinate of transformation origin along height @zOrigin (float): coordinate of transformation origin along depth ***/ Curtains.Plane.prototype.setTransformOrigin = function(xOrigin, yOrigin, zOrigin) { xOrigin = isNaN(xOrigin) ? this.transformOrigin.x : parseFloat(xOrigin); yOrigin = isNaN(yOrigin) ? this.transformOrigin.y : parseFloat(yOrigin); zOrigin = isNaN(zOrigin) ? this.transformOrigin.z : parseFloat(zOrigin); if(xOrigin !== this.transformOrigin.x || yOrigin !== this.transformOrigin.y || zOrigin !== this.transformOrigin.z) { this.transformOrigin = { x: xOrigin, y: yOrigin, z: zOrigin, }; this._updateMVMatrix = true; } }; /*** This will set our plane translation by adding plane computed bounding box values and computed relative position values ***/ Curtains.Plane.prototype._setTranslation = function() { // avoid unnecessary calculations if we don't have a users set relative position var relativePosition = { x: 0, y: 0, z: 0, }; if(this.relativeTranslation.x !== 0 || this.relativeTranslation.y !== 0 || this.relativeTranslation.z !== 0) { relativePosition = this._documentToLocalSpace(this.relativeTranslation.x, this.relativeTranslation.y); } this._translation.x = this._boundingRect.world.left + relativePosition.x; this._translation.y = this._boundingRect.world.top + relativePosition.y; // we should update the plane mvMatrix this._updateMVMatrix = true; }; /*** This function takes pixel values along X and Y axis and convert them to clip space coordinates, and then apply the corresponding translation params : @translationX (float): translation to apply on X axis @translationY (float): translation to apply on Y axis ***/ Curtains.Plane.prototype.setRelativePosition = function(translationX, translationY, translationZ) { translationX = isNaN(translationX) ? this.relativeTranslation.x : parseFloat(translationX); translationY = isNaN(translationY) ? this.relativeTranslation.y : parseFloat(translationY); translationZ = isNaN(translationZ) ? this.relativeTranslation.z : parseFloat(translationZ); // only apply if values changed if(translationX !== this.relativeTranslation.x || translationY !== this.relativeTranslation.y || translationZ !== this.relativeTranslation.z) { this.relativeTranslation = { x: translationX, y: translationY, z: translationZ, }; this._setTranslation(); } }; /*** This function takes pixel values along X and Y axis and convert them to clip space coordinates params : @xPosition (float): position to convert on X axis @yPosition (float): position to convert on Y axis returns : @relativePosition: plane's position in WebGL space ***/ Curtains.Plane.prototype._documentToLocalSpace = function(xPosition, yPosition) { var relativePosition = { x: xPosition / (this._curtains._boundingRect.width / this._curtains.pixelRatio) * (this._curtains._boundingRect.width / this._curtains._boundingRect.height), y: -yPosition / (this._curtains._boundingRect.height / this._curtains.pixelRatio), }; return relativePosition; }; /*** This function checks if the plane is currently visible in the canvas and sets _shouldDraw property according to this test This checks DOM positions for now but we might want to improve it to use real frustum calculations ***/ Curtains.Plane.prototype._shouldDrawCheck = function() { // get plane bounding rect var actualPlaneBounds = this._getWebGLDrawRect(); var self = this; // if we decide to draw the plane only when visible inside the canvas // we got to check if its actually inside the canvas if( Math.round(actualPlaneBounds.right) <= this._curtains._boundingRect.left || Math.round(actualPlaneBounds.left) >= this._curtains._boundingRect.left + this._curtains._boundingRect.width || Math.round(actualPlaneBounds.bottom) <= this._curtains._boundingRect.top || Math.round(actualPlaneBounds.top) >= this._curtains._boundingRect.top + this._curtains._boundingRect.height ) { if(this._shouldDraw) { this._shouldDraw = false; // callback for leaving view setTimeout(function() { if(self._onLeaveViewCallback) { self._onLeaveViewCallback(); } }, 0); } } else { if(!this._shouldDraw) { // callback for entering view setTimeout(function() { if(self._onReEnterViewCallback) { self._onReEnterViewCallback(); } }, 0); } this._shouldDraw = true; } }; /*** This function returns if the plane is actually drawn (ie fully initiated, visible property set to true and not culled) ***/ Curtains.Plane.prototype.isDrawn = function() { return this._canDraw && this.visible && (this._shouldDraw || this.alwaysDraw); }; /*** This function uses our plane HTML Element bounding rectangle values and convert them to the world clip space coordinates, and then apply the corresponding translation ***/ Curtains.Plane.prototype._applyWorldPositions = function() { // set our plane sizes and positions relative to the world clipspace this._setWorldSizes(); // set the translation values this._setTranslation(); }; /*** This function updates the plane position based on its CSS positions and transformations values. Useful if the HTML element has been moved while the container size has not changed. ***/ Curtains.Plane.prototype.updatePosition = function() { // set the new plane sizes and positions relative to document by triggering getBoundingClientRect() this._setDocumentSizes(); // apply them this._applyWorldPositions(); }; /*** This function updates the plane position based on the Curtains class scroll manager values ***/ Curtains.Plane.prototype.updateScrollPosition = function() { // actually update the plane position only if last X delta or last Y delta is not equal to 0 if(this._curtains._scrollManager.lastXDelta || this._curtains._scrollManager.lastYDelta) { // set new positions based on our delta without triggering reflow this._boundingRect.document.top += this._curtains._scrollManager.lastYDelta * this._curtains.pixelRatio; this._boundingRect.document.left += this._curtains._scrollManager.lastXDelta * this._curtains.pixelRatio; // apply them this._applyWorldPositions(); } }; /*** This function set/unset the depth test for that plane params : @shouldEnableDepthTest (bool): enable/disable depth test for that plane ***/ Curtains.Plane.prototype.enableDepthTest = function(shouldEnableDepthTest) { this._depthTest = shouldEnableDepthTest; }; /*** This function puts the plane at the end of the draw stack, allowing it to overlap any other plane ***/ Curtains.Plane.prototype.moveToFront = function() { // disable the depth test this.enableDepthTest(false); var drawType = this._transparent ? "transparent" : "opaque"; var drawStack = this._curtains._drawStacks[drawType]["programs"]["program-" + this._usedProgram.id]; for(var i = 0; i < drawStack.length; i++) { if(this.index === drawStack[i]) { drawStack.splice(i, 1); } } if(drawType === "transparent") { drawStack.unshift(this.index); } else { drawStack.push(this.index); } this._curtains._drawStacks[drawType]["programs"]["program-" + this._usedProgram.id] = drawStack; // update order array for(var i = 0; i < this._curtains._drawStacks[drawType]["order"].length; i++) { if(this._curtains._drawStacks[drawType]["order"][i] === this._usedProgram.id) { this._curtains._drawStacks[drawType]["order"].splice(i, 1); } } this._curtains._drawStacks[drawType]["order"].push(this._usedProgram.id); }; /*** PLANE EVENTS ***/ /*** This is called each time a plane is entering again the view bounding box params : @callback (function) : a function to execute returns : @this: our plane to handle chaining ***/ Curtains.Plane.prototype.onReEnterView = function(callback) { if(callback) { this._onReEnterViewCallback = callback; } return this; }; /*** This is called each time a plane is leaving the view bounding box params : @callback (function) : a function to execute returns : @this: our plane to handle chaining ***/ Curtains.Plane.prototype.onLeaveView = function(callback) { if(callback) { this._onLeaveViewCallback = callback; } return this; }; /*** RENDERTARGET CLASS ***/ /*** Here we create a render target params : @curtainWrapper : our curtain object that wraps all the planes @params (object, optionnal): additionnal params - plane (plane object, optionnal): the plane to attach this render target to. Set under the hood by shader passes - depth (bool, optionnal): whether the render target should use a depth buffer and handle depth returns : @this: our render target element ***/ Curtains.RenderTarget = function(curtainWrapper, params) { if(!params) params = {}; this._curtains = curtainWrapper; this.index = this._curtains.renderTargets.length; this._type = "RenderTarget"; this._shaderPass = params.shaderPass || null; // whether to create a render buffer this._depth = params.depth || false; this._shouldClear = params.clear; if(this._shouldClear === null || this._shouldClear === undefined) { this._shouldClear = true; } this._minSize = { width: params.minWidth || 1024 * this._curtains.pixelRatio, height: params.minHeight || 1024 * this._curtains.pixelRatio, }; this.userData = {}; this.uuid = this._curtains._generateUUID(); this._curtains.renderTargets.push(this); this._initRenderTarget(); }; /*** Init our RenderTarget by setting its size and creating a textures array ***/ Curtains.RenderTarget.prototype._initRenderTarget = function() { this._setSize(); // create our render texture this.textures = []; // create our frame buffer this._createFrameBuffer(); }; /*** Sets our RenderTarget size based on its parent plane size ***/ Curtains.RenderTarget.prototype._setSize = function() { if(this._shaderPass && this._shaderPass._isScenePass) { this._size = { width: this._curtains._boundingRect.width, height: this._curtains._boundingRect.height, }; } else { this._size = { width: Math.max(this._minSize.width, this._curtains._boundingRect.width), height: Math.max(this._minSize.height, this._curtains._boundingRect.height), }; } }; /*** Resizes our RenderTarget (basically only resize it if it's a ShaderPass scene pass FBO) ***/ Curtains.RenderTarget.prototype.resize = function() { // resize render target only if its a child of a shader pass if(this._shaderPass && this._shaderPass._isScenePass) { this._setSize(); // cancel clear on resize this._curtains._bindFrameBuffer(this, true); if(this._depth) { this._bindDepthBuffer(); } this._curtains._bindFrameBuffer(null); } }; /*** Binds our depth buffer ***/ Curtains.RenderTarget.prototype._bindDepthBuffer = function() { var gl = this._curtains.gl; // render to our target texture by binding the framebuffer if(this._depthBuffer) { gl.bindRenderbuffer(gl.RENDERBUFFER, this._depthBuffer); // allocate renderbuffer gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this._size.width, this._size.height); // attach renderbuffer gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this._depthBuffer); } }; /*** Here we create our FBO texture and assign it as our FBO attachment ***/ Curtains.RenderTarget.prototype._createFBOTexture = function() { var gl = this._curtains.gl; if(this.textures.length > 0) { // we're restoring context, re init the texture this.textures[0]._canDraw = false; this.textures[0]._init(); } else { // attach the texture to the parent ShaderPass if it exists, to the render target otherwise var texture = new Curtains.Texture(this._shaderPass ? this._shaderPass : this, { index: this.textures.length, sampler: "uRenderTexture", isFBOTexture: true, }); this.textures.push(texture); } // attach the texture as the first color attachment // this.textures[0]._sampler.texture contains our WebGLTexture object gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.textures[0]._sampler.texture, 0); }; /*** Here we create our frame buffer object We're also adding a render buffer object to handle depth if needed ***/ Curtains.RenderTarget.prototype._createFrameBuffer = function() { var gl = this._curtains.gl; this._frameBuffer = gl.createFramebuffer(); // cancel clear on init this._curtains._bindFrameBuffer(this, true); this._createFBOTexture(); // create a depth renderbuffer if(this._depth) { this._depthBuffer = gl.createRenderbuffer(); this._bindDepthBuffer(); } this._curtains._bindFrameBuffer(null); }; /*** Restore a render target Used only for render targets that are not attached to shader passes Those attached to shader passes are restored inside the _restoreContext method of the ShaderPass class ***/ Curtains.RenderTarget.prototype._restoreContext = function() { if(!this._shaderPass || !this._shaderPass._isScenePass) { // if there's a _shaderPass attached it will be re-attached in the _shaderPass's restoreContext method anyway this._shaderPass = null; // recreate frame buffer this._createFrameBuffer(); } }; /*** Remove a RenderTarget buffers ***/ Curtains.RenderTarget.prototype._dispose = function() { if(this._frameBuffer) { this._curtains.gl.deleteFramebuffer(this._frameBuffer); this._frameBuffer = null; } if(this._depthBuffer) { this._curtains.gl.deleteRenderbuffer(this._depthBuffer); this._depthBuffer = null; } this.textures[0]._dispose(); this.textures = []; }; /*** SHADERPASS CLASS ***/ /*** Here we create our ShaderPass object (note that we are using the Curtains namespace to avoid polluting the global scope) It will inherits from ou BasePlane class that handles all the WebGL part ShaderPass class will handle the frame buffer params : @curtainWrapper : our curtain object that (we will use its container property and its size) @params (obj) : see addShaderPass method of the wrapper returns : @this: our ShaderPass element ***/ Curtains.ShaderPass = function(curtainWrapper, params) { if(!params) params = {}; // force plane defintion to 1x1 params.widthSegments = 1; params.heightSegments = 1; this._type = "ShaderPass"; // default to scene pass this._isScenePass = true; // inherit Curtains.BasePlane.call(this, curtainWrapper, curtainWrapper.container, params); this.index = this._curtains.shaderPasses.length; this._depth = params.depth || false; this._shouldClear = params.clear; if(this._shouldClear === null || this._shouldClear === undefined) { this._shouldClear = true; } this.target = params.renderTarget || null; if(this.target) { // if there's a target defined it's not a scene pass this._isScenePass = false; // inherit clear param this._shouldClear = this.target._shouldClear; } // if the program is valid, go on if(this._usedProgram) { this._initShaderPassPlane(); } }; Curtains.ShaderPass.prototype = Object.create(Curtains.BasePlane.prototype); Curtains.ShaderPass.prototype.constructor = Curtains.ShaderPass; /*** Here we init additionnal shader pass planes properties This mainly consists in creating our render texture and add a frame buffer object ***/ Curtains.ShaderPass.prototype._initShaderPassPlane = function() { // create our frame buffer if(!this.target) { this._createFrameBuffer(); } else { // set the render target this.setRenderTarget(this.target); this.target._shaderPass = this; // copy the render target texture var texture = new Curtains.Texture(this, { index: this.textures.length, sampler: "uRenderTexture", isFBOTexture: true, fromTexture: this.target.textures[0], }); this.textures.push(texture); } // onReady callback this._isPlaneReady(); this._canDraw = true; // be sure we'll update the scene even if drawing is disabled this._curtains.needRender(); }; /*** Here we override the parent _getDefaultVS method because shader passes vs don't have projection and model view matrices ***/ Curtains.ShaderPass.prototype._getDefaultVS = function(params) { return "precision mediump float;\nattribute vec3 aVertexPosition;attribute vec2 aTextureCoord;varying vec3 vVertexPosition;varying vec2 vTextureCoord;void main() {vTextureCoord = aTextureCoord;vVertexPosition = aVertexPosition;gl_Position = vec4(aVertexPosition, 1.0);}"; }; /*** Here we override the parent _getDefaultFS method taht way we can still draw our render texture ***/ Curtains.ShaderPass.prototype._getDefaultFS = function(params) { return "precision mediump float;\nvarying vec3 vVertexPosition;varying vec2 vTextureCoord;uniform sampler2D uRenderTexture;void main( void ) {gl_FragColor = texture2D(uRenderTexture, vTextureCoord);}"; }; /*** Here we create our frame buffer object We're also adding a render buffer object to handle depth inside our shader pass ***/ Curtains.ShaderPass.prototype._createFrameBuffer = function() { var target = new Curtains.RenderTarget(this._curtains, { shaderPass: this, clear: this._shouldClear, depth: this._depth, }); this.setRenderTarget(target); // add the frame buffer texture to the shader pass texture array this.textures.push(this.target.textures[0]); }; /*** TEXTURE CLASS ***/ /*** Here we create our Texture object (note that we are using the Curtains namespace to avoid polluting the global scope) params: @parent (Plane, ShaderPass or RenderTarget object): the parent object using that texture @params (obj): see createTexture method of the Plane returns: @this: our newly created texture object ***/ Curtains.Texture = function(parent, params) { // set up base properties this._parent = parent; this._curtains = parent._curtains; this.uuid = this._curtains._generateUUID(); if(!parent._usedProgram && !params.isFBOTexture) { if(!this._curtains.productionMode) { console.warn("Unable to create the texture because the program is not valid"); } return; } this.index = parent.textures.length; // prepare texture sampler this._sampler = { isActive: false, name: params.sampler || "uSampler" + this.index }; // we will always declare a texture matrix this._textureMatrix = { name: params.sampler ? params.sampler + "Matrix" : "uTextureMatrix" + this.index, matrix: null, }; // _willUpdate and shouldUpdate property are set to false by default // we will handle that in the setSource() method for videos and canvases this._willUpdate = false; this.shouldUpdate = false; // if we need to force a texture update this._forceUpdate = false; this.scale = { x: 1, y: 1, }; // custom user properties this.userData = {}; // is it a frame buffer object texture? // if it's not, type will change when the source will be loaded this.type = params.isFBOTexture ? "fboTexture" : "empty"; // useful flag to avoid binding texture that does not belong to current context this._canDraw = false; // is it set from an existing texture? if(params.fromTexture) { this._initFromTexture = true; // set sampler loation if needed if(this._parent._usedProgram) { // set our texture sampler uniform this._setTextureUniforms(); } // copy from the original texture this.setFromTexture(params.fromTexture); // we're done! return; } this._initFromTexture = false; // init our texture this._init(); return this; }; /*** Init our texture object ***/ Curtains.Texture.prototype._init = function() { var gl = this._curtains.gl; // create our WebGL texture this._sampler.texture = gl.createTexture(); // texImage2D properties this._internalFormat = gl.RGBA; this._format = gl.RGBA; this._textureType = gl.UNSIGNED_BYTE; // set texture parameters once this._texParameters = false; this._flipY = false; // bind the texture the target (TEXTURE_2D) of the active texture unit. gl.bindTexture(gl.TEXTURE_2D, this._sampler.texture); // we don't use Y flip yet if(this._curtains._glState.flipY) { this._curtains._glState.flipY = this._flipY; gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this._flipY); } gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); // if the parent has a program it means its not a render target texture if(this._parent._usedProgram) { // set its size based on parent element size this._size = { width: this._parent._boundingRect.document.width, height: this._parent._boundingRect.document.height, }; // set uniform this._setTextureUniforms(); // its a plane texture if(this.type === "empty") { // draw a black plane before the real texture's content has been loaded gl.texImage2D(gl.TEXTURE_2D, 0, this._internalFormat, 1, 1, 0, this._format, this._textureType, new Uint8Array([0, 0, 0, 255])); // our texture source hasn't been loaded yet this._sourceLoaded = false; } else if(!this.source) { // get a texture matrix even if it fits the viewport var sizes = this._getSizes(); // always update texture matrix anyway this._updateTextureMatrix(sizes); } } else { // its a render target texture, it has no uniform location and no texture matrix this._size = { width: this._parent._size.width || this._curtains._boundingRect.width, height: this._parent._size.height || this._curtains._boundingRect.height, }; } // if its a render target texture use nearest filters and half float whenever possible if(this.type === "fboTexture") { // update texImage2D properties if(this._curtains._isWebGL2 && this._curtains._extensions['EXT_color_buffer_float']) { this._internalFormat = gl.RGBA16F; this._textureType = gl.HALF_FLOAT; } else if(this._curtains._extensions['OES_texture_half_float']) { this._textureType = this._curtains._extensions['OES_texture_half_float'].HALF_FLOAT_OES; } // define its size gl.texImage2D(gl.TEXTURE_2D, 0, this._internalFormat, this._size.width, this._size.height, 0, this._format, this._textureType, null); // set texture parameters this._setMipmaps(); } this._canDraw = true; }; /*** SEND DATA TO THE GPU ***/ /*** Check if our textures is effectively used in our shaders If so, set it to active, get its uniform locations and bind it to our texture unit ***/ Curtains.Texture.prototype._setTextureUniforms = function() { // check if our texture is used in our program shaders // if so, get its uniform locations and bind it to our program for(var i = 0; i < this._parent._activeTextures.length; i++) { if(this._parent._activeTextures[i].name === this._sampler.name) { // this texture is active this._sampler.isActive = true; // set our texture sampler uniform this._sampler.location = this._curtains.gl.getUniformLocation(this._parent._usedProgram.program, this._sampler.name); // texture matrix uniform this._textureMatrix.location = this._curtains.gl.getUniformLocation(this._parent._usedProgram.program, this._textureMatrix.name); // use the program and get our sampler and texture matrices uniforms this._curtains._useProgram(this._parent._usedProgram); // tell the shader we bound the texture to our indexed texture unit this._curtains.gl.uniform1i(this._sampler.location, this.index); } } }; /*** LOADING SOURCES ***/ /*** This applies an already existing Texture object to our texture params: @texture (Texture): texture to set from ***/ Curtains.Texture.prototype.setFromTexture = function(texture) { if(texture) { this.type = texture.type; this._sampler.texture = texture._sampler.texture; this.source = texture.source; this._size = texture._size; this._sourceLoaded = texture._sourceLoaded; this._internalFormat = texture._internalFormat; this._format = texture._format; this._textureType = texture._textureType; this._texParameters = texture._texParameters; this._originalTexture = texture; // update its texture matrix if needed and we're good to go! if(this._parent._usedProgram && (!this._canDraw || !this._textureMatrix.matrix)) { var sizes = this._getSizes(); // always update texture matrix anyway this._updateTextureMatrix(sizes); this._canDraw = true; } } else if(!this._curtains.productionMode) { console.warn("Unable to set the texture from texture:", texture); } }; /*** This uses our source as texture params: @source (images/video/canvas): either an image, a video or a canvas ***/ Curtains.Texture.prototype.setSource = function(source) { // if our program hasn't been validated we can't set a texture source if(!this._parent._usedProgram) { if(!this._curtains.productionMode) { console.warn("Unable to set the texture source because the program is not valid"); } return; } this.source = source; if(this.type === "empty") { if(source.tagName.toUpperCase() === "IMG") { this.type = "image"; } else if(source.tagName.toUpperCase() === "VIDEO") { this.type = "video"; // a video should be updated by default // _willUpdate property will be set to true if the video has data to draw this.shouldUpdate = true; } else if(source.tagName.toUpperCase() === "CANVAS") { this.type = "canvas"; // a canvas could change each frame so we need to update it by default this._willUpdate = true; this.shouldUpdate = true; } else if(!this._curtains.productionMode) { console.warn("this HTML tag could not be converted into a texture:", source.tagName); } } this._size = { width: this.source.naturalWidth || this.source.width || this.source.videoWidth, height: this.source.naturalHeight || this.source.height || this.source.videoHeight, }; // our source is loaded now this._sourceLoaded = true; var gl = this._curtains.gl; // Bind the texture the target (TEXTURE_2D) of the active texture unit. gl.activeTexture(gl.TEXTURE0 + this.index); gl.bindTexture(gl.TEXTURE_2D, this._sampler.texture); // maybe we should handle alpha premultiplying separately for each texture // for now we just use our gl context premultipliedAlpha value if(this._curtains.premultipliedAlpha) { gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); } this._flipY = true; if(!this._curtains._glState.flipY) { this._curtains._glState.flipY = this._flipY; gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this._flipY); } this.resize(); // set our webgl texture only if it is an image // canvas and video textures will be updated anyway in the rendering loop // thanks to the shouldUpdate and _willUpdate flags if(this.type === "image") { gl.texImage2D(gl.TEXTURE_2D, 0, this._internalFormat, this._format, this._textureType, source); // set texture parameters this._setMipmaps(); } // update scene this._curtains.needRender(); }; /*** Sets the texture parameters Always clamp to edge Generates mipmapping for images in WebGL2 context ***/ Curtains.Texture.prototype._setMipmaps = function() { var gl = this._curtains.gl; gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // generate mip map only for images if(this._curtains._isWebGL2 && this.type === "image") { gl.generateMipmap(gl.TEXTURE_2D); // improve quality of scaled down images gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST); } else { // Set the parameters so we can render any size image. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); } this._texParameters = true; }; /*** This forces a texture to be updated on the next draw call ***/ Curtains.Texture.prototype.needUpdate = function() { this._forceUpdate = true; }; /*** This updates our texture Called inside our drawing loop if shouldUpdate property is set to true Typically used by videos or canvas ***/ Curtains.Texture.prototype._update = function() { var gl = this._curtains.gl; if(this.source) { gl.texImage2D(gl.TEXTURE_2D, 0, this._internalFormat, this._format, this._textureType, this.source); if(!this._texParameters) { this._setMipmaps(); } } else { gl.texImage2D(gl.TEXTURE_2D, 0, this._internalFormat, this._size.width, this._size.height, 0, this._format, this._textureType, this.source); } }; /*** TEXTURE SIZINGS ***/ /*** This is used to calculate how to crop/center an texture returns: @sizes (obj): an object containing plane sizes, source sizes and x and y offset to center the source in the plane ***/ Curtains.Texture.prototype._getSizes = function() { // remember our ShaderPass objects don't have a scale property var scale = this._parent.scale ? this._parent.scale : {x: 1, y: 1}; var parentWidth = this._parent._boundingRect.document.width * scale.x; var parentHeight = this._parent._boundingRect.document.height * scale.y; var sourceWidth = this._size.width; var sourceHeight = this._size.height; var sourceRatio = sourceWidth / sourceHeight; var parentRatio = parentWidth / parentHeight; // center image in its container var xOffset = 0; var yOffset = 0; if(parentRatio > sourceRatio) { // means parent is larger yOffset = Math.min(0, parentHeight - (parentWidth * (1 / sourceRatio))); } else if(parentRatio < sourceRatio) { // means parent is taller xOffset = Math.min(0, parentWidth - (parentHeight * sourceRatio)); } return { parentWidth: parentWidth, parentHeight: parentHeight, sourceWidth: sourceWidth, sourceHeight: sourceHeight, xOffset: xOffset, yOffset: yOffset, }; }; /*** Set the texture scale and then update its matrix params: @scaleX (float): scale to apply on X axis @scaleY (float): scale to apply on Y axis ***/ Curtains.Texture.prototype.setScale = function(scaleX, scaleY) { scaleX = isNaN(scaleX) ? this.scale.x : parseFloat(scaleX); scaleY = isNaN(scaleY) ? this.scale.y : parseFloat(scaleY); scaleX = Math.max(scaleX, 0.001); scaleY = Math.max(scaleY, 0.001); if(scaleX !== this.scale.x || scaleY !== this.scale.y) { this.scale = { x: scaleX, y: scaleY, }; this.resize(); } }; /*** This is used to crop/center a texture If the texture is using texture matrix then we just have to update its matrix If it is a render pass texture we also upload the texture with its new size on the GPU ***/ Curtains.Texture.prototype.resize = function() { if(this.type === "fboTexture") { var gl = this._curtains.gl; this._size = { width: this._parent._boundingRect.document.width, height: this._parent._boundingRect.document.height, }; // if its not a texture set from another texture if(!this._originalTexture) { gl.bindTexture(gl.TEXTURE_2D, this._parent.textures[0]._sampler.texture); gl.texImage2D(gl.TEXTURE_2D, 0, this._internalFormat, this._size.width, this._size.height, 0, this._format, this._textureType, this.source); } } else if(this.source) { // reset texture sizes (useful for canvas because their dimensions might change on resize) this._size = { width: this.source.naturalWidth || this.source.width || this.source.videoWidth, height: this.source.naturalHeight || this.source.height || this.source.videoHeight, }; } // if we need to update the texture matrix uniform if(this._parent._usedProgram) { // no point in resizing texture if it does not have a source yet var sizes = this._getSizes(); // always update texture matrix anyway this._updateTextureMatrix(sizes); } }; /*** This updates our textures matrix uniform based on plane and sources sizes params: @sizes (object): object containing plane sizes, source sizes and x and y offset to center the source in the plane ***/ Curtains.Texture.prototype._updateTextureMatrix = function(sizes) { // calculate scale to apply to the matrix var texScale = { x: sizes.parentWidth / (sizes.parentWidth - sizes.xOffset), y: sizes.parentHeight / (sizes.parentHeight - sizes.yOffset), }; // apply texture scale texScale.x /= this.scale.x; texScale.y /= this.scale.y; // translate texture to center it var textureTranslation = new Float32Array([ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, (1 - texScale.x) / 2, (1 - texScale.y) / 2, 0.0, 1.0 ]); // scale texture this._textureMatrix.matrix = this._curtains._scaleMatrix( textureTranslation, texScale.x, texScale.y, 1 ); // update the texture matrix uniform this._curtains._useProgram(this._parent._usedProgram); this._curtains.gl.uniformMatrix4fv(this._textureMatrix.location, false, this._textureMatrix.matrix); }; /*** This calls our loading callback and set our media as texture source ***/ Curtains.Texture.prototype._onSourceLoaded = function(source) { // increment our loading manager this._parent._loadingManager.sourcesLoaded++; // fire callback during load (useful for a loader) var self = this; if(!this._sourceLoaded) { setTimeout(function() { if(self._parent._onPlaneLoadingCallback) { self._parent._onPlaneLoadingCallback(self); } }, 0); } // set the media as our texture source this.setSource(source); // fire parent plane onReady callback if needed this._parent._isPlaneReady(); // add to the cache if needed if(this.type === "image") { var shouldCache = true; for(var i = 0; i < this._curtains._imageCache.length; i++) { if(this._curtains._imageCache[i].source && this._curtains._imageCache[i].source.src === source.src) { shouldCache = false; } } if(shouldCache) { this._curtains._imageCache.push(this); } } }; /*** This handles our canplaythrough data event, then handles source loaded ***/ Curtains.Texture.prototype._onVideoLoadedData = function(video) { // check if we have not already loaded the source to avoid calling loading callback twice if(!this._sourceLoaded) { this._onSourceLoaded(video); } }; /*** This is called to draw the texture ***/ Curtains.Texture.prototype._drawTexture = function() { // only draw if the texture is active (used in the shader) if(this._sampler.isActive) { // bind the texture this._parent._bindPlaneTexture(this); // force flip y for textures that needs it if(this._flipY && !this._curtains._glState.flipY) { this._curtains._glState.flipY = this._flipY; this._curtains.gl.pixelStorei(this._curtains.gl.UNPACK_FLIP_Y_WEBGL, this._flipY); } // check if the video is actually really playing if(this.type === "video" && this.source && this.source.readyState >= this.source.HAVE_CURRENT_DATA) { this._willUpdate = true; } if(this._forceUpdate || (this._willUpdate && this.shouldUpdate)) { this._update(); } // reset the video willUpdate flag if(this.type === "video") { this._willUpdate = false; } this._forceUpdate = false; } }; /*** Restore a WebGL texture that is a copy Depending on whether it's a copy from start or not, just reset its uniforms or run the full init And finally copy our original texture back again ***/ Curtains.Texture.prototype._restoreFromTexture = function() { if(this._initFromTexture) { this._setTextureUniforms(); } else { this._init(); } this.setFromTexture(this._originalTexture); }; /*** Restore our WebGL texture If it is an original texture, just re run the init function and eventually reset its source If it is a texture set from another texture, wait for the original texture to be ready first ***/ Curtains.Texture.prototype._restoreContext = function() { // avoid binding that texture before reseting it this._canDraw = false; this._sampler.isActive = false; // this is an original texture, reset it right away if(!this._originalTexture) { this._init(); if(this.source) { // cache again if it is an image if(this.type === "image") { this._curtains._imageCache.push(this); } this.setSource(this.source); // force update this.needUpdate(); } } else { // here we will have to wait for the original texture to be ready before resetting our copy var self = this; // original texture is not ready yet, wait for it! if(!this._originalTexture._canDraw) { var textureReadyInterval = setInterval(function() { if(self._originalTexture._canDraw) { self._restoreFromTexture(); clearInterval(textureReadyInterval); } }, 16); } else { // original texture has been resetted already, wait a tick and restore this one setTimeout(function() { self._restoreFromTexture(); }, 0); } } }; /*** This is used to destroy a texture and free the memory space Usually used on a plane/shader pass/render target removal ***/ Curtains.Texture.prototype._dispose = function() { if(this.type === "video") { // remove event listeners this.source.removeEventListener("canplaythrough", this._onSourceLoadedHandler, false); this.source.removeEventListener("error", this._parent._sourceLoadError, false); // empty source to properly delete video element and free the memory this.source.pause(); this.source.removeAttribute("src"); this.source.load(); // clear source this.source = null; } else if(this.type === "canvas") { // clear all canvas states this.source.width = this.source.width; // clear source this.source = null; } else if(this.type === "image" && this._curtains._isDestroying) { // delete image only if we're destroying the context (keep in cache otherwise) this.source.removeEventListener("load", this._onSourceLoadedHandler, false); this.source.removeEventListener("error", this._parent._sourceLoadError, false); // clear source this.source = null; } var gl = this._curtains.gl; // do not delete original texture if this texture is a copy, or image texture if we're not destroying the context var shouldDelete = gl && !this._originalTexture && (this.type !== "image" || this._curtains._isDestroying); if(shouldDelete) { gl.activeTexture(gl.TEXTURE0 + this.index); gl.bindTexture(gl.TEXTURE_2D, null); gl.deleteTexture(this._sampler.texture); } // decrease textures loaded this._parent._loadingManager && this._parent._loadingManager.sourcesLoaded--; };
Edit
Rename
Chmod
Delete
FILE
FOLDER
Name
Size
Permission
Action
curtains.js
172281 bytes
0644
curtains.min.js
71246 bytes
0644
N4ST4R_ID | Naxtarrr