diff --git a/Gruntfile.js b/Gruntfile.js index b0f452ce..fc440893 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -66,14 +66,6 @@ module.exports = function(grunt) { "src/tiledimage.js", "src/tilecache.js", "src/world.js", - - //Aiosa's webgl drawer - needs optimization, polishing, trimming - "src/webgl/renderer.js", - "src/webgl/shaderLayer.js", - "src/webgl/dataLoader.js", - "src/webgl/webGLContext.js", - "src/webgl/drawer.js", - "src/webgl/plainShader.js", ]; var banner = "//! <%= pkg.name %> <%= pkg.version %>\n" + diff --git a/src/drawerbase.js b/src/drawerbase.js index 62cb1e04..ac7b3bd1 100644 --- a/src/drawerbase.js +++ b/src/drawerbase.js @@ -38,6 +38,12 @@ $.DrawerOptions = class DrawerOptions{ constructor(options){} }; +/** + * @typedef {Object} Point + * @property {number} x + * @property {number} y + */ + /** * @class DrawerBase * @memberof OpenSeadragon @@ -297,7 +303,7 @@ $.DrawerBase = class DrawerBase{ * @inner * Calculate width and height of the canvas based on viewport dimensions * and pixelDensityRatio - * @returns {Dictionary} {x, y} size of the canvas + * @returns {Point} {x, y} size of the canvas */ _calculateCanvasSize() { var pixelDensityRatio = $.pixelDensityRatio; diff --git a/src/tilecache.js b/src/tilecache.js index 7d9e5478..c776a077 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -272,7 +272,8 @@ $.TileCache.prototype = { * @property {CanvasRenderingContext2D} context2D - The context that is being unloaded */ tiledImage.viewer.raiseEvent("image-unloaded", { - context2D: context2D + context2D: context2D, + tile: tile }); } diff --git a/src/webgl/dataLoader.js b/src/webgl/dataLoader.js deleted file mode 100644 index 6e800aba..00000000 --- a/src/webgl/dataLoader.js +++ /dev/null @@ -1,542 +0,0 @@ - -(function($) { - -/** - * IDataLoader conforms to a specific texture type and WebGL version. - * It provides API for uniform handling of textures: - * - texture loading - * - GLSL texture handling - */ -$.WebGLModule.IDataLoader = class { - /** - * Creation - * @param {WebGLRenderingContextBase} gl - * @param {string} webglVersion - * @param {object} options - * @param {GLuint} options.wrap texture wrap parameteri - * @param {GLuint} options.magFilter texture filter parameteri - * @param {GLuint} options.minFilter texture filter parameteri - * */ - constructor(gl, webglVersion, options) { - //texture cache to keep track of loaded GPU data - this.__cache = new Map(); - this.wrap = options.wrap; - this.minFilter = options.minFilter; - this.magFilter = options.magFilter; - this.maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); - - /** - * Loader strategy based on toString result, extend with your type if necessary. - * If your type cannot use the given version strategy (TEXTURE_2D_ARRAY UNIT), you have - * to re-define the whole API. - * - * When a data is sent to the shader for processing, `toString` method is called to - * get the data identifier. A typeLoaders key must be present to handle loading - * of that texture(s) data. - * @member typeLoaders - * @memberOf OpenSeadragon.WebGLModule.IDataLoader - * - * @return {object} whatever you need to stare in the cache to later free the object - */ - this.typeLoaders = {}; - } - - /** - * @param {string} version - * @return {boolean} true if given webgl version is supported by the loader - */ - supportsWebglVersion(version) { - throw("::supportsWebglVersion must be implemented!"); - } - - /** - * Get stored options under ID - * @param id - * @return {unknown} options stored by setLoaded - */ - getLoaded(id) { - return this.__cache.get(id); - } - - /** - * Store options object - * @param id - * @param options - */ - setLoaded(id, options) { - this.__cache.set(id, options); - } - - /** - * Unload stored options - * @param id - */ - setUnloaded(id) { - this.__cache.delete(id); - } - - /** - * Set texture sampling parameters - * @param {string} name one of 'minFilter', 'magFilter', 'wrap' - * @param {GLuint} value - */ - setTextureParam(name, value) { - if (!['minFilter', 'magFilter', 'wrap'].includes(name)) { - return; - } - this[name] = value; - } - - /** - * - * @param renderer - * @param id - * @param options - */ - unloadTexture(renderer, id, options) { - throw("::unloadTexture must be implemented!"); - } - - /** - * @param {OpenSeadragon.WebGLModule} renderer renderer renderer reference - * @param id - * @param data - * @param width - * @param height - * @param {[number]} shaderDataIndexToGlobalDataIndex mapping of array indices to data indices, e.g. texture 0 for - * this shader corresponds to index shaderDataIndexToGlobalDataIndex[0] in the data array, - * -1 value used for textures not loaded - */ - load(renderer, id, data, width, height, shaderDataIndexToGlobalDataIndex) { - if (!data) { - $.console.warn("Attempt to draw nullable data!"); - return; - } - const textureLoader = this.typeLoaders[toString.apply(data)]; - if (!textureLoader) { - throw "WebGL Renderer cannot load data as texture: " + toString.apply(data); - } - this.setLoaded(id, textureLoader(this, renderer, data, width, height, shaderDataIndexToGlobalDataIndex)); - } - - /** - * - * @param renderer - * @param id - */ - free(renderer, id) { - this.unloadTexture(renderer, id, this.getLoaded(id)); - this.setUnloaded(id); - } - - /** - * Called when the program is being loaded (set as active) - * @param {OpenSeadragon.WebGLModule} renderer - * @param {WebGLRenderingContextBase} gl WebGL context - * @param {WebGLProgram} program - * @param {object} specification reference to the specification object used - */ - programLoaded(renderer, gl, program, specification) { - //not needed - } - - /** - * Called when tile is processed - * @param {OpenSeadragon.WebGLModule} renderer renderer renderer reference - * @param {object} specification reference to the current active specification object - * @param {*} id data object present in the texture cache - * @param {WebGLProgram} program current WebGLProgram - * @param {WebGL2RenderingContext} gl - */ - programUsed(renderer, specification, id, program, gl) { - - } - - /** - * Sample texture - * @param {number|string} index texture index, must respect index re-mapping (see declare()) - * @param {string} vec2coords GLSL expression that evaluates to vec2 - * @return {string} GLSL expression (unterminated) that evaluates to vec4 - */ - sample(index, vec2coords) { - return `texture(_vis_data_sampler_array, vec3(${vec2coords}, _vis_data_sampler_array_indices[${index}]))`; - } - - /** - * Declare GLSL texture logic (global scope) in the GLSL shader - * @param {[number]} shaderDataIndexToGlobalDataIndex mapping of array indices to data indices, e.g. texture 0 for - * this shader corresponds to index shaderDataIndexToGlobalDataIndex[0] in the data array, - * -1 value used for textures not loaded - * @return {string} GLSL declaration (terminated with semicolon) of necessary elements for textures - */ - declare(shaderDataIndexToGlobalDataIndex) { - return ` -vec4 osd_texture(float index, vec2 coords) { - //This method must be implemented! -} - -//TODO: is this relevant? -// vec2 osd_texture_size() { -// //This method must be implemented! -// } -`; - } -}; - -/** - * Data loading strategies for different WebGL versions. - * Should you have your own data format, change/re-define these - * to correctly load the textures to GPU, based on the WebGL version used. - * - * The processing accepts arrays of images to feed to the shader built from configuration. - * This implementation supports data as Image or Canvas objects. We will refer to them as - * - * Implemented texture loaders support - * - working with object - image data chunks are vertically concatenated - * - working with [] object - images are in array - * - * @namespace OpenSeadragon.WebGLModule.Loaders - */ -$.WebGLModule.Loaders = { - - /** - * //TODO: ugly - * In case the system is fed by anything but 'Image' (or the like) data object, - * implement here conversion so that debug mode can draw it. - * @param {*} data - * @return {HTMLElement} Dom Element - */ - dataAsHtmlElement: function(data) { - return { - "[object HTMLImageElement]": () => data, - "[object HTMLCanvasElement]": () => data, - //Image objects in Array, we assume image objects only - "[object Array]": function() { - const node = document.createElement("div"); - for (let image of data) { - node.append(image); - } - return node; - } - }[toString.apply(data)](); - }, - - /** - * Data loader for WebGL 2.0. Must load the data to a Texture2DArray. - * The name of the texture is a constant. The order od the textures in - * the z-stacking is defined in shaderDataIndexToGlobalDataIndex. - * - * For details, please, see the implementation. - * @class OpenSeadragon.WebGLModule.Loaders.TEXTURE_2D_ARRAY - */ - TEXTURE_2D_ARRAY: class /**@lends $.WebGLModule.Loaders.TEXTURE_2D_ARRAY */ extends OpenSeadragon.WebGLModule.IDataLoader { - unloadTexture(renderer, id, options) { - renderer.gl.deleteTexture(options); - } - - /** - * Creation - * @param {WebGL2RenderingContext} gl - * @param {string} webglVersion - * @param {object} options - * @param {GLuint} options.wrap texture wrap parameteri - * @param {GLuint} options.magFilter texture filter parameteri - * @param {GLuint} options.minFilter texture filter parameteri - * @memberOf OpenSeadragon.WebGLModule.Loaders.TEXTURE_2D_ARRAY - * */ - constructor(gl, webglVersion, options) { - super(gl, webglVersion, options); - - if (webglVersion !== "2.0") { - throw "Incompatible WebGL version for TEXTURE_2D_ARRAY data loader!"; - } - - // this.batchSize = 5; - // let lastBatch = null; - - this.typeLoaders["[object HTMLImageElement]"] = - this.typeLoaders["[object HTMLCanvasElement]"] = function (self, webglModule, data, width, height, - shaderDataIndexToGlobalDataIndex) { - - const NUM_IMAGES = Math.round(data.height / height); - const gl = webglModule.gl; - - // //todo different tile sizes are problems - // if (!lastBatch || lastBatch.length < NUM_IMAGES) { - // lastBatch = { - // texId: gl.createTexture(), - // length: this.batchSize, - // texCount: 0, - // }; - // gl.bindTexture(gl.TEXTURE_2D_ARRAY, options.texId); - // gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, data[0].width, data[0].height, data.length + 1); - // } else { - // gl.bindTexture(gl.TEXTURE_2D_ARRAY, options.texId); - // } - - const textureId = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D_ARRAY, textureId); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, self.magFilter); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, self.minFilter); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, self.wrap); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, self.wrap); - - gl.texImage3D( - gl.TEXTURE_2D_ARRAY, - 0, - gl.RGBA, - width, - height, - NUM_IMAGES, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - data - ); - - return textureId; - }; - - //Image objects in Array, we assume image objects only todo ugly, can be array of anything - this.typeLoaders["[object Array]"] = function (self, webglModule, data, options, width, height, shaderDataIndexToGlobalDataIndex) { - const gl = webglModule.gl; - const textureId = gl.createTexture(); - - //gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D_ARRAY, textureId); - gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, data[0].width, data[0].height, data.length + 1); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LEVEL, 0); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, self.minFilter); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, self.magFilter); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, self.wrap); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, self.wrap); - - let index = 0; - for (let image of data) { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, index++, image.width, image.height, - 1, gl.RGBA, gl.UNSIGNED_BYTE, image); - } - return textureId; - }; - } - - supportsWebglVersion(version) { - return version === "2.0"; - } - - programUsed(renderer, specification, id, program, gl) { - gl.activeTexture(gl.TEXTURE0); - const tid = this.getLoaded(id); - gl.bindTexture(gl.TEXTURE_2D_ARRAY, tid); - } - - sample(index, vec2coords) { - return `texture(_vis_data_sampler_array, vec3(${vec2coords}, _vis_data_sampler_array_indices[${index}]))`; - } - - declare(shaderDataIndexToGlobalDataIndex) { - return `uniform sampler2DArray _vis_data_sampler_array; -int _vis_data_sampler_array_indices[${shaderDataIndexToGlobalDataIndex.length}] = int[${shaderDataIndexToGlobalDataIndex.length}]( - ${shaderDataIndexToGlobalDataIndex.join(",")} -); - -vec4 osd_texture(int index, vec2 coords) { - return ${this.sample("index", "coords")}; -} -`; - } - }, - - - /** - * Data loader for WebGL 2.0. Must load the data to a Texture2DArray. - * The name of the texture is a constant. The order od the textures in - * the z-stacking is defined in shaderDataIndexToGlobalDataIndex. - * - * For details, please, see the implementation. - * @class OpenSeadragon.WebGLModule.Loaders.TEXTURE_2D - */ - TEXTURE_2D: class /**@lends $.WebGLModule.Loaders.TEXTURE_2D */ extends OpenSeadragon.WebGLModule.IDataLoader { - - /** - * Creation - * @param {WebGL2RenderingContext} gl - * @param {string} webglVersion - * @param {object} options - * @param {GLuint} options.wrap texture wrap parameteri - * @param {GLuint} options.magFilter texture filter parameteri - * @param {GLuint} options.minFilter texture filter parameteri - * @memberOf OpenSeadragon.WebGLModule.Loaders.TEXTURE_2D - * */ - constructor(gl, webglVersion, options) { - super(gl, webglVersion, options); - - this._samples = webglVersion === "1.0" ? "texture2D" : "texture"; - - this.typeLoaders["[object HTMLImageElement]"] = - this.typeLoaders["[object HTMLCanvasElement]"] = function (self, webglModule, data, width, height, - shaderDataIndexToGlobalDataIndex) { - - //Avoid canvas slicing if possible - const NUM_IMAGES = Math.round(data.height / height); - if (NUM_IMAGES === 1) { - const texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, self.minFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, self.magFilter); - gl.texImage2D(gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - data); - return [texture]; - } - - if (!self._canvas) { - self._canvas = document.createElement('canvas'); - self._canvasReader = self._canvas.getContext('2d', {willReadFrequently: true}); - self._canvasConverter = document.createElement('canvas'); - self._canvasConverterReader = self._canvasConverter.getContext('2d', - {willReadFrequently: true}); - } - - let index = 0; - width = Math.round(width); - height = Math.round(height); - - const units = []; - - //we read from here - self._canvas.width = data.width; - self._canvas.height = data.height; - self._canvasReader.drawImage(data, 0, 0); - - //Allowed texture size dimension only 256+ and power of two... - - //it worked for arbitrary size until we begun with image arrays... is it necessary? - const IMAGE_SIZE = data.width < 256 ? 256 : Math.pow(2, Math.ceil(Math.log2(data.width))); - self._canvasConverter.width = IMAGE_SIZE; - self._canvasConverter.height = IMAGE_SIZE; - - //just load all images and let shaders reference them... - for (let i = 0; i < shaderDataIndexToGlobalDataIndex.length; i++) { - if (shaderDataIndexToGlobalDataIndex[i] < 0) { - continue; - } - if (index >= NUM_IMAGES) { - console.warn("The visualisation contains less data than layers. Skipping layers ..."); - return units; - } - - units.push(gl.createTexture()); - gl.bindTexture(gl.TEXTURE_2D, units[index]); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, self.minFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, self.magFilter); - - let pixels; - if (width !== IMAGE_SIZE || height !== IMAGE_SIZE) { - self._canvasConverterReader.drawImage(self._canvas, 0, - shaderDataIndexToGlobalDataIndex[i] * height, - width, height, 0, 0, IMAGE_SIZE, IMAGE_SIZE); - - pixels = self._canvasConverterReader.getImageData(0, 0, IMAGE_SIZE, IMAGE_SIZE); - } else { - //load data - pixels = self._canvasReader.getImageData(0, - shaderDataIndexToGlobalDataIndex[i] * height, width, height); - } - - gl.texImage2D(gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - pixels); - index++; - } - return units; - }; - - //Image objects in Array, we assume image objects only todo ugly, can be array of anything - this.typeLoaders["[object Array]"] = function (self, webglModule, data, options, width, height, shaderDataIndexToGlobalDataIndex) { - const gl = webglModule.gl; - - let index = 0; - const NUM_IMAGES = data.length; - const units = []; - - //just load all images and let shaders reference them... - for (let i = 0; i < shaderDataIndexToGlobalDataIndex.length; i++) { - if (shaderDataIndexToGlobalDataIndex[i] < 0) { - continue; - } - if (index >= NUM_IMAGES) { - console.warn("The visualisation contains less data than layers. Skipping layers ..."); - return units; - } - - //create textures - units.push(gl.createTexture()); - gl.bindTexture(gl.TEXTURE_2D, units[index]); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, self.minFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, self.magFilter); - //do not check the image size, we render what wwe - gl.texImage2D(gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - data[index++] - ); - } - return units; - }; - } - - unloadTexture(renderer, id, options) { - for (let textureUnit of options) { - renderer.gl.deleteTexture(textureUnit); - } - } - - supportsWebglVersion(version) { - return true; - } - - programUsed(renderer, specification, id, program, gl) { - const units = this.getLoaded(id); - for (let i = 0; i < units.length; i++) { - let textureUnit = units[i]; - let bindConst = `TEXTURE${i}`; - gl.activeTexture(gl[bindConst]); - gl.bindTexture(gl.TEXTURE_2D, textureUnit); - let location = gl.getUniformLocation(program, `vis_data_sampler_${i}`); - gl.uniform1i(location, i); - } - } - - sample(index, vec2coords) { - return `${this._samples}(vis_data_sampler_${index}, ${vec2coords})`; - } - - declare(shaderDataIndexToGlobalDataIndex) { - let samplers = 'uniform vec2 sampler_size;'; - for (let i = 0; i < shaderDataIndexToGlobalDataIndex.length; i++) { - if (shaderDataIndexToGlobalDataIndex[i] === -1) { - continue; - } - samplers += `uniform sampler2D vis_data_sampler_${i};`; - } - return samplers; - } - }, -}; - -})(OpenSeadragon); diff --git a/src/webgl/drawer.js b/src/webgl/drawer.js deleted file mode 100644 index 3b20f8ea..00000000 --- a/src/webgl/drawer.js +++ /dev/null @@ -1,289 +0,0 @@ - -/* - * OpenSeadragon - WebGLDrawer - * - * Copyright (C) 2010-2023 OpenSeadragon contributors - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * - Neither the name of CodePlex Foundation nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -(function( $ ){ - -/** - * @class WebGLDrawer - * @memberof OpenSeadragon - * @classdesc Default implementation of WebGLDrawer for an {@link OpenSeadragon.Viewer}. - * @param {Object} options - Options for this Drawer. - * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer. - * @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport. - * @param {Element} options.element - Parent element. - * @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details. - */ - -$.WebGL = class WebGL extends OpenSeadragon.DrawerBase { - constructor(options){ - super(options); - - this.destroyed = false; - // Add listeners for events that require modifying the scene or camera - this.viewer.addHandler("tile-ready", this._tileReadyHandler.bind(this)); - this.viewer.addHandler("image-unloaded", this.renderer.freeData.bind(this.renderer)); - } - - // Public API required by all Drawer implementations - /** - * Clean up the renderer, removing all resources - */ - destroy(){ - if(this.destroyed){ - return; - } - //todo - this.destroyed = true; - } - - // Public API required by all Drawer implementations - /** - * - * @returns true if the drawer supports rotation - */ - canRotate(){ - return true; - } - - // Public API required by all Drawer implementations - - /** - * @returns {Boolean} returns true if canvas and webgl are supported - */ - static isSupported(){ - return true; //todo - } - - getType() { - return 'universal_webgl'; - } - - /** - * create the HTML element (canvas in this case) that the image will be drawn into - * @returns {Element} the canvas to draw into - */ - createDrawingElement(){ - - const engine = new $.WebGLModule($.extend(this.options, { - uniqueId: "openseadragon", - })); - - engine.addRenderingSpecifications({ - shaders: { - renderShader: { - type: "identity", - dataReferences: [0], - } - } - }); - - engine.prepare(); - - const size = this._calculateCanvasSize(); - engine.init(size.x, size.y); - this.viewer.addHandler("resize", this._resizeRenderer.bind(this)); - this.renderer = engine; - return engine.canvas; - } - - /** - * - * @param {Array} tiledImages Array of TiledImage objects to draw - */ - draw(tiledImages){ - let viewport = { - bounds: this.viewport.getBoundsNoRotate(true), - center: this.viewport.getCenter(true), - rotation: this.viewport.getRotation(true) * Math.PI / 180, - zoom: this.viewport.getZoom(true) - }; - - let flipMultiplier = this.viewport.flipped ? -1 : 1; - // calculate view matrix for viewer - let posMatrix = $.Mat3.makeTranslation(-viewport.center.x, -viewport.center.y); - let scaleMatrix = $.Mat3.makeScaling(2 / viewport.bounds.width * flipMultiplier, -2 / viewport.bounds.height); - let rotMatrix = $.Mat3.makeRotation(-viewport.rotation); - let viewMatrix = scaleMatrix.multiply(rotMatrix).multiply(posMatrix); - - this.renderer.clear(); - this.renderer.setDataBlendingEnabled(false); - - //iterate over tiled images and draw each one using a two-pass rendering pipeline if needed - - let drawn = 0; - - for (const tiledImage of tiledImages) { - let tilesToDraw = tiledImage.getTilesToDraw(); - - if (tilesToDraw.length === 0) { - break; - } - - if (drawn === 1) { - this.renderer.setDataBlendingEnabled(true); - } - - let overallMatrix = viewMatrix; - let imageRotation = tiledImage.getRotation(true); - // if needed, handle the tiledImage being rotated - if( imageRotation % 360 !== 0){ - let imageRotationMatrix = $.Mat3.makeRotation(-imageRotation * Math.PI / 180); - let imageCenter = tiledImage.getBoundsNoRotate(true).getCenter(); - let t1 = $.Mat3.makeTranslation(imageCenter.x, imageCenter.y); - let t2 = $.Mat3.makeTranslation(-imageCenter.x, -imageCenter.y); - - // update the view matrix to account for this image's rotation - let localMatrix = t1.multiply(imageRotationMatrix).multiply(t2); - overallMatrix = viewMatrix.multiply(localMatrix); - } - - //todo better access to the rendering context - const shader = this.renderer.specification(0).shaders.renderShader._renderContext; - - // iterate over tiles and add data for each one to the buffers - for (let tileIndex = 0; tileIndex < tilesToDraw.length; tileIndex++){ - const tile = tilesToDraw[tileIndex].tile; - const matrix = this._getTileMatrix(tile, tiledImage, overallMatrix); - shader.opacity.set(tile.opacity * tiledImage.opacity); - - //todo pixelSize value (not yet memoized) - this.renderer.processData(tile.cacheKey, { - transform: matrix, zoom: - viewport.zoom, - pixelSize: 0 - }); - } - - // Fire tiled-image-drawn event. - // TODO: the image data may not be on the output canvas yet!! - if( this.viewer ){ - /** - * Raised when a tiled image is drawn to the canvas. Only valid - * for webgl drawer. - * - * @event tiled-image-drawn - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {Array} tiles - An array of Tile objects that were drawn. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.viewer.raiseEvent( 'tiled-image-drawn', { - tiledImage: tiledImage, - tiles: tilesToDraw.map(info => info.tile), - }); - } - - drawn++; - } - } - - /** - * Set the context2d imageSmoothingEnabled parameter - * @param {Boolean} enabled - */ - setImageSmoothingEnabled(enabled){ - //todo - // this._clippingContext.imageSmoothingEnabled = enabled; - // this._outputContext.imageSmoothingEnabled = enabled; - } - - // private - _getTileMatrix(tile, tiledImage, viewMatrix){ - // compute offsets that account for tile overlap; needed for calculating the transform matrix appropriately - let overlapFraction = this._calculateOverlapFraction(tile, tiledImage); - let xOffset = tile.positionedBounds.width * overlapFraction.x; - let yOffset = tile.positionedBounds.height * overlapFraction.y; - - // x, y, w, h in viewport coords - let x = tile.positionedBounds.x + (tile.x === 0 ? 0 : xOffset); - let y = tile.positionedBounds.y + (tile.y === 0 ? 0 : yOffset); - let right = tile.positionedBounds.x + tile.positionedBounds.width - (tile.isRightMost ? 0 : xOffset); - let bottom = tile.positionedBounds.y + tile.positionedBounds.height - (tile.isBottomMost ? 0 : yOffset); - let w = right - x; - let h = bottom - y; - - let matrix = new $.Mat3([ - w, 0, 0, - 0, h, 0, - x, y, 1, - ]); - - if(tile.flipped){ - // flip the tile around the center of the unit quad - let t1 = $.Mat3.makeTranslation(0.5, 0); - let t2 = $.Mat3.makeTranslation(-0.5, 0); - - // update the view matrix to account for this image's rotation - let localMatrix = t1.multiply($.Mat3.makeScaling(-1, 1)).multiply(t2); - matrix = matrix.multiply(localMatrix); - } - let overallMatrix = viewMatrix.multiply(matrix); - return overallMatrix.values; - } - - _resizeRenderer(){ - const size = this._calculateCanvasSize(); - this.renderer.setDimensions(0, 0, size.x, size.y); - } - - _imageUnloadedHandler(event){ - this.renderer.freeData(event.tile.cacheKey); - } - - _tileReadyHandler(event){ - //todo tile overlap - let tile = event.tile; - //todo fix cache system and then this line - //access by default raw tile data, and only access canvas if not cache set - let data = tile.cacheImageRecord ? tile.cacheImageRecord.getData() : tile.getCanvasContext().canvas; - this.renderer.loadData(tile.cacheKey, data, tile.sourceBounds.width, tile.sourceBounds.height); - } - - _calculateOverlapFraction(tile, tiledImage){ - let overlap = tiledImage.source.tileOverlap; - let nativeWidth = tile.sourceBounds.width; // in pixels - let nativeHeight = tile.sourceBounds.height; // in pixels - let overlapWidth = (tile.x === 0 ? 0 : overlap) + (tile.isRightMost ? 0 : overlap); // in pixels - let overlapHeight = (tile.y === 0 ? 0 : overlap) + (tile.isBottomMost ? 0 : overlap); // in pixels - let widthOverlapFraction = overlap / (nativeWidth + overlapWidth); // as a fraction of image including overlap - let heightOverlapFraction = overlap / (nativeHeight + overlapHeight); // as a fraction of image including overlap - return { - x: widthOverlapFraction, - y: heightOverlapFraction - }; - } -}; -}( OpenSeadragon )); diff --git a/src/webgl/plainShader.js b/src/webgl/plainShader.js deleted file mode 100644 index ed7e7f05..00000000 --- a/src/webgl/plainShader.js +++ /dev/null @@ -1,40 +0,0 @@ -(function($) { - /** - * Identity shader - * - * data reference must contain one index to the data to render using identity - */ - $.WebGLModule.IdentityLayer = class extends $.WebGLModule.ShaderLayer { - - static type() { - return "identity"; - } - - static name() { - return "Identity"; - } - - static description() { - return "shows the data AS-IS"; - } - - static sources() { - return [{ - acceptsChannelCount: (x) => x === 4, - description: "4d texture to render AS-IS" - }]; - } - - getFragmentShaderExecution() { - return `return ${this.sampleChannel("tile_texture_coords")};`; - } - }; - -//todo why cannot be inside object :/ -$.WebGLModule.IdentityLayer.defaultControls["use_channel0"] = { - required: "rgba" -}; - -$.WebGLModule.ShaderMediator.registerLayer($.WebGLModule.IdentityLayer); - -})(OpenSeadragon); diff --git a/src/webgl/renderer.js b/src/webgl/renderer.js deleted file mode 100644 index 33654dc3..00000000 --- a/src/webgl/renderer.js +++ /dev/null @@ -1,982 +0,0 @@ - - -(function($) { - - -/** - * Wrapping the funcionality of WebGL to be suitable for tile processing and rendering. - * Written by Aiosa - * @class OpenSeadragon.WebGLModule - * @memberOf OpenSeadragon - */ -$.WebGLModule = class extends $.EventSource { - /** - * @typedef {{ - * name: string, - * lossless: boolean, - * shaders: Object. - * }} OpenSeadragon.WebGLModule.RenderingConfig - * - * //use_channel[X] name - * @template {Object} TUseChannel - * //use_[fitler_name] - * @template {Object} TUseFilter - * @template {Object} TIControlConfig - * @typedef OpenSeadragon.WebGLModule.ShaderLayerParams - * @type {{TUseChannel,TUseFilter,TIControlConfig}} - * - * @typedef {{ - * name: string, - * type: string, - * visible: boolean, - * dataReferences: number[], - * params: OpenSeadragon.WebGLModule.ShaderLayerParams - * }} OpenSeadragon.WebGLModule.ShaderLayerConfig - * - * - * @typedef OpenSeadragon.WebGLModule.UIControlsRenderer - * @type function - * @param {string} title - * @param {string} html - * @param {string} dataId - * @param {boolean} isVisible - * @param {OpenSeadragon.WebGLModule.ShaderLayer} layer - * @param {boolean} wasErrorWhenLoading - */ - - - /** - * @param {object} incomingOptions - * @param {string} incomingOptions.htmlControlsId: where to render html controls, - * @param {string} incomingOptions.webGlPreferredVersion prefered WebGL version, for now "1.0" or "2.0" - * @param {OpenSeadragon.WebGLModule.UIControlsRenderer} incomingOptions.htmlShaderPartHeader function that generates particular layer HTML - * @param {boolean} incomingOptions.debug debug mode default false - * @param {function} incomingOptions.ready function called when ready - * @param {function} incomingOptions.resetCallback function called when user input changed, e.g. changed output of the current rendering - * @param {function} incomingOptions.visualisationInUse function called when a specification is initialized and run - * @param {function} incomingOptions.visualisationChanged function called when a visualization swap is performed: - * signature f({Visualization} oldVisualisation,{Visualization} newVisualisation) - * @constructor - * @memberOf OpenSeadragon.WebGLModule - */ - constructor(incomingOptions) { - super(); - - ///////////////////////////////////////////////////////////////////////////////// - ///////////// Default values overrideable from incomingOptions ///////////////// - ///////////////////////////////////////////////////////////////////////////////// - this.uniqueId = ""; - - //todo events instead - this.ready = function() { }; - this.htmlControlsId = null; - this.webGlPreferredVersion = "2.0"; - this.htmlShaderPartHeader = function(title, html, dataId, isVisible, layer, isControllable = true) { - return `
${title}
${html}
`; - }; - this.resetCallback = function() { }; - //called once a visualisation is compiled and linked (might not happen) - this.visualisationReady = function(i, visualisation) { }; - //called once a visualisation is switched to (including first run) - this.visualisationInUse = function(visualisation) { }; - this.visualisationChanged = function(oldVis, newVis) { }; - - - /** - * Debug mode. - * @member {boolean} - */ - this.debug = false; - - ///////////////////////////////////////////////////////////////////////////////// - ///////////// Incoming Values /////////////////////////////////////////////////// - ///////////////////////////////////////////////////////////////////////////////// - - // Assign from incoming terms - for (let key in incomingOptions) { - if (incomingOptions[key]) { - this[key] = incomingOptions[key]; - } - } - - if (!this.constructor.idPattern.test(this.uniqueId)) { - throw "$.WebGLModule: invalid ID! Id can contain only letters, numbers and underscore. ID: " + this.uniqueId; - } - - /** - * Current rendering context - * @member {OpenSeadragon.WebGLModule.WebGLImplementation} - */ - this.webglContext = null; - - /** - * WebGL context - * @member {WebGLRenderingContextBase} - */ - this.gl = null; - - ///////////////////////////////////////////////////////////////////////////////// - ///////////// Internals ///////////////////////////////////////////////////////// - ///////////////////////////////////////////////////////////////////////////////// - - this.reset(); - - try { - const canvas = document.createElement("canvas"); - for (let version of [this.webGlPreferredVersion, "2.0", "1.0"]) { - const Context = $.WebGLModule.determineContext(version); - let glContext = Context && Context.create(canvas); - - if (glContext) { - this.gl = glContext; - const contextOpts = incomingOptions[version] || {}; - const readGlProp = function(prop, defaultValue) { - return glContext[contextOpts[prop] || defaultValue] || glContext[defaultValue]; - }; - - /** - * @param {object} options - * @param {string} options.wrap texture wrap parameteri - * @param {string} options.magFilter texture filter parameteri - * @param {string} options.minFilter texture filter parameteri - * @param {string|WebGLModule.IDataLoader} options.dataLoader class name or implementation of a given loader - */ - const options = { - wrap: readGlProp("wrap", "MIRRORED_REPEAT"), - magFilter: readGlProp("magFilter", "LINEAR"), - minFilter: readGlProp("minFilter", "NEAREST"), - dataLoader: contextOpts.dataLoader || "TEXTURE_2D" - }; - this.webglContext = new Context(this, glContext, options); - } - } - - } catch (e) { - /** - * @event fatal-error - */ - this.raiseEvent('fatal-error', {message: "Unable to initialize the WebGL renderer.", - details: e}); - console.error(e); - return; - } - console.log(`WebGL ${this.webglContext.getVersion()} Rendering module (ID ${this.uniqueId})`); - } - - /** - * Reset the engine to the initial state - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - reset() { - this._unloadCurrentProgram(); - this._programConfigurations = []; - this._dataSources = []; - this._shaderDataIndexToGlobalDataIndex = []; - this._origDataSources = []; - this._programs = {}; - this._program = -1; - this._prepared = false; - this.running = false; - this._initialized = false; - } - - /** - * Check if prepare() was called. - * @return {boolean} - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - get isPrepared() { - return this._prepared; - } - - /** - * WebGL target canvas - * @return {HTMLCanvasElement} - */ - get canvas() { - return this.gl.canvas; - } - - /** - * WebGL active program - * @return {WebGLProgram} - */ - get program() { - return this._programs[this._program]; - } - - /** - * Check if init() was called. - * @return {boolean} - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - get isInitialized() { - return this._initialized; - } - - /** - * Change the dimensions, useful for borders, used by openSeadragonGL - * @instance - * @memberOf WebGLModule - */ - setDimensions(x, y, width, height) { - if (width === this.width && height === this.height) { - return; - } - - this.width = width; - this.height = height; - this.gl.canvas.width = width; - this.gl.canvas.height = height; - this.gl.viewport(x, y, width, height); - } - - /** - * Set program shaders. Vertex shader is set by default a square. - * @param {RenderingConfig} configurations - objects that define the what to render (see Readme) - * @return {boolean} true if loaded successfully - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - addRenderingSpecifications(...configurations) { - if (this._prepared) { - console.error("New specification cannot be introduced after the visualiser was prepared."); - return false; - } - for (let config of configurations) { - if (!config.shaders) { - console.warn("Invalid visualization: no shaders defined", config); - continue; - } - - let count = 0; - for (let sid in config.shaders) { - const shader = config.shaders[sid]; - if (!shader.params) { - shader.params = {}; - } - count++; - } - - if (count < 0) { - console.warn("Invalid configualization: no shader configuration present!", config); - continue; - } - this._programConfigurations.push(config); - } - return true; - } - - /** - * Runs a callback on each specification - * @param {function} call callback to perform on each specification (its object given as the only parameter) - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - foreachRenderingSpecification(call) { - this._programConfigurations.forEach(vis => call(vis)); - } - - /** - * Rebuild specification and update scene - * @param {string[]|undefined} order of shaders, ID's of data as defined in setup JSON, last element - * is rendered last (top) - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - rebuildSpecification(order = undefined) { - let vis = this._programConfigurations[this._program]; - - if (order) { - vis.order = order; - } - this._unloadCurrentProgram(); - this._specificationToProgram(vis, this._program); - this._forceSwitchShader(this._program); - } - - /** - * Get currently used specification - * @return {object} current specification - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - specification(index) { - return this._programConfigurations[Math.min(index, this._programConfigurations.length - 1)]; - } - - /** - * Get currently used specification ilayer.params,ndex - * @return {number} index of the current specification - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - currentSpecificationIndex() { - return this._program; - } - - /** - * Switch to program at index: this is the index (order) in which - * setShaders(...) was called. If you want to switch to shader that - * has been set with second setShaders(...) call, pass i=1. - * @param {Number} i program index or null if you wish to re-initialize the current one - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - useSpecification(i) { - if (!this._initialized) { - console.warn("$.WebGLModule::useSpecification(): not initialized."); - return; - } - if (this._program === i) { - return; - } - let oldIndex = this._program; - this._forceSwitchShader(i); - this.visualisationChanged(this._programConfigurations[oldIndex], this._programConfigurations[i]); - } - - /** - * Get a list of image pyramids used to compose the current active specification - * @instance - * @memberOf WebGLModule - */ - getSources() { - //return this._programConfigurations[this._program].dziExtendedUrl; - return this._dataSources; - } - - /** - * Supported: 'wrap', 'minFilter', 'magFilter' - * @param {string} name WebGL name of the parameter - * @param {GLuint} value - */ - setTextureParam(name, value) { - this.webglContext.texture.setTextureParam(name, value); - } - - /** - * @param id - * @param data - * @param width - * @param height - */ - loadData(id, data, width, height) { - this.webglContext.texture.load(this, id, data, width, height); - } - - /** - * Renders data using WebGL - * @param {string} id used in loadImage() - * - * @param {object} tileOpts - * @param {number} tileOpts.zoom value passed to the shaders as zoom_level - * @param {number} tileOpts.pixelSize value passed to the shaders as pixel_size_in_fragments - * @param {OpenSeadragon.Mat3} tileOpts.transform position of the rendered tile - * - * @instance - * @memberOf WebGLModule - */ - processData(id, tileOpts) { - this.webglContext.programUsed( - this.program, - this._programConfigurations[this._program], - id, - tileOpts - ); - - // if (this.debug) { - // //todo - // this._renderDebugIO(data, result); - // } - } - - freeData(id) { - - } - - /** - * Clear the output canvas - */ - clear() { - //todo: necessary? - this.gl.clearColor(0, 0, 0, 0); - this.gl.clear(this.gl.COLOR_BUFFER_BIT); - } - - /** - * Whether the webgl module renders UI - * @return {boolean|boolean} - * @instance - * @memberOf WebGLModule - */ - supportsHtmlControls() { - return typeof this.htmlControlsId === "string" && this.htmlControlsId.length > 0; - } - - /** - * Execute call on each visualization layer with no errors - * @param {object} vis current specification setup context - * @param {function} callback call to execute - * @param {function} onFail handle exception during execition - * @return {boolean} true if no exception occured - * @instance - * @memberOf WebGLModule - */ - static eachValidShaderLayer(vis, callback, - onFail = (layer, e) => { - layer.error = e.message; - console.error(e); - }) { - let shaders = vis.shaders; - let noError = true; - for (let key in shaders) { - let shader = shaders[key]; - - if (shader && !shader.error) { - try { - callback(shader); - } catch (e) { - if (!onFail) { - throw e; - } - onFail(shader, e); - noError = false; - } - } - } - return noError; - } - - /** - * Execute call on each _visible_ specification layer with no errors. - * Visible is subset of valid. - * @param {object} vis current specification setup context - * @param {function} callback call to execute - * @param {function} onFail handle exception during execition - * @return {boolean} true if no exception occured - * @instance - * @memberOf WebGLModule - */ - static eachVisibleShaderLayer(vis, callback, - onFail = (layer, e) => { - layer.error = e.message; - console.error(e); - }) { - - let shaders = vis.shaders; - let noError = true; - for (let key in shaders) { - //rendering == true means no error - let shader = shaders[key]; - if (shader && shader.rendering) { - try { - callback(shader); - } catch (e) { - if (!onFail) { - throw e; - } - onFail(shader, e); - noError = false; - } - } - } - return noError; - } - - ///////////////////////////////////////////////////////////////////////////////////// - //// YOU PROBABLY WANT TO READ FUNCTIONS BELOW SO YOU KNOW HOW TO SET UP YOUR SHADERS - //// BUT YOU SHOULD NOT CALL THEM DIRECTLY - ///////////////////////////////////////////////////////////////////////////////////// - - /** - * Get current program, reset if invalid - * @return {number} program index - */ - getCurrentProgramIndex() { - if (this._program < 0 || this._program >= this._programConfigurations.length) { - this._program = 0; - } - return this._program; - } - - /** - * Function to JSON.stringify replacer - * @param key key to the value - * @param value value to be exported - * @return {*} value if key passes exportable condition, undefined otherwise - */ - static jsonReplacer(key, value) { - return key.startsWith("_") || ["eventSource"].includes(key) ? undefined : value; - } - - /** - * For easy initialization, do both in once call. - * For separate initialization (prepare|init), see functions below. - * @param {string[]|undefined} dataSources a list of data identifiers available to the specifications - * - specification configurations should not reference data not present in this array - * - the module gives you current list of required subset of this list for particular active visualization goal - * @param width initialization width - * @param height initialization height - */ - prepareAndInit(dataSources = undefined, width = 1, height = 1) { - this.prepare(dataSources); - this.init(width, height); - } - - /** - * Prepares the WebGL wrapper for being initialized. It is separated from - * initialization as this must be finished before OSD is ready (we must be ready to draw when the data comes). - * The idea is to open the protocol for OSD in onPrepared. - * Shaders are fetched from `specification.url` parameter. - * - * @param {string[]|undefined} dataSources id's of data such that server can understand which image to send (usually paths) - * @param {number} visIndex index of the initial specification - */ - prepare(dataSources = undefined, visIndex = 0) { - if (this._prepared) { - console.error("Already prepared!"); - return; - } - - if (this._programConfigurations.length < 1) { - console.error("No specification specified!"); - /** - * @event fatal-error - */ - this.raiseEvent('fatal-error', {message: "No specification specified!", - details: "::prepare() called with no specification set."}); - return; - } - this._origDataSources = dataSources || []; - this._program = visIndex; - - this._prepared = true; - this.getCurrentProgramIndex(); //resets index - this._specificationToProgram(this._programConfigurations[this._program], this._program); - } - - /** - * Initialization. It is separated from preparation as this actually initiates the rendering, - * sometimes this can happen only when other things are ready. Must be performed after - * all the prepare() strategy finished: e.g. as onPrepared. Or use prepareAndInit(); - * - * @param {int} width width of the first tile going to be drawn - * @param {int} height height of the first tile going to be drawn - */ - init(width = 1, height = 1) { - if (!this._prepared) { - console.error("The viaGL was not yet prepared. Call prepare() before init()!"); - return; - } - if (this._initialized) { - console.error("Already initialized!"); - return; - } - this._initialized = true; - this.setDimensions(width, height); - - - //todo rotate anticlockwise to cull backfaces - this.gl.enable(this.gl.CULL_FACE); - this.gl.cullFace(this.gl.FRONT); - - this.running = true; - - this._forceSwitchShader(null); - this.ready(); - } - - setDataBlendingEnabled(enabled) { - if (enabled) { - this.gl.enable(this.gl.BLEND); - this.gl.blendEquation(this.gl.FUNC_ADD); - this.gl.blendFuncSeparate(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA, this.gl.ONE, this.gl.ONE); - } else { - this.gl.disable(this.gl.BLEND); - } - } - - /** - * Supported are two modes: show and blend - * show is the default option, stacking layers by generalized alpha blending - * blend is a custom alternative, default is a mask (remove background where foreground.a > 0.001) - * - * vec4 my_blend(vec4 foreground, vec4 background) { - * <> //here goes your blending code - * } - * - * @param code GLSL code to blend - must return vec4() and can use - * two variables: background, foreground - */ - setLayerBlending(code) { - this.webglContext.setBlendEquation(code); - this.rebuildSpecification(); - } - - ////////////////////////////////////////////////////////////////////////////// - ///////////// YOU PROBABLY DON'T WANT TO READ/CHANGE FUNCTIONS BELOW - ////////////////////////////////////////////////////////////////////////////// - - /** - * Forward glLoaded event to the active layer - * @param gl - * @param program - * @param vis - */ - glLoaded(gl, program, vis) { - $.WebGLModule.eachVisibleShaderLayer(vis, layer => layer._renderContext.glLoaded(program, gl)); - } - - /** - * Forward glDrawing event to the active layer - * @param gl - * @param program - * @param vis - * @param bounds - */ - glDrawing(gl, program, vis, bounds) { - $.WebGLModule.eachVisibleShaderLayer(vis, layer => layer._renderContext.glDrawing(program, gl)); - } - - /** - * Force switch shader (program), will reset even if the specified - * program is currently active, good if you need 'gl-loaded' to be - * invoked (e.g. some uniform variables changed) - * @param {Number} i program index or null if you wish to re-initialize the current one - * @param _reset - * @private - */ - _forceSwitchShader(i, _reset = true) { - if (isNaN(i) || i === null || i === undefined) { - i = this._program; - } - - if (i >= this._programConfigurations.length) { - console.error("Invalid specification index ", i, "trying to use index 0..."); - if (i === 0) { - return; - } - i = 0; - } - - let target = this._programConfigurations[i]; - if (!this._programs[i]) { - this._specificationToProgram(target, i); - } else if (i !== this._program) { - this._updateRequiredDataSources(target); - } - - this._program = i; - if (target.error) { - if (this.supportsHtmlControls()) { - this._loadHtml(i, this._program); - } - this._loadScript(i, this._program); - this.running = false; - if (this._programConfigurations.length < 2) { - /** - * @event fatal-error - */ - this.raiseEvent('fatal-error', {message: "The only rendering specification left is invalid!", target: target}); - } else { - /** - * @event error - */ - this.raiseEvent('error', {message: "Currently chosen rendering specification is not valid!", target: target}); - } - } else { - this.running = true; - if (this.supportsHtmlControls()) { - this._loadHtml(i, this._program); - } - this._loadDebugInfo(); - if (!this._loadScript(i, this._program)) { - if (!_reset) { - throw "Could not build visualization"; - } - this._forceSwitchShader(i, false); //force reset in errors - return; - } - this.webglContext.programLoaded(this._programs[i], target); - } - } - - _unloadCurrentProgram() { - let program = this._programs && this._programs[this._program]; - if (program) { - //must remove before attaching new - this._detachShader(program, "VERTEX_SHADER"); - this._detachShader(program, "FRAGMENT_SHADER"); - } - } - - _loadHtml(visId) { - let htmlControls = document.getElementById(this.htmlControlsId); - htmlControls.innerHTML = this._programConfigurations[visId]._built["html"]; - } - - _loadScript(visId) { - return $.WebGLModule.eachValidShaderLayer(this._programConfigurations[visId], layer => layer._renderContext.init()); - } - - _getDebugInfoPanel() { - return `
-WebGL Processing I/O (debug mode) -
-Input:
No input.

-Output:
No output.
`; - } - - _loadDebugInfo() { - if (!this.debug) { - return; - } - - let container = document.getElementById(`test-${this.uniqueId}-webgl`); - if (!container) { - if (!this.htmlControlsId) { - document.body.innerHTML += `
${this._getDebugInfoPanel()}
`; - } else { - //safe as we do this before handlers are attached - document.getElementById(this.htmlControlsId).parentElement.innerHTML += `
${this._getDebugInfoPanel()}
`; - } - } - } - - _renderDebugIO(inputData, outputData) { - let input = document.getElementById(`test-${this.uniqueId}-webgl-input`); - let output = document.getElementById(`test-${this.uniqueId}-webgl-output`); - - input.innerHTML = ""; - input.append($.WebGLModule.Loaders.dataAsHtmlElement(inputData)); - - if (outputData) { - output.innerHTML = ""; - if (!this._ocanvas) { - this._ocanvas = document.createElement("canvas"); - } - this._ocanvas.width = outputData.width; - this._ocanvas.height = outputData.height; - let octx = this._ocanvas.getContext('2d'); - octx.drawImage(outputData, 0, 0); - output.append(this._ocanvas); - } else { - output.innerHTML = "No output!"; - } - } - - _buildFailed(specification, error) { - console.error(error); - specification.error = "Failed to compose this specification."; - specification.desc = error; - } - - _buildSpecification(order, specification) { - try { - let data = this.webglContext.compileSpecification(order, specification, - this._shaderDataIndexToGlobalDataIndex, this.supportsHtmlControls()); - - if (data.usableShaders < 1) { - this._buildFailed(specification, `Empty specification: no valid specification has been specified. -
Specification setup:
${JSON.stringify(specification, $.WebGLModule.jsonReplacer)} -
Dynamic shader data:
${JSON.stringify(specification.data)}`); - return null; - } - data.dziExtendedUrl = data.dataUrls.join(","); - specification._built = data; - - //preventive - delete specification.error; - delete specification.desc; - return data; - } catch (error) { - this._buildFailed(specification, error); - } - return null; - } - - _detachShader(program, type) { - let shader = program[type]; - if (shader) { - this.gl.detachShader(program, shader); - this.gl.deleteShader(shader); - program[type] = null; - } - } - - _specificationToProgram(vis, idx) { - if (!vis._built) { - vis._built = {}; - } - - this._updateRequiredDataSources(vis); - this._processSpecification(vis, idx); - return idx; - } - - _initializeShaderFactory(ShaderFactoryClass, layer, idx) { - if (!ShaderFactoryClass) { - layer.error = "Unknown layer type."; - layer.desc = `The layer type '${layer.type}' has no associated factory. Missing in 'shaderSources'.`; - console.warn("Skipping layer " + layer.name); - return; - } - layer._index = idx; - layer.visible = layer.visible === undefined ? true : layer.visible; - layer._renderContext = new ShaderFactoryClass(`${this.uniqueId}${idx}`, layer.params || {}, { - layer: layer, - webgl: this.webglContext, - invalidate: this.resetCallback, - rebuild: this.rebuildSpecification.bind(this, undefined) - }); - } - - _updateRequiredDataSources(specs) { - //for now just request all data, later decide in the context on what to really send - //might in the future decide to only request used data, now not supported - let usedIds = new Set(); - for (let key in specs.shaders) { - let layer = specs.shaders[key]; - if (layer) { - for (let x of layer.dataReferences) { - usedIds.add(x); - } - } - } - usedIds = [...usedIds].sort(); - this._dataSources = []; - - while (usedIds[usedIds.length - 1] >= this._origDataSources.length) { - //make sure values are set if user did not provide - this._origDataSources.push("__generated_do_not_use__"); - } - - this._shaderDataIndexToGlobalDataIndex = new Array( - Math.max(this._origDataSources.length, usedIds[usedIds.length - 1]) - ).fill(-1); - - for (let id of usedIds) { - this._shaderDataIndexToGlobalDataIndex[id] = this._dataSources.length; - this._dataSources.push(this._origDataSources[id]); - while (id > this._shaderDataIndexToGlobalDataIndex.length) { - this._shaderDataIndexToGlobalDataIndex.push(-1); - } - } - } - - _processSpecification(spec, idx) { - let gl = this.gl, - err = function(message, description) { - spec.error = message; - spec.desc = description; - }; - - let program; - - if (!this._programs[idx]) { - program = gl.createProgram(); - this._programs[idx] = program; - - let index = 0; - //init shader factories and unique id's - for (let key in spec.shaders) { - let layer = spec.shaders[key]; - if (layer) { - let ShaderFactoryClass = $.WebGLModule.ShaderMediator.getClass(layer.type); - if (layer.type === "none") { - continue; - } - this._initializeShaderFactory(ShaderFactoryClass, layer, index++); - } - } - } else { - program = this._programs[idx]; - for (let key in spec.shaders) { - let layer = spec.shaders[key]; - - if (layer) { - if (!layer.error && - layer._renderContext && - layer._renderContext.constructor.type() === layer.type) { - continue; - } - delete layer.error; - delete layer.desc; - if (layer.type === "none") { - continue; - } - let ShaderFactoryClass = $.WebGLModule.ShaderMediator.getClass(layer.type); - this._initializeShaderFactory(ShaderFactoryClass, layer, layer._index); - } - } - } - - if (!Array.isArray(spec.order) || spec.order.length < 1) { - spec.order = Object.keys(spec.shaders); - } - - this._buildSpecification(spec.order, spec); - - if (spec.error) { - this.visualisationReady(idx, spec); - return; - } - - this.constructor.compileShader(gl, program, - spec._built.vertexShader, spec._built.fragmentShader, err, this.debug); - this.visualisationReady(idx, spec); - } - - static compileShader(gl, program, VS, FS, onError, isDebugMode) { - function ok (kind, status, value, sh) { - if (!gl['get' + kind + 'Parameter'](value, gl[status + '_STATUS'])) { - console.error((sh || 'LINK') + ':\n' + gl['get' + kind + 'InfoLog'](value)); - return false; - } - return true; - } - - function useShader(gl, program, data, type) { - let shader = gl.createShader(gl[type]); - gl.shaderSource(shader, data); - gl.compileShader(shader); - gl.attachShader(program, shader); - program[type] = shader; - return ok('Shader', 'COMPILE', shader, type); - } - - function numberLines(str) { - //https://stackoverflow.com/questions/49714971/how-to-add-line-numbers-to-beginning-of-each-line-in-string-in-javascript - return str.split('\n').map((line, index) => `${index + 1} ${line}`).join('\n'); - } - - if (!useShader(gl, program, VS, 'VERTEX_SHADER') || - !useShader(gl, program, FS, 'FRAGMENT_SHADER')) { - onError("Unable to use this specification.", - "Compilation of shader failed. For more information, see logs in the console."); - console.warn("VERTEX SHADER\n", numberLines( VS )); - console.warn("FRAGMENT SHADER\n", numberLines( FS )); - } else { - gl.linkProgram(program); - if (!ok('Program', 'LINK', program)) { - onError("Unable to use this specification.", - "Linking of shader failed. For more information, see logs in the console."); - } else { //if (isDebugMode) { //todo testing - console.info("FRAGMENT SHADER\n", numberLines( FS )); - } - } - } -}; - -/** - * ID pattern allowed for module, ID's are used in GLSL - * to distinguish uniquely between static generated code parts - * @type {RegExp} - */ -$.WebGLModule.idPattern = /[0-9a-zA-Z_]*/; - -})(OpenSeadragon); diff --git a/src/webgl/shaderLayer.js b/src/webgl/shaderLayer.js deleted file mode 100644 index 23ffd4eb..00000000 --- a/src/webgl/shaderLayer.js +++ /dev/null @@ -1,1305 +0,0 @@ -(function($) { - - /** - * Shader sharing point - * @class OpenSeadragon.WebGLModule.ShaderMediator - */ -$.WebGLModule.ShaderMediator = class { - - /** - * Register shader - * @param {function} LayerRendererClass class extends OpenSeadragon.WebGLModule.ShaderLayer - */ - static registerLayer(LayerRendererClass) { - //todo why not hasOwnProperty check allowed by syntax checker - // if (this._layers.hasOwnProperty(LayerRendererClass.type())) { - // console.warn("Registering an already existing layer renderer:", LayerRendererClass.type()); - // } - // if (!$.WebGLModule.ShaderLayer.isPrototypeOf(LayerRendererClass)) { - // throw `${LayerRendererClass} does not inherit from ShaderLayer!`; - // } - this._layers[LayerRendererClass.type()] = LayerRendererClass; - } - - /** - * Get the shader class by type id - * @param {string} id - * @return {function} class extends OpenSeadragon.WebGLModule.ShaderLayer - */ - static getClass(id) { - return this._layers[id]; - } - - /** - * Get all available shaders - * @return {function[]} classes that extend OpenSeadragon.WebGLModule.ShaderLayer - */ - static availableShaders() { - return Object.values(this._layers); - } -}; -//todo why cannot be inside object :/ -$.WebGLModule.ShaderMediator._layers = {}; - - -/** - * Abstract interface to any Shader. - * @abstract - */ -$.WebGLModule.ShaderLayer = class { - - /** - * Override **static** type definition - * The class must be registered using the type - * @returns {string} unique id under which is the shader registered - */ - static type() { - throw "ShaderLayer::type() Type must be specified!"; - } - - /** - * Override **static** name definition - * @returns {string} name of the shader (user-friendly) - */ - static name() { - throw "ShaderLayer::name() Name must be specified!"; - } - - /** - * Provide description - * @returns {string} optional description - */ - static description() { - return "ShaderLayer::description() WebGL shader must provide description."; - } - - /** - * Declare the number of data sources it reads from (how many dataSources indexes should the shader contain) - * @return {Array.} array of source specifications: - * acceptsChannelCount: predicate that evaluates whether given number of channels (argument) is acceptable - * [optional] description: the description of the source - what it is being used for - */ - static sources() { - throw "ShaderLayer::sources() Shader must specify channel acceptance predicates for each source it uses!"; - } - - /** - * Global supported options - * @param {string} id unique ID among all webgl instances and shaders - * @param {OpenSeadragon.WebGLModule.ShaderLayerParams} options - * options.channel: "r", "g" or "b" channel to sample, default "r" - * options.use_mode: blending mode - default alpha ("show"), custom blending ("mask") and clipping mask blend ("mask_clip") - * options.use_[*]: filtering, gamma/exposure/logscale with a float filter parameter (e.g. "use_gamma" : 1.5) - * @param {object} privateOptions options that should not be touched, necessary for linking the layer to the core - */ - constructor(id, options, privateOptions) { - this.uid = id; - this._setContextShaderLayer(privateOptions.layer); - this.webglContext = privateOptions.webgl; - this.invalidate = privateOptions.invalidate; - //use with care... - this._rebuild = privateOptions.rebuild; - - this._buildControls(options); - this.resetChannel(options); - this.resetMode(options); - } - - /** - * Code placed outside fragment shader's main(...). - * By default, it includes all definitions of - * controls you defined in defaultControls - * - * NOTE THAT ANY VARIABLE NAME - * WITHIN THE GLOBAL SPACE MUST BE - * ESCAPED WITH UNIQUE ID: this.uid - * - * DO NOT SAMPLE TEXTURE MANUALLY: use this.sampleChannel(...) to generate the code - * - * WHEN OVERRIDING, INCLUDE THE OUTPUT OF THIS METHOD AT THE BEGINNING OF THE NEW OUTPUT. - * - * @return {string} - */ - getFragmentShaderDefinition() { - let controls = this.constructor.defaultControls, - html = []; - for (let control in controls) { - if (control.startsWith("use_")) { - continue; - } - - const controlObject = this[control]; - if (controlObject) { - let code = controlObject.define(); - if (code) { - code = code.trim(); - html.push(code); - } - } - } - return html.join("\n"); - } - - /** - * Code executed to create the output color. The code - * must always return a vec4 value, otherwise the visualization - * will fail to compile (this code actually runs inside a vec4 function). - * - * DO NOT SAMPLE TEXTURE MANUALLY: use this.sampleChannel(...) to generate the code - * - * @return {string} - */ - getFragmentShaderExecution() { - throw "ShaderLayer::getFragmentShaderExecution must be implemented!"; - } - - /** - * Called when an image is rendered - * @param {WebGLProgram} program WebglProgram instance - * @param {WebGLRenderingContextBase} gl - */ - glDrawing(program, gl) { - let controls = this.constructor.defaultControls; - for (let control in controls) { - if (control.startsWith("use_")) { - continue; - } - - const controlObject = this[control]; - if (controlObject) { - controlObject.glDrawing(program, gl); - } - } - } - - /** - * Called when associated webgl program is switched to - * @param {WebGLProgram} program WebglProgram instance - * @param {WebGLRenderingContextBase} gl WebGL Context - */ - glLoaded(program, gl) { - let controls = this.constructor.defaultControls; - for (let control in controls) { - if (control.startsWith("use_")) { - continue; - } - - const controlObject = this[control]; - if (controlObject) { - controlObject.glLoaded(program, gl); - } - } - } - - /** - * This function is called once at - * the beginning of the layer use - * (might be multiple times), after htmlControls() - */ - init() { - let controls = this.constructor.defaultControls; - for (let control in controls) { - if (control.startsWith("use_")) { - continue; - } - - const controlObject = this[control]; - if (controlObject) { - controlObject.init(); - } - } - } - - /** - * Get the shader UI controls - * @return {string} HTML controls for the particular shader - */ - htmlControls() { - let controls = this.constructor.defaultControls, - html = []; - for (let control in controls) { - if (control.startsWith("use_")) { - continue; - } - - const controlObject = this[control]; - if (controlObject) { - html.push(controlObject.toHtml(true)); - } - } - return html.join(""); - } - - /** - * Include GLSL shader code on global scope - * (e.g. define function that is repeatedly used) - * does not have to use unique ID extended names as this code is included only once - * @param {string} key a key under which is the code stored, so that the same key is not loaded twice - * @param {string} code GLSL code to add to the shader - */ - includeGlobalCode(key, code) { - let container = this.constructor.__globalIncludes; - if (!container[key]) { - container[key] = code; - } - } - - /** - * Parses value to a float string representation with given precision (length after decimal) - * @param {number} value value to convert - * @param {number} defaultValue default value on failure - * @param {number} precisionLen number of decimals - * @return {string} - */ - toShaderFloatString(value, defaultValue, precisionLen = 5) { - return this.constructor.toShaderFloatString(value, defaultValue, precisionLen); - } - - /** - * Parses value to a float string representation with given precision (length after decimal) - * @param {number} value value to convert - * @param {number} defaultValue default value on failure - * @param {number} precisionLen number of decimals - * @return {string} - */ - static toShaderFloatString(value, defaultValue, precisionLen = 5) { - if (!Number.isInteger(precisionLen) || precisionLen < 0 || precisionLen > 9) { - precisionLen = 5; - } - try { - return value.toFixed(precisionLen); - } catch (e) { - return defaultValue.toFixed(precisionLen); - } - } - - /** - * Sample only one channel (which is defined in options) - * @param {string} textureCoords valid GLSL vec2 object as string - * @param {number} otherDataIndex index of the data in self.dataReference JSON array - * @param {boolean} raw whether to output raw value from the texture (do not apply filters) - * @return {string} code for appropriate texture sampling within the shader, - * where only one channel is extracted or float with zero value if - * the reference is not valid - */ - sampleChannel(textureCoords, otherDataIndex = 0, raw = false) { - let refs = this.__visualisationLayer.dataReferences; - const chan = this.__channels[otherDataIndex]; - - if (otherDataIndex >= refs.length) { - switch (chan.length) { - case 1: return ".0"; - case 2: return "vec2(.0)"; - case 3: return "vec3(.0)"; - default: - return 'vec4(0.0)'; - } - } - let sampled = `${this.webglContext.texture.sample(refs[otherDataIndex], textureCoords)}.${chan}`; - // if (raw) return sampled; - // return this.filter(sampled); - return sampled; - } - - /** - * For error detection, how many textures are available - * @return {number} number of textures available - */ - dataSourcesCount() { - return this.__visualisationLayer.dataReferences.length; - } - - /** - * Load value, useful for controls value caching - * @param {string} name value name - * @param {string} defaultValue default value if no stored value available - * @return {string} stored value or default value - */ - loadProperty(name, defaultValue) { - let selfType = this.constructor.type(); - if (!this.__visualisationLayer) { - return defaultValue; - } - - const value = this.__visualisationLayer.cache[selfType][name]; - return value === undefined ? defaultValue : value; - } - - /** - * Store value, useful for controls value caching - * @param {string} name value name - * @param {*} value value - */ - storeProperty(name, value) { - this.__visualisationLayer.cache[this.constructor.type()][name] = value; - } - - /** - * Evaluates option flag, e.g. any value that indicates boolean 'true' - * @param {*} value value to interpret - * @return {boolean} true if the value is considered boolean 'true' - */ - isFlag(value) { - return value === "1" || value === true || value === "true"; - } - - isFlagOrMissing(value) { - return value === undefined || this.isFlag(value); - } - - /** - * Get the mode we operate in - * @return {string} mode - */ - get mode() { - return this._mode; - } - - /** - * Returns number of textures available to this shader - * @return {number} number of textures available - */ - get texturesCount() { - return this.__visualisationLayer.dataReferences.length; - } - - /** - * Set sampling channel - * @param {object} options - * @param {string} options.use_channel[X] chanel swizzling definition to sample - */ - resetChannel(options) { - const parseChannel = (name, def, sourceDef) => { - const predefined = this.constructor.defaultControls[name]; - - if (options[name] || predefined) { - let channel = predefined ? (predefined.required ? predefined.required : predefined.default) : undefined; - if (!channel) { - channel = this.loadProperty(name, options[name]); - } - - if (!channel || typeof channel !== "string" || this.constructor.__chanPattern.exec(channel) === null) { - console.warn(`Invalid channel '${name}'. Will use channel '${def}'.`, channel, options); - this.storeProperty(name, "r"); - channel = def; - } - - if (!sourceDef.acceptsChannelCount(channel.length)) { - throw `${this.constructor.name()} does not support channel length for channel: ${channel}`; - } - - if (channel !== options[name]) { - this.storeProperty(name, channel); - } - return channel; - } - return def; - }; - this.__channels = this.constructor.sources().map((source, i) => parseChannel(`use_channel${i}`, "r", source)); - } - - /** - * Set blending mode - * @param {object} options - * @param {string} options.use_mode blending mode to use: "show" or "mask" - */ - resetMode(options) { - const predefined = this.constructor.defaultControls.use_mode; - if (options["use_mode"]) { - this._mode = predefined && predefined.required; - if (!this._mode) { - this._mode = this.loadProperty("use_mode", options.use_mode); - } - - if (this._mode !== options.use_mode) { - this.storeProperty("use_mode", this._mode); - } - } else { - this._mode = predefined ? (predefined.default || "show") : "show"; - } - - this.__mode = this.constructor.modes[this._mode] || "show"; - } - - //////////////////////////////////// - ////////// PRIVATE ///////////////// - //////////////////////////////////// - - - _buildControls(options) { - let controls = this.constructor.defaultControls; - - if (controls.opacity === undefined || (typeof controls.opacity === "object" && !controls.opacity.accepts("float"))) { - controls.opacity = { - default: {type: "range", default: 1, min: 0, max: 1, step: 0.1, title: "Opacity: "}, - accepts: (type, instance) => type === "float" - }; - } - - for (let control in controls) { - let buildContext = controls[control]; - - if (buildContext) { - if (control.startsWith("use_")) { - continue; - } - - this[control] = $.WebGLModule.UIControls.build(this, control, options[control], - buildContext.default, buildContext.accepts, buildContext.required); - } - } - } - - _setContextShaderLayer(visualisationLayer) { - this.__visualisationLayer = visualisationLayer; - if (!this.__visualisationLayer.cache) { - this.__visualisationLayer.cache = {}; - } - if (!this.__visualisationLayer.cache[this.constructor.type()]) { - this.__visualisationLayer.cache[this.constructor.type()] = {}; - } - } -}; - -/** - * Declare supported controls by a particular shader - * each controls is automatically created for the shader - * and this[controlId] instance set - * structure: - * { - * controlId: { - default: {type: <>, title: <>, interactive: true|false...}, - accepts: (type, instance) => <>, - required: {type: <> ...} [OPTIONAL] - * }, ... - * } - * - * use: controlId: false to disable a specific control (e.g. all shaders - * support opacity by default - use to remove this feature) - * - * - * Additionally, use_[...] value can be specified, such controls enable shader - * to specify default or required values for built-in use_[...] params. example: - * { - * use_channel0: { - * default: "bg" - * }, - * use_channel1: { - * required: "rg" - * }, - * use_gamma: { - * default: 0.5 - * } - * } - * reads by default for texture 1 channels 'bg', second texture is always forced to read 'rg', - * textures apply gamma filter with 0.5 by default if not overridden - * todo: allow to use_[filter][X] to distinguish between textures - * - * @member {object} - */ -$.WebGLModule.ShaderLayer.defaultControls = {}; - - -/** - * todo make blending more 'nice' - * Available use_mode modes - * @type {{show: string, mask: string}} - */ -$.WebGLModule.ShaderLayer.modes = { - show: "show", - mask: "blend" -}; -$.WebGLModule.ShaderLayer.modes["mask_clip"] = "blend_clip"; //todo parser error not camel case -$.WebGLModule.ShaderLayer.__globalIncludes = {}; -$.WebGLModule.ShaderLayer.__chanPattern = new RegExp('[rgba]{1,4}'); - -/** - * Factory Manager for predefined UIControls - * - you can manage all your UI control logic within your shader implementation - * and not to touch this class at all, but here you will find some most common - * or some advanced controls ready to use, simple and powerful - * - registering an IComponent implementation (or an UiElement) in the factory results in its support - * among all the shaders (given the GLSL type, result of sample(...) matches). - * - UiElements are objects to create simple controls quickly and get rid of code duplicity, - * for more info @see OpenSeadragon.WebGLModule.UIControls.register() - * @class OpenSeadragon.WebGLModule.UIControls - */ -$.WebGLModule.UIControls = class { - - /** - * Get all available control types - * @return {string[]} array of available control types - */ - static types() { - return Object.keys(this._items).concat(Object.keys(this._impls)); - } - - /** - * Get an element used to create simple controls, if you want - * an implementation of the controls themselves (IControl), use build(...) to instantiate - * @param {string} id type of the control - * @return {*} - */ - static getUiElement(id) { - let ctrl = this._items[id]; - if (!ctrl) { - console.error("Invalid control: " + id); - ctrl = this._items["number"]; - } - return ctrl; - } - - /** - * Get an element used to create advanced controls, if you want - * an implementation of simple controls, use build(...) to instantiate - * @param {string} id type of the control - * @return {OpenSeadragon.WebGLModule.UIControls.IControl} - */ - static getUiClass(id) { - let ctrl = this._impls[id]; - if (!ctrl) { - console.error("Invalid control: " + id); - ctrl = this._impls["colormap"]; - } - return ctrl; - } - - /** - * Build UI control object based on given parameters - * @param {OpenSeadragon.WebGLModule.ShaderLayer} context owner of the control - * @param {string} name name used for the layer, should be unique among different context types - * @param {object|*} params parameters passed to the control (defined by the control) or set as default value if not object - * @param {object} defaultParams default parameters that the shader might leverage above defaults of the control itself - * @param {function} accepts required GLSL type of the control predicate, for compatibility typechecking - * @param {object} requiredParams parameters that override anything sent by user or present by defaultParams - * @return {OpenSeadragon.WebGLModule.UIControls.IControl} - */ - static build(context, name, params, defaultParams = {}, accepts = () => true, requiredParams = {}) { - //if not an object, but a value: make it the default one - if (!(typeof params === 'object')) { - params = {default: params}; - } - let originalType = defaultParams.type; - - defaultParams = $.extend(true, {}, defaultParams, params, requiredParams); - - if (!this._items[defaultParams.type]) { - if (!this._impls[defaultParams.type]) { - return this._buildFallback(defaultParams.type, originalType, context, - name, params, defaultParams, accepts, requiredParams); - } - - let cls = new this._impls[defaultParams.type]( - context, name, `${name}_${context.uid}`, defaultParams - ); - if (accepts(cls.type, cls)) { - return cls; - } - return this._buildFallback(defaultParams.type, originalType, context, - name, params, defaultParams, accepts, requiredParams); - } else { - let contextComponent = this.getUiElement(defaultParams.type); - let comp = new $.WebGLModule.UIControls.SimpleUIControl( - context, name, `${name}_${context.uid}`, defaultParams, contextComponent - ); - if (accepts(comp.type, comp)) { - return comp; - } - return this._buildFallback(contextComponent.glType, originalType, context, - name, params, defaultParams, accepts, requiredParams); - } - } - - /** - * Register simple UI element by providing necessary object - * implementation: - * { defaults: function() {...}, // object with all default values for all supported parameters - html: function(uniqueId, params, css="") {...}, //how the HTML UI controls look like - glUniformFunName: function() {...}, //what function webGL uses to pass this attribute to GPU - decode: function(fromValue) {...}, //parse value obtained from HTML controls into something - gl[glUniformFunName()](...) can pass to GPU - glType: //what's the type of this parameter wrt. GLSL: int? vec3? - * @param type the identifier under which is this control used: lookup made against params.type - * @param uiElement the object to register, fulfilling the above-described contract - */ - static register(type, uiElement) { - function check(el, prop, desc) { - if (!el[prop]) { - console.warn(`Skipping UI control '${type}' due to '${prop}': missing ${desc}.`); - return false; - } - return true; - } - - if (check(uiElement, "defaults", "defaults():object") && - check(uiElement, "html", "html(uniqueId, params, css):htmlString") && - check(uiElement, "glUniformFunName", "glUniformFunName():string") && - check(uiElement, "decode", "decode(encodedValue):") && - check(uiElement, "normalize", "normalize(value, params):") && - check(uiElement, "sample", "sample(value, valueGlType):glslString") && - check(uiElement, "glType", "glType:string") - ) { - uiElement.prototype.getName = () => type; - if (this._items[type]) { - console.warn("Registering an already existing control component: ", type); - } - uiElement["uiType"] = type; - this._items[type] = uiElement; - } - } - - /** - * Register class as a UI control - * @param {string} type unique control name / identifier - * @param {OpenSeadragon.WebGLModule.UIControls.IControl} cls to register, implementation class of the controls - */ - static registerClass(type, cls) { - //todo not really possible with syntax checker :/ - // if ($.WebGLModule.UIControls.IControl.isPrototypeOf(cls)) { - cls.prototype.getName = () => type; - - if (this._items[type]) { - console.warn("Registering an already existing control component: ", type); - } - cls._uiType = type; - this._impls[type] = cls; - // } else { - // console.warn(`Skipping UI control '${type}': does not inherit from $.WebGLModule.UIControls.IControl.`); - // } - } - - ///////////////////////// - /////// PRIVATE ///////// - ///////////////////////// - - - static _buildFallback(newType, originalType, context, name, params, defaultParams, requiredType, requiredParams) { - //repeated check when building object from type - - params.interactive = false; - if (originalType === newType) { //if default and new equal, fail - recursion will not help - console.error(`Invalid parameter in shader '${params.type}': the parameter could not be built.`); - return undefined; - } else { //otherwise try to build with originalType (default) - params.type = originalType; - console.warn("Incompatible UI control type '" + newType + "': making the input non-interactive."); - return this.build(context, name, params, defaultParams, requiredType, requiredParams); - } - } -}; - -//implementation of UI control classes -//more complex functionality -$.WebGLModule.UIControls._impls = { - //colormap: $.WebGLModule.UIControls.ColorMap -}; -//implementation of UI control objects -//simple functionality -$.WebGLModule.UIControls._items = { - number: { - defaults: function() { - return {title: "Number", interactive: true, default: 0, min: 0, max: 100, step: 1}; - }, - html: function(uniqueId, params, css = "") { - let title = params.title ? ` ${params.title}` : ""; - return `${title}`; - }, - glUniformFunName: function() { - return "uniform1f"; - }, - decode: function(fromValue) { - return Number.parseFloat(fromValue); - }, - normalize: function(value, params) { - return (value - params.min) / (params.max - params.min); - }, - sample: function(name, ratio) { - return name; - }, - glType: "float", - uiType: "number" - }, - - range: { - defaults: function() { - return {title: "Range", interactive: true, default: 0, min: 0, max: 100, step: 1}; - }, - html: function(uniqueId, params, css = "") { - let title = params.title ? ` ${params.title}` : ""; - return `${title}`; - }, - glUniformFunName: function() { - return "uniform1f"; - }, - decode: function(fromValue) { - return Number.parseFloat(fromValue); - }, - normalize: function(value, params) { - return (value - params.min) / (params.max - params.min); - }, - sample: function(name, ratio) { - return name; - }, - glType: "float", - uiType: "range" - }, - - color: { - defaults: function() { - return { title: "Color", interactive: true, default: "#fff900" }; - }, - html: function(uniqueId, params, css = "") { - let title = params.title ? ` ${params.title}` : ""; - return `${title}`; - }, - glUniformFunName: function() { - return "uniform3fv"; - }, - decode: function(fromValue) { - try { - let index = fromValue.startsWith("#") ? 1 : 0; - return [ - parseInt(fromValue.slice(index, index + 2), 16) / 255, - parseInt(fromValue.slice(index + 2, index + 4), 16) / 255, - parseInt(fromValue.slice(index + 4, index + 6), 16) / 255 - ]; - } catch (e) { - return [0, 0, 0]; - } - }, - normalize: function(value, params) { - return value; - }, - sample: function(name, ratio) { - return name; - }, - glType: "vec3", - uiType: "color" - }, - - bool: { - defaults: function() { - return { title: "Checkbox", interactive: true, default: true }; - }, - html: function(uniqueId, params, css = "") { - let title = params.title ? ` ${params.title}` : ""; - let value = this.decode(params.default) ? "checked" : ""; - //note a bit dirty, but works :) - we want uniform access to 'value' property of all inputs - return `${title}`; - }, - glUniformFunName: function() { - return "uniform1i"; - }, - decode: function(fromValue) { - return fromValue && fromValue !== "false" ? 1 : 0; - }, - normalize: function(value, params) { - return value; - }, - sample: function(name, ratio) { - return name; - }, - glType: "bool", - uiType: "bool" - } -}; - -/** - * @interface - */ -$.WebGLModule.UIControls.IControl = class { - - /** - * Sets common properties needed to create the controls: - * this.context @extends WebGLModule.ShaderLayer - owner context - * this.name - name of the parameter for this.context.[load/store]Property(...) call - * this.id - unique ID for HTML id attribute, to be able to locate controls in DOM, - * created as ${uniq}${name}-${context.uid} - * this.webGLVariableName - unique webgl uniform variable name, to not to cause conflicts - * - * If extended (class-based definition, see registerCass) children should define constructor as - * - * @example - * constructor(context, name, webGLVariableName, params) { - * super(context, name, webGLVariableName); - * ... - * //possibly make use of params: - * this.params = this.getParams(params); - * - * //now access params: - * this.params... - * } - * - * @param {WebGLModule.ShaderLayer} context shader context owning this control - * @param {string} name name of the control (key to the params in the shader configuration) - * @param {string} webGLVariableName configuration parameters, - * depending on the params.type field (the only one required) - * @param {string} uniq another element to construct the DOM id from, mostly for compound controls - */ - constructor(context, name, webGLVariableName, uniq = "") { - this.context = context; - this.id = `${uniq}${name}-${context.uid}`; - this.name = name; - this.webGLVariableName = webGLVariableName; - this._params = {}; - this.__onchange = {}; - } - - /** - * Safely sets outer params with extension from 'supports' - * - overrides 'supports' values with the correct type (derived from supports or supportsAll) - * - sets 'supports' as defaults if not set - * @param params - */ - getParams(params) { - const t = this.constructor.getVarType; - function mergeSafeType(mask, from, possibleTypes) { - const to = Object.assign({}, mask); - Object.keys(from).forEach(key => { - const tVal = to[key], - fVal = from[key], - tType = t(tVal), - fType = t(fVal); - - const typeList = possibleTypes ? possibleTypes[key] : undefined, - pTypeList = typeList ? typeList.map(x => t(x)) : []; - - //our type detector distinguishes arrays and objects - if (tVal && fVal && tType === "object" && fType === "object") { - to[key] = mergeSafeType(tVal, fVal, typeList); - } else if (tVal === undefined || tType === fType || pTypeList.includes(fType)) { - to[key] = fVal; - } else if (fType === "string") { - //try parsing NOTE: parsing from supportsAll is ignored! - if (tType === "number") { - const parsed = Number.parseFloat(fVal); - if (!Number.isNaN(parsed)) { - to[key] = parsed; - } - } else if (tType === "boolean") { - const value = fVal.toLowerCase(); - if (value === "false") { - to[key] = false; - } - if (value === "true") { - to[key] = true; - } - } - } - }); - return to; - } - return mergeSafeType(this.supports, params, this.supportsAll); - } - - /** - * Safely check certain param value - * @param value value to check - * @param defaultValue default value to return if check fails - * @param paramName name of the param to check value type against - * @return {boolean|number|*} - */ - getSafeParam(value, defaultValue, paramName) { - const t = this.constructor.getVarType; - function nest(suppNode, suppAllNode) { - if (t(suppNode) !== "object") { - return [suppNode, suppAllNode]; - } - if (!suppNode[paramName]) { - return [undefined, undefined]; - } - return nest(suppNode[paramName], suppAllNode ? suppAllNode[paramName] : undefined); - } - const param = nest(this.supports, this.supportsAll), - tParam = t(param[0]); - - if (tParam === "object") { - console.warn("Parameters should not be stored at object level. No type inspection is done."); - return true; //no supported inspection - } - const tValue = t(value); - //supported type OR supports all types includes the type - if (tValue === tParam || (param[1] && param[1].map(t).includes(tValue))) { - return value; - } - - if (tValue === "string") { - //try parsing NOTE: parsing from supportsAll is ignored! - if (tParam === "number") { - const parsed = Number.parseFloat(value); - if (!Number.isNaN(parsed)) { - return parsed; - } - } else if (tParam === "boolean") { - const val = value.toLowerCase(); - if (val === "false") { - return false; - } - if (val === "true") { - return true; - } - } - } - - //todo test - console.debug("Failed to load safe param -> new feature, debugging! ", value, defaultValue, paramName); - return defaultValue; - } - - /** - * Uniform behaviour wrt type checking in shaders - * @param x - * @return {string} - */ - static getVarType(x) { - if (x === undefined) { - return "undefined"; - } - if (x === null) { - return "null"; - } - return Array.isArray(x) ? "array" : typeof x; - } - - /** - * JavaScript initialization - * - read/store default properties here using this.context.[load/store]Property(...) - * - work with own HTML elements already attached to the DOM - * - set change listeners, input values! - */ - init() { - throw "WebGLModule.UIControls.IControl::init() must be implemented."; - } - - /** - * TODO: improve overall setter API - * Allows to set the control value programatically. - * Does not trigger canvas re-rednreing, must be done manually (e.g. control.context.invalidate()) - * @param encodedValue any value the given control can support, encoded - * (e.g. as the control acts on the GUI - for input number of - * values between 5 and 42, the value can be '6' or 6 or 6.15 - */ - set(encodedValue) { - throw "WebGLModule.UIControls.IControl::set() must be implemented."; - } - - /** - * Called when an image is rendered - * @param program WebglProgram instance - * @param {WebGLRenderingContextBase} gl - */ - glDrawing(program, gl) { - //the control should send something to GPU - throw "WebGLModule.UIControls.IControl::glDrawing() must be implemented."; - } - - /** - * Called when associated webgl program is switched to - * @param program WebglProgram instance - * @param gl WebGL Context - */ - glLoaded(program, gl) { - //the control should send something to GPU - throw "WebGLModule.UIControls.IControl::glLoaded() must be implemented."; - } - - /** - * Get the UI HTML controls - * - these can be referenced in this.init(...) - * - should respect this.params.interactive attribute and return non-interactive output if interactive=false - * - don't forget to no to work with DOM elements in init(...) in this case - */ - toHtml(breakLine = true, controlCss = "") { - throw "WebGLModule.UIControls.IControl::toHtml() must be implemented."; - } - - /** - * Handles how the variable is being defined in GLSL - * - should use variable names derived from this.webGLVariableName - */ - define() { - throw "WebGLModule.UIControls.IControl::define() must be implemented."; - } - - /** - * Sample the parameter using ratio as interpolation, must be one-liner expression so that GLSL code can write - * `vec3 mySampledValue = ${this.color.sample("0.2")};` - * NOTE: you can define your own global-scope functions to keep one-lined sampling, - * see this.context.includeGlobalCode(...) - * @param {(string|undefined)} value openGL value/variable, used in a way that depends on the UI control currently active - * (do not pass arguments, i.e. 'undefined' just get that value, note that some inputs might require you do it..) - * @param {string} valueGlType GLSL type of the value - * @return {string} valid GLSL oneliner (wihtout ';') for sampling the value, or invalid code (e.g. error message) to signal error - */ - sample(value = undefined, valueGlType = 'void') { - throw "WebGLModule.UIControls.IControl::sample() must be implemented."; - } - - /** - * Parameters supported by this UI component, must contain at least - * - 'interactive' - type bool, enables and disables the control interactivity - * (by changing the content available when rendering html) - * - 'title' - type string, the control title - * - * Additionally, for compatibility reasons, you should, if possible, define - * - 'default' - type any; the default value for the particular control - * @return {{}} name: default value mapping - */ - get supports() { - throw "WebGLModule.UIControls.IControl::supports must be implemented."; - } - - /** - * Type definitions for supports. Can return empty object. In case of missing - * type definitions, the type is derived from the 'supports()' default value type. - * - * Each key must be an array of default values for the given key if applicable. - * This is an _extension_ to the supports() and can be used only for keys that have more - * than one default type applicable - * @return {{}} - */ - get supportsAll() { - throw "WebGLModule.UIControls.IControl::typeDefs must be implemented."; - } - - /** - * GLSL type of this control: what type is returned from this.sample(...) ? - * @return {string} - */ - get type() { - throw "WebGLModule.UIControls.IControl::type must be implemented."; - } - - /** - * Raw value sent to the GPU, note that not necessarily typeof raw() === type() - * some controls might send whole arrays of data (raw) and do smart sampling such that type is only a number - * @return {any} - */ - get raw() { - throw "WebGLModule.UIControls.IControl::raw must be implemented."; - } - - /** - * Encoded value as used in the UI, e.g. a name of particular colormap, or array of string values of breaks... - * @return {any} - */ - get encoded() { - throw "WebGLModule.UIControls.IControl::encoded must be implemented."; - } - - ////////////////////////////////////// - //////// COMMON API ////////////////// - ////////////////////////////////////// - - /** - * The control type component was registered with. Handled internally. - * @return {*} - */ - get uiControlType() { - return this.constructor._uiType; - } - - /** - * Get current control parameters - * the control should set the value as this._params = this.getParams(incomingParams); - * @return {{}} - */ - get params() { - return this._params; - } - - /** - * Automatically overridden to return the name of the control it was registered with - * @return {string} - */ - getName() { - return "IControl"; - } - - /** - * Load a value from cache to support its caching - should be used on all values - * that are available for the user to play around with and change using UI controls - * - * @param defaultValue value to return in case of no cached value - * @param paramName name of the parameter, must be equal to the name from 'supports' definition - * - default value can be empty string - * @return {*} cached or default value - */ - load(defaultValue, paramName = "") { - if (paramName === "default") { - paramName = ""; - } - const value = this.context.loadProperty(this.name + paramName, defaultValue); - //check param in case of input cache collision between shader types - return this.getSafeParam(value, defaultValue, paramName === "" ? "default" : paramName); - } - - /** - * Store a value from cache to support its caching - should be used on all values - * that are available for the user to play around with and change using UI controls - * - * @param value to store - * @param paramName name of the parameter, must be equal to the name from 'supports' definition - * - default value can be empty string - */ - store(value, paramName = "") { - if (paramName === "default") { - paramName = ""; - } - return this.context.storeProperty(this.name + paramName, value); - } - - /** - * On parameter change register self - * @param {string} event which event to fire on - * - events are with inputs the names of supported parameters (this.supports), separated by dot if nested - * - most controls support "default" event - change of default value - * - see specific control implementation to see what events are fired (Advanced Slider fires "breaks" and "mask" for instance) - * @param {function} clbck(rawValue, encodedValue, context) call once change occurs, context is the control instance - */ - on(event, clbck) { - this.__onchange[event] = clbck; //only one possible event -> rewrite? - } - - /** - * Clear events of the event type - * @param {string} event type - */ - off(event) { - delete this.__onchange[event]; - } - - /** - * Clear ALL events - */ - clearEvents() { - this.__onchange = {}; - } - - /** - * Invoke changed value event - * -- should invoke every time a value changes !driven by USER!, and use unique or compatible - * event name (event 'value') so that shader knows what changed - * @param event event to call - * @param value decoded value of encodedValue - * @param encodedValue value that was received from the UI input - * @param context self reference to bind to the callback - */ - changed(event, value, encodedValue, context) { - if (typeof this.__onchange[event] === "function") { - this.__onchange[event](value, encodedValue, context); - } - } - - -}; - - -/** - * Generic UI control implementations - * used if: - * { - * type: "CONTROL TYPE", - * ... - * } - * - * The subclass constructor should get the context reference, the name - * of the input and the parametrization. - * - * Further parameters passed are dependent on the control type, see - * @ WebGLModule.UIControls - * - * @class WebGLModule.UIControls.SimpleUIControl - */ -$.WebGLModule.UIControls.SimpleUIControl = class extends $.WebGLModule.UIControls.IControl { - - //uses intristicComponent that holds all specifications needed to work with the component uniformly - constructor(context, name, webGLVariableName, params, intristicComponent, uniq = "") { - super(context, name, webGLVariableName, uniq); - this.component = intristicComponent; - this._params = this.getParams(params); - - this.encodedValue = this.load(this.params.default); - //this unfortunatelly makes cache erasing and rebuilding vis impossible, the shader part has to be fully re-instantiated - this.params.default = this.encodedValue; - } - - init() { - this.value = this.component.normalize(this.component.decode(this.encodedValue), this.params); - - if (this.params.interactive) { - const _this = this; - let node = document.getElementById(this.id); - if (node) { - let updater = function(e) { - _this.set(e.target.value); - _this.context.invalidate(); - }; - node.value = this.encodedValue; - node.addEventListener('change', updater); - } - } - } - - set(encodedValue) { - this.encodedValue = encodedValue; - this.value = this.component.normalize(this.component.decode(this.encodedValue), this.params); - this.changed("default", this.value, this.encodedValue, this); - this.store(this.encodedValue); - } - - glDrawing(program, gl) { - gl[this.component.glUniformFunName()](this.glLocation, this.value); - } - - glLoaded(program, gl) { - this.glLocation = gl.getUniformLocation(program, this.webGLVariableName); - } - - toHtml(breakLine = true, controlCss = "") { - if (!this.params.interactive) { - return ""; - } - const result = this.component.html(this.id, this.params, controlCss); - return breakLine ? `
${result}
` : result; - } - - define() { - return `uniform ${this.component.glType} ${this.webGLVariableName};`; - } - - sample(value = undefined, valueGlType = 'void') { - if (!value || valueGlType !== 'float') { - return this.webGLVariableName; - } - return this.component.sample(this.webGLVariableName, value); - } - - get uiControlType() { - return this.component["uiType"]; - } - - get supports() { - return this.component.defaults(); - } - - get supportsAll() { - return {}; - } - - get raw() { - return this.value; - } - - get encoded() { - return this.encodedValue; - } - - get type() { - return this.component.glType; - } -}; -})(OpenSeadragon); diff --git a/src/webgl/webGLContext.js b/src/webgl/webGLContext.js deleted file mode 100644 index 72e3e5ad..00000000 --- a/src/webgl/webGLContext.js +++ /dev/null @@ -1,391 +0,0 @@ -(function($) { - -$.WebGLModule.determineContext = function( version ){ - const namespace = OpenSeadragon.WebGLModule; - for (let property in namespace) { - const context = namespace[ property ], - proto = context.prototype; - if( proto && - proto instanceof namespace.WebGLImplementation && - $.isFunction( proto.getVersion ) && - proto.getVersion.call( context ) === version - ){ - return context; - } - } - return null; -}; - -/** - * @interface OpenSeadragon.WebGLModule.webglContext - * Interface for the visualisation rendering implementation which can run - * on various GLSL versions - */ -$.WebGLModule.WebGLImplementation = class { - - /** - * Create a WebGL Renderer Context Implementation (version-dependent) - * @param {WebGLModule} renderer - * @param {WebGLRenderingContextBase} gl - * @param webglVersion - * @param {object} options - * @param {GLuint} options.wrap texture wrap parameteri - * @param {GLuint} options.magFilter texture filter parameteri - * @param {GLuint} options.minFilter texture filter parameteri - * @param {string|WebGLModule.IDataLoader} options.dataLoader class name or implementation of a given loader - */ - constructor(renderer, gl, webglVersion, options) { - //Set default blending to be MASK - this.renderer = renderer; - this.gl = gl; - this.glslBlendCode = "return background * (step(0.001, foreground.a));"; - - let Loader = options.dataLoader; - if (typeof Loader === "string") { - Loader = $.WebGLModule.Loaders[Loader]; - } - if (!Loader) { - throw("Unknown data loader: " + options.dataLoader); - } - if (!(Loader.prototype instanceof $.WebGLModule.IDataLoader)) { - throw("Incompatible texture loader used: " + options.dataLoader); - } - - this._texture = new Loader(gl, webglVersion, options); - if (!this.texture.supportsWebglVersion(this.getVersion())) { - throw("Incompatible texture loader version to the renderer context version! Context WebGL" + this.getVersion()); - } - } - - /** - * Static context creation (to avoid class instantiation in case of missing support) - * @param canvas - * @return {WebGLRenderingContextBase} //todo base is not common to all, remove from docs - */ - static create(canvas) { - throw("::create() must be implemented!"); - } - - /** - * @return {string} WebGL version used - */ - getVersion() { - return "undefined"; - } - - /** - * Get GLSL texture sampling code - * @return {string} GLSL code that is correct in texture sampling wrt. WebGL version used - */ - get texture() { - return this._texture; - } - - /** - * Create a visualisation from the given JSON params - * @param {string[]} order keys of visualisation.shader in which order to build the visualization - * the order: painter's algorithm: the last drawn is the most visible - * @param {object} visualisation - * @param {[number]} shaderDataIndexToGlobalDataIndex - * @param {boolean} withHtml whether html should be also created (false if no UI controls are desired) - * @return {object} compiled specification object ready to be used by the wrapper, with the following keys: - {string} object.vertexShader vertex shader code - {string} object.fragmentShader fragment shader code - {string} object.html html for the UI - {number} object.usableShaders how many layers are going to be visualised - {(array|string[])} object.dataUrls ID's of data in use (keys of visualisation.shaders object) in desired order - the data is guaranteed to arrive in this order (images stacked below each other in imageElement) - */ - compileSpecification(order, visualisation, shaderDataIndexToGlobalDataIndex, withHtml) { - throw("::compileSpecification() must be implemented!"); - } - - /** - * Called once program is switched to: initialize all necessary items - * @param {WebGLProgram} program used program - * @param {OpenSeadragon.WebGLModule.RenderingConfig} currentConfig JSON parameters used for this visualisation - */ - programLoaded(program, currentConfig) { - throw("::programLoaded() must be implemented!"); - } - - /** - * Draw on the canvas using given program - * @param {WebGLProgram} program used program - * @param {OpenSeadragon.WebGLModule.RenderingConfig} currentConfig JSON parameters used for this visualisation - * - * @param {string} id dataId - * @param {object} tileOpts - * @param {number} tileOpts.zoom value passed to the shaders as zoom_level - * @param {number} tileOpts.pixelSize value passed to the shaders as pixel_size_in_fragments - * @param {OpenSeadragon.Mat3} tileOpts.transform position of the rendered tile - */ - programUsed(program, currentConfig, id, tileOpts) { - throw("::programUsed() must be implemented!"); - } - - /** - * Code to be included only once, required by given shader type (keys are considered global) - * @param {string} type shader type - * @returns {object} global-scope code used by the shader in format - */ - globalCodeRequiredByShaderType(type) { - return $.WebGLModule.ShaderMediator.getClass(type).__globalIncludes; - } - - /** - * Blend equation sent from the outside, must be respected - * @param glslCode code for blending, using two variables: 'foreground', 'background' - * @example - * //The shader context must define the following: - * - * vec4 some_blending_name_etc(in vec4 background, in vec4 foreground) { - * // << glslCode >> - * } - * - * void blend_clip(vec4 input) { - * //for details on clipping mask approach see show() below - * // <> - * } - * - * void blend(vec4 input) { //must be called blend, API - * // <> - * } - * - * //Also, default alpha blending equation 'show' must be implemented: - * void show(vec4 color) { - * //pseudocode - * //note that the blending output should not immediatelly work with 'color' but perform caching of the color, - * //render the color given in previous call and at the execution end of main call show(vec4(.0)) - * //this way, the previous color is not yet blended for the next layer show/blend/blend_clip which can use it to create a clipping mask - * - * compute t = color.a + background.a - color.a*background.a; - * output vec4((color.rgb * color.a + background.rgb * background.a - background.rgb * (background.a * color.a)) / t, t) - * } - */ - setBlendEquation(glslCode) { - this.glslBlendCode = glslCode; - } -}; - -$.WebGLModule.WebGL20 = class extends $.WebGLModule.WebGLImplementation { - /** - * - * @param {OpenSeadragon.WebGLModule} renderer - * @param {WebGL2RenderingContext} gl - * @param options - */ - constructor(renderer, gl, options) { - super(renderer, gl, "2.0", options); - this.emptyBuffer = gl.createBuffer(); - } - - getVersion() { - return "2.0"; - } - - static create(canvas) { - return canvas.getContext('webgl2', { premultipliedAlpha: false, alpha: true }); - } - - //todo try to implement on the global scope version-independntly - compileSpecification(order, visualisation, shaderDataIndexToGlobalDataIndex, withHtml) { - var definition = "", - execution = "", - html = "", - _this = this, - usableShaders = 0, - globalScopeCode = {}; - - order.forEach(dataId => { - let layer = visualisation.shaders[dataId]; - layer.rendering = false; - - if (layer.type === "none") { - //prevents the layer from being accounted for - layer.error = "Not an error - layer type none."; - } else if (layer.error) { - if (withHtml) { - html = _this.renderer.htmlShaderPartHeader(layer.name, layer.error, dataId, false, layer, false) + html; - } - console.warn(layer.error, layer["desc"]); - - } else if (layer._renderContext && (layer._index || layer._index === 0)) { - let visible = false; - usableShaders++; - - //make visible textures if 'visible' flag set - if (layer.visible) { - let renderCtx = layer._renderContext; - definition += renderCtx.getFragmentShaderDefinition() + ` -vec4 lid_${layer._index}_xo() { - ${renderCtx.getFragmentShaderExecution()} -}`; - if (renderCtx.opacity) { - execution += ` - vec4 l${layer._index}_out = lid_${layer._index}_xo(); - l${layer._index}_out.a *= ${renderCtx.opacity.sample()}; - ${renderCtx.__mode}(l${layer._index}_out);`; - } else { - execution += ` - ${renderCtx.__mode}(lid_${layer._index}_xo());`; - } - - layer.rendering = true; - visible = true; - OpenSeadragon.extend(globalScopeCode, _this.globalCodeRequiredByShaderType(layer.type)); - } - - //reverse order append to show first the last drawn element (top) - if (withHtml) { - html = _this.renderer.htmlShaderPartHeader(layer.name, - layer._renderContext.htmlControls(), dataId, visible, layer, true) + html; - } - } else { - if (withHtml) { - html = _this.renderer.htmlShaderPartHeader(layer.name, - `The requested visualisation type does not work properly.`, dataId, false, layer, false) + html; - } - console.warn("Invalid shader part.", "Missing one of the required elements.", layer); - } - }); - - return { - vertexShader: this.getVertexShader(), - fragmentShader: this.getFragmentShader(definition, execution, shaderDataIndexToGlobalDataIndex, globalScopeCode), - html: html, - usableShaders: usableShaders, - dataUrls: this.renderer._dataSources - }; - } - - getFragmentShader(definition, execution, shaderDataIndexToGlobalDataIndex, globalScopeCode) { - return `#version 300 es -precision mediump float; -precision mediump sampler2DArray; - -${this.texture.declare(shaderDataIndexToGlobalDataIndex)} -uniform float pixel_size_in_fragments; -uniform float zoom_level; -uniform vec2 u_tile_size; -vec4 _last_rendered_color = vec4(.0); - -in vec2 tile_texture_coords; - -out vec4 final_color; - -bool close(float value, float target) { - return abs(target - value) < 0.001; -} - -void show(vec4 color) { - //premultiplied alpha blending - vec4 fg = _last_rendered_color; - _last_rendered_color = color; - vec4 pre_fg = vec4(fg.rgb * fg.a, fg.a); - final_color = pre_fg + final_color * (1.0-fg.a); -} - -void finalize() { - show(vec4(.0)); - - if (close(final_color.a, 0.0)) { - final_color = vec4(0.); - } else { - final_color = vec4(final_color.rgb/final_color.a, final_color.a); - } -} - -vec4 blend_equation(in vec4 foreground, in vec4 background) { -${this.glslBlendCode} -} - -void blend_clip(vec4 foreground) { - _last_rendered_color = blend_equation(foreground, _last_rendered_color); -} - -void blend(vec4 foreground) { - show(_last_rendered_color); - final_color = blend_equation(foreground, final_color); - _last_rendered_color = vec4(.0); -} - -${Object.values(globalScopeCode).join("\n")} - -${definition} - -void main() { - ${execution} - - finalize(); -}`; - } - - getVertexShader() { - //UNPACK_FLIP_Y_WEBGL not supported with 3D textures so sample bottom up - return `#version 300 es -precision mediump float; - -uniform mat3 transform_matrix; -out vec2 tile_texture_coords; -const vec3 quad[4] = vec3[4] ( - vec3(0.0, 1.0, 1.0), - vec3(0.0, 0.0, 1.0), - vec3(1.0, 1.0, 1.0), - vec3(1.0, 0.0, 1.0) -); - -void main() { - vec3 vertex = quad[gl_VertexID]; - tile_texture_coords = vec2(vertex.x, -vertex.y); - gl_Position = vec4(transform_matrix * vertex, 1); -} -`; - } - - programLoaded(program, currentConfig) { - if (!this.renderer.running) { - return; - } - - let context = this.renderer, - gl = this.gl; - - // Allow for custom loading - gl.useProgram(program); - context.visualisationInUse(currentConfig); - context.glLoaded(gl, program, currentConfig); - - //Note that the drawing strategy is not to resize canvas, and simply draw everyhing on squares - this.texture.programLoaded(context, gl, program, currentConfig); - - //Empty ARRAY: get the vertices directly from the shader - gl.bindBuffer(gl.ARRAY_BUFFER, this.emptyBuffer); - } - - programUsed(program, currentConfig, id, tileOpts) { - if (!this.renderer.running) { - return; - } - // Allow for custom drawing in webGL and possibly avoid using webGL at all - - let context = this.renderer, - gl = this.gl; - - context.glDrawing(gl, program, currentConfig, tileOpts); - - // Set Attributes for GLSL - gl.uniform1f(gl.getUniformLocation(program, "pixel_size_in_fragments"), tileOpts.pixelSize || 1); - gl.uniform1f(gl.getUniformLocation(program, "zoom_level"), tileOpts.zoom || 1); - gl.uniformMatrix3fv(gl.getUniformLocation(program, "transform_matrix"), false, - tileOpts.transform || OpenSeadragon.Mat3.makeIdentity()); - - // Upload textures - this.texture.programUsed(context, currentConfig, id, program, gl); - - // Draw triangle strip (two triangles) from a static array defined in the vertex shader - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); - } -}; - -})(OpenSeadragon); diff --git a/test/demo/basic.html b/test/demo/basic.html index 6a677420..5f4262ab 100644 --- a/test/demo/basic.html +++ b/test/demo/basic.html @@ -20,12 +20,23 @@
diff --git a/test/demo/drawercomparison.html b/test/demo/drawercomparison.html index 3e007419..d40c8ef8 100644 --- a/test/demo/drawercomparison.html +++ b/test/demo/drawercomparison.html @@ -65,12 +65,12 @@

Compare behavior of Context2d and WebGL drawers

-

Context2d drawer (default in OSD <= 4.1.0)

+

Loading...

-

New WebGL drawer

+

Loading...

diff --git a/test/demo/drawercomparison.js b/test/demo/drawercomparison.js index 59bdd11b..03720816 100644 --- a/test/demo/drawercomparison.js +++ b/test/demo/drawercomparison.js @@ -13,6 +13,18 @@ const labels = { bblue: 'Blue B', duomo: 'Duomo', } +const drawers = { + canvas: "Context2d drawer (default in OSD <= 4.1.0)", + webgl: "New WebGL drawer" +} + +//Support drawer type from the url +const url = new URL(window.location.href); +const drawer1 = url.searchParams.get("left") || 'canvas'; +const drawer2 = url.searchParams.get("right") || 'webgl'; + +$("#title-w1").html(drawers[drawer1]); +$("#title-w2").html(drawers[drawer2]); //Double viewer setup for comparison - CanvasDrawer and WebGLDrawer // viewer1: canvas drawer @@ -25,7 +37,7 @@ let viewer1 = window.viewer1 = OpenSeadragon({ crossOriginPolicy: 'Anonymous', ajaxWithCredentials: false, // maxImageCacheCount: 30, - drawer:'canvas', + drawer:drawer1, blendTime:0 }); @@ -39,25 +51,23 @@ let viewer2 = window.viewer2 = OpenSeadragon({ crossOriginPolicy: 'Anonymous', ajaxWithCredentials: false, // maxImageCacheCount: 30, - drawer:'webgl', + drawer:drawer2, blendTime:0, }); -// viewer3: html drawer -var viewer3 = window.viewer3 = OpenSeadragon({ - id: "htmldrawer", - drawer:'html', - blendTime:2, - prefixUrl: "../../build/openseadragon/images/", - minZoomImageRatio:0.01, - customDrawer: OpenSeadragon.HTMLDrawer, - tileSources: [sources['leaves'], sources['rainbow'], sources['duomo']], - sequenceMode: true, - crossOriginPolicy: 'Anonymous', - ajaxWithCredentials: false -}); - - +// // viewer3: html drawer, unused +// var viewer3 = window.viewer3 = OpenSeadragon({ +// id: "htmldrawer", +// drawer:'html', +// blendTime:2, +// prefixUrl: "../../build/openseadragon/images/", +// minZoomImageRatio:0.01, +// customDrawer: OpenSeadragon.HTMLDrawer, +// tileSources: [sources['leaves'], sources['rainbow'], sources['duomo']], +// sequenceMode: true, +// crossOriginPolicy: 'Anonymous', +// ajaxWithCredentials: false +// }); // Sync navigation of viewer1 and viewer 2 @@ -126,6 +136,8 @@ Object.keys(sources).forEach((key, index)=>{ } }) +$('#image-picker').append(makeComparisonSwitcher()); + $('#image-picker input.toggle').on('change',function(){ let data = $(this).data(); if(this.checked){ @@ -150,9 +162,10 @@ $('#image-picker input:not(.toggle)').on('change',function(){ }); function updateTiledImage(tiledImage, data, value, item){ + let field = data.field; + if(tiledImage){ //item = tiledImage - let field = data.field; if(field == 'x'){ let bounds = tiledImage.getBoundsNoRotate(); let position = new OpenSeadragon.Point(Number(value), bounds.y); @@ -185,14 +198,15 @@ function updateTiledImage(tiledImage, data, value, item){ } else { tiledImage.setClip(null); } - } - else if (field == 'debug'){ + } else if (field == 'debug'){ if( $(item).prop('checked') ){ tiledImage.debugMode = true; } else { tiledImage.debugMode = false; } } + } else { + //viewer-level option } } @@ -288,6 +302,30 @@ function addTileSource(viewer, image, checkbox){ } } +function getAvailableDrawerSelect(name, selectedDrawer) { + return ` +`; +} + +function makeComparisonSwitcher() { + const left = getAvailableDrawerSelect("left", drawer1), + right = getAvailableDrawerSelect("right", drawer2); + return ` +
+ Note: you can run the comparison with desired drawers like this: drawercomparison.html?left=[type]&right=[type] +
+ ${left} + ${right} + +
+
`; +} + function makeImagePickerElement(key, label){ return $(`
@@ -302,11 +340,11 @@ function makeImagePickerElement(key, label){ +
- `.replaceAll('data-image=""', `data-image="${key}"`).replace('__title__', label)); } diff --git a/test/demo/drawerperformance.html b/test/demo/drawerperformance.html index 70715641..17e70637 100644 --- a/test/demo/drawerperformance.html +++ b/test/demo/drawerperformance.html @@ -27,9 +27,7 @@
diff --git a/test/demo/drawerperformance.js b/test/demo/drawerperformance.js index 6a52c920..0fa6bfef 100644 --- a/test/demo/drawerperformance.js +++ b/test/demo/drawerperformance.js @@ -13,6 +13,13 @@ const labels = { bblue: 'Blue B', duomo: 'Duomo', } +const drawers = { + canvas: "Context2d drawer (default in OSD <= 4.1.0)", + webgl: "New WebGL drawer", + html: "" +} + + let viewer; ( function () { @@ -408,6 +415,10 @@ function rStats ( settings ) { _base = document.createElement( 'div' ); _base.className = 'rs-base'; + _base.style.bottom = '0px'; + _base.style.right = '0px'; + _base.style.top = 'initial'; + _base.style.left = 'initial'; _div = document.createElement( 'div' ); _div.className = 'rs-container'; _div.style.height = 'auto'; @@ -509,6 +520,8 @@ function rStats ( settings ) { } + + var glStats = function() { var _rS = null; @@ -898,16 +911,9 @@ Stats.Panel = function ( name, fg, bg ) { // })(); - -$('#create-drawer').on('click',function(){ - let drawerType = $('#select-drawer').val(); - let num = Math.floor($('#input-number').val()); - rS('other').start(); - run(drawerType, num); -}); - - function run(drawerType, num) { + rS('other').start(); + if(viewer){ viewer.destroy(); } @@ -969,3 +975,28 @@ function makeTileSources(num){ } +const url = new URL(window.location.href); +const drawer = url.searchParams.get("drawer"); +const numberOfSources = Number.parseInt(url.searchParams.get("sources")) || 1; + +$('#create-drawer').on('click',function(){ + const drawer = $('#select-drawer').val(); + let num = Math.floor($('#input-number').val()); + + url.searchParams.set("drawer", drawer); + url.searchParams.set("sources", num); + if ("undefined" !== typeof history.replaceState) { + history.replaceState(null, window.location.title, url.toString()); + } + run(drawer, num); +}); + +$('#input-number').val(numberOfSources); +$("#select-drawer").html(Object.entries(drawers).map(([k, v]) => { + const selected = drawer === k ? "selected" : ""; + return ``; +}).join("\n")); +if (drawer) { + run(drawer, numberOfSources); +} +