diff --git a/src/webgldrawer.js b/src/webgldrawer.js index f3387960..44daa72a 100644 --- a/src/webgldrawer.js +++ b/src/webgldrawer.js @@ -34,299 +34,393 @@ (function( $ ){ - // internal class Mat3: implements matrix operations - // Modified from https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html -class Mat3{ - constructor(values){ - if(!values) { - values = [ - 0, 0, 0, - 0, 0, 0, - 0, 0, 0 - ]; - } + // internal class Mat3: implements matrix operations + // Modified from https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html + class Mat3{ + constructor(values){ + if(!values) { + values = [ + 0, 0, 0, + 0, 0, 0, + 0, 0, 0 + ]; + } - this.values = values; - } + this.values = values; + } - static makeIdentity(){ - return new Mat3([ - 1, 0, 0, - 0, 1, 0, - 0, 0, 1 - ]); - } + static makeIdentity(){ + return new Mat3([ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ]); + } - static makeTranslation(tx, ty) { - return new Mat3([ - 1, 0, 0, - 0, 1, 0, - tx, ty, 1, - ]); - } + static makeTranslation(tx, ty) { + return new Mat3([ + 1, 0, 0, + 0, 1, 0, + tx, ty, 1, + ]); + } - static makeRotation(angleInRadians) { - var c = Math.cos(angleInRadians); - var s = Math.sin(angleInRadians); - return new Mat3([ - c, -s, 0, - s, c, 0, - 0, 0, 1, - ]); - } + static makeRotation(angleInRadians) { + var c = Math.cos(angleInRadians); + var s = Math.sin(angleInRadians); + return new Mat3([ + c, -s, 0, + s, c, 0, + 0, 0, 1, + ]); + } - static makeScaling(sx, sy) { - return new Mat3([ - sx, 0, 0, - 0, sy, 0, - 0, 0, 1, - ]); - } + static makeScaling(sx, sy) { + return new Mat3([ + sx, 0, 0, + 0, sy, 0, + 0, 0, 1, + ]); + } - multiply(other) { - let a = this.values; - let b = other.values; + multiply(other) { + let a = this.values; + let b = other.values; - var a00 = a[0 * 3 + 0]; - var a01 = a[0 * 3 + 1]; - var a02 = a[0 * 3 + 2]; - var a10 = a[1 * 3 + 0]; - var a11 = a[1 * 3 + 1]; - var a12 = a[1 * 3 + 2]; - var a20 = a[2 * 3 + 0]; - var a21 = a[2 * 3 + 1]; - var a22 = a[2 * 3 + 2]; - var b00 = b[0 * 3 + 0]; - var b01 = b[0 * 3 + 1]; - var b02 = b[0 * 3 + 2]; - var b10 = b[1 * 3 + 0]; - var b11 = b[1 * 3 + 1]; - var b12 = b[1 * 3 + 2]; - var b20 = b[2 * 3 + 0]; - var b21 = b[2 * 3 + 1]; - var b22 = b[2 * 3 + 2]; - return new Mat3([ - b00 * a00 + b01 * a10 + b02 * a20, - b00 * a01 + b01 * a11 + b02 * a21, - b00 * a02 + b01 * a12 + b02 * a22, - b10 * a00 + b11 * a10 + b12 * a20, - b10 * a01 + b11 * a11 + b12 * a21, - b10 * a02 + b11 * a12 + b12 * a22, - b20 * a00 + b21 * a10 + b22 * a20, - b20 * a01 + b21 * a11 + b22 * a21, - b20 * a02 + b21 * a12 + b22 * a22, - ]); - } + var a00 = a[0 * 3 + 0]; + var a01 = a[0 * 3 + 1]; + var a02 = a[0 * 3 + 2]; + var a10 = a[1 * 3 + 0]; + var a11 = a[1 * 3 + 1]; + var a12 = a[1 * 3 + 2]; + var a20 = a[2 * 3 + 0]; + var a21 = a[2 * 3 + 1]; + var a22 = a[2 * 3 + 2]; + var b00 = b[0 * 3 + 0]; + var b01 = b[0 * 3 + 1]; + var b02 = b[0 * 3 + 2]; + var b10 = b[1 * 3 + 0]; + var b11 = b[1 * 3 + 1]; + var b12 = b[1 * 3 + 2]; + var b20 = b[2 * 3 + 0]; + var b21 = b[2 * 3 + 1]; + var b22 = b[2 * 3 + 2]; + return new Mat3([ + b00 * a00 + b01 * a10 + b02 * a20, + b00 * a01 + b01 * a11 + b02 * a21, + b00 * a02 + b01 * a12 + b02 * a22, + b10 * a00 + b11 * a10 + b12 * a20, + b10 * a01 + b11 * a11 + b12 * a21, + b10 * a02 + b11 * a12 + b12 * a22, + b20 * a00 + b21 * a10 + b22 * a20, + b20 * a01 + b21 * a11 + b22 * a21, + b20 * a02 + b21 * a12 + b22 * a22, + ]); + } -} + } -/** - * @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. - */ + /** + * @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. + */ -$.WebGLDrawer = class WebGLDrawer extends OpenSeadragon.DrawerBase{ - constructor(options){ - super(options); + $.WebGLDrawer = class WebGLDrawer extends OpenSeadragon.DrawerBase{ + constructor(options){ + super(options); - this.destroyed = false; - // private members + this.destroyed = false; + // private members - this._TextureMap = new Map(); - this._TileMap = new Map(); + this._TextureMap = new Map(); + this._TileMap = new Map(); - this._gl = null; - this._glLocs = null; - this._glProgram = null; - this._glPositionBuffer = null; - this._outputCanvas = null; - this._outputContext = null; - this._clippingCanvas = null; - this._clippingContext = null; - this._renderingCanvas = null; + this._gl = null; + this._glLocs = null; + this._glProgram = null; + this._glUnitQuadBuffer = null; + this._glFrameBuffer = null; + this._glTiledImageTexture = null; + this._glFramebufferToCanvasTransform = null; + this._outputCanvas = null; + this._outputContext = null; + this._clippingCanvas = null; + this._clippingContext = null; + this._renderingCanvas = null; - // Add listeners for events that require modifying the scene or camera - this.viewer.addHandler("tile-ready", ev => this._tileReadyHandler(ev)); - this.viewer.addHandler("image-unloaded", ev => this._imageUnloadedHandler(ev)); + // Add listeners for events that require modifying the scene or camera + this.viewer.addHandler("tile-ready", ev => this._tileReadyHandler(ev)); + this.viewer.addHandler("image-unloaded", ev => this._imageUnloadedHandler(ev)); - // this.viewer is set by parent constructor - // this.canvas is set by parent constructor, created and appended to the viewer container element - this._setupCanvases(); + // this.viewer is set by parent constructor + // this.canvas is set by parent constructor, created and appended to the viewer container element + this._setupCanvases(); - this._setupRenderer(); + this._setupRenderer(); - this.context = this._outputContext; // API required by tests - } + this.context = this._outputContext; // API required by tests + } - // Public API required by all Drawer implementations - /** - * Clean up the renderer, removing all resources - */ - destroy(){ - if(this.destroyed){ - return; - } - // clear all resources used by the renderer, geometries, textures etc - let gl = this._gl; - - // adapted from https://stackoverflow.com/a/23606581/1214731 - var numTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); - for (let unit = 0; unit < numTextureUnits; ++unit) { - gl.activeTexture(gl.TEXTURE0 + unit); - gl.bindTexture(gl.TEXTURE_2D, null); - gl.bindTexture(gl.TEXTURE_CUBE_MAP, null); - } - gl.bindBuffer(gl.ARRAY_BUFFER, null); - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); - gl.bindRenderbuffer(gl.RENDERBUFFER, null); - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - - let canvases = Array.from(this._TextureMap.keys()); - canvases.forEach(canvas => { - this._cleanupImageData(canvas); // deletes texture, removes from _TextureMap - }); - - // Delete all our created resources - gl.deleteBuffer(this._glPositionBuffer); - - // TO DO: if/when render buffers or frame buffers are used, release them: - // gl.deleteRenderbuffer(someRenderbuffer); - // gl.deleteFramebuffer(someFramebuffer); - - // make canvases 1 x 1 px and delete references - this._renderingCanvas.width = this._renderingCanvas.height = 1; - this._clippingCanvas.width = this._clippingCanvas.height = 1; - this._outputCanvas.width = this._outputCanvas.height = 1; - this._renderingCanvas = null; - this._clippingCanvas = this._clippingContext = null; - this._outputCanvas = this._outputContext = null; - - let ext = gl.getExtension('WEBGL_lose_context'); - if(ext){ - ext.loseContext(); - } - - // set our webgl context reference to null to enable garbage collection - this._gl = null; - - // set our destroyed flag to true - 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 and - * three.js has been exposed as a global variable named THREE - */ - isSupported(){ - let canvasElement = document.createElement( 'canvas' ); - let webglContext = $.isFunction( canvasElement.getContext ) && - canvasElement.getContext( 'webgl' ); - let ext = webglContext.getExtension('WEBGL_lose_context'); - if(ext){ - ext.loseContext(); - } - return !!( webglContext ); - } - - /** - * create the HTML element (canvas in this case) that the image will be drawn into - * @returns {Element} the canvas to draw into - */ - createDrawingElement(){ - let canvas = $.makeNeutralElement("canvas"); - let viewportSize = this._calculateCanvasSize(); - canvas.width = viewportSize.x; - canvas.height = viewportSize.y; - return 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 - }; - - 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); - - //iterate over tiled imagesget the list of tiles to draw - this._outputContext.clearRect(0, 0, this._outputCanvas.width, this._outputCanvas.height); - - // TO DO: further optimization is possible. - // If no clipping and no composite operation, the tiled images - // can all be drawn onto the rendering canvas at the same time, avoiding - // unnecessary clearing and copying of the pixel data. - // For now, I'm doing it this way to replicate full functionality - // of the context2d drawer - tiledImages.forEach( (tiledImage, i) => { - // clear the rendering canvas - this._gl.clear(this._gl.COLOR_BUFFER_BIT); - - // set opacity for this image - this._gl.uniform1f(this._glLocs.uOpacityMultiplier, tiledImage.opacity); - - //get the list of tiles to draw - let tilesToDraw = tiledImage.getTilesToDraw(); - - if(tilesToDraw.length === 0){ + // Public API required by all Drawer implementations + /** + * Clean up the renderer, removing all resources + */ + destroy(){ + if(this.destroyed){ return; } + // clear all resources used by the renderer, geometries, textures etc + let gl = this._gl; - let overallMatrix = viewMatrix; + // adapted from https://stackoverflow.com/a/23606581/1214731 + var numTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); + for (let unit = 0; unit < numTextureUnits; ++unit) { + gl.activeTexture(gl.TEXTURE0 + unit); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.bindTexture(gl.TEXTURE_CUBE_MAP, null); + } + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); - let imageRotation = tiledImage.getRotation(true); - 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); + let canvases = Array.from(this._TextureMap.keys()); + canvases.forEach(canvas => { + this._cleanupImageData(canvas); // deletes texture, removes from _TextureMap + }); - // update the view matrix to account for this image's rotation - let localMatrix = t1.multiply(imageRotationMatrix).multiply(t2); - overallMatrix = viewMatrix.multiply(localMatrix); + // Delete all our created resources + gl.deleteBuffer(this._glUnitQuadBuffer); + gl.deleteBuffer(this._glFrameBuffer); + // TO DO: if/when render buffers or frame buffers are used, release them: + // gl.deleteRenderbuffer(someRenderbuffer); + // gl.deleteFramebuffer(someFramebuffer); + + // make canvases 1 x 1 px and delete references + this._renderingCanvas.width = this._renderingCanvas.height = 1; + this._clippingCanvas.width = this._clippingCanvas.height = 1; + this._outputCanvas.width = this._outputCanvas.height = 1; + this._renderingCanvas = null; + this._clippingCanvas = this._clippingContext = null; + this._outputCanvas = this._outputContext = null; + + let ext = gl.getExtension('WEBGL_lose_context'); + if(ext){ + ext.loseContext(); } - for(let i = 0; i < tilesToDraw.length; i++){ - let tile = tilesToDraw[i].tile; - let texture = this._TextureMap.get(tile.getCanvasContext().canvas); - if(texture){ - this._drawTile(tile, tiledImage, texture, overallMatrix, tiledImage.opacity); - } else { - // console.log('No tile info', tile); + // set our webgl context reference to null to enable garbage collection + this._gl = null; + + // set our destroyed flag to true + 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 and + * three.js has been exposed as a global variable named THREE + */ + isSupported(){ + let canvasElement = document.createElement( 'canvas' ); + let webglContext = $.isFunction( canvasElement.getContext ) && + canvasElement.getContext( 'webgl' ); + let ext = webglContext.getExtension('WEBGL_lose_context'); + if(ext){ + ext.loseContext(); + } + return !!( webglContext ); + } + + /** + * create the HTML element (canvas in this case) that the image will be drawn into + * @returns {Element} the canvas to draw into + */ + createDrawingElement(){ + let canvas = $.makeNeutralElement("canvas"); + let viewportSize = this._calculateCanvasSize(); + canvas.width = viewportSize.x; + canvas.height = viewportSize.y; + return canvas; + } + + /** + * + * @param {Array} tiledImages Array of TiledImage objects to draw + */ + draw(tiledImages){ + let gl = this._gl; + let viewport = { + bounds: this.viewport.getBoundsNoRotate(true), + center: this.viewport.getCenter(true), + rotation: this.viewport.getRotation(true) * Math.PI / 180 + }; + + 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); + + // clear the output canvas + this._outputContext.clearRect(0, 0, this._outputCanvas.width, this._outputCanvas.height); + + + // TO DO: further optimization is possible. + // If no clipping and no composite operation, the tiled images + // can all be drawn onto the rendering canvas at the same time, avoiding + // unnecessary clearing and copying of the pixel data. + // For now, I'm doing it this way to replicate full functionality + // of the context2d drawer + + //iterate over tiled imagesget the list of tiles to draw + tiledImages.forEach( (tiledImage, i) => { + + //get the list of tiles to draw + let tilesToDraw = tiledImage.getTilesToDraw(); + + if(tilesToDraw.length === 0){ + return; } - } + // bind to the framebuffer for render-to-texture + gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); + + // clear the buffer + gl.clear(gl.COLOR_BUFFER_BIT); + + // set opacity for this image + gl.uniform1f(this._glLocs.uOpacityMultiplier, tiledImage.opacity); + + + + 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); + } + + // iterate over tiles and draw each one to the buffer + for(let ti = 0; ti < tilesToDraw.length; ti++){ + let tile = tilesToDraw[ti].tile; + let textureInfo = this._TextureMap.get(tile.getCanvasContext().canvas); + if(textureInfo){ + this._drawTile(tile, tiledImage, textureInfo, overallMatrix, tiledImage.opacity); + } else { + // console.log('No tile info', tile); + } + } + + + // Draw from the Framebuffer onto the rendering canvas buffer + + gl.flush(); // finish drawing to the texture + gl.bindFramebuffer(gl.FRAMEBUFFER, null); // null means bind to the backbuffer for drawing + gl.bindTexture(gl.TEXTURE_2D, this._glTiledImageTexture); // bind the rendered texture to use + gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer + + // set up the matrix to draw the whole framebuffer to the entire clip space + gl.uniformMatrix3fv(this._glLocs.uMatrix, false, this._glFramebufferToCanvasTransform); + + // reset texturebuffer to unit quad + gl.bindBuffer(gl.ARRAY_BUFFER, this._glTextureBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this._glUnitQuad, gl.DYNAMIC_DRAW); + + // set opacity to the value for the current tiledImage + this._gl.uniform1f(this._glLocs.uOpacityMultiplier, tiledImage.opacity); + gl.drawArrays(gl.TRIANGLES, 0, 6); + + // iterate over any filters - filters can use this._glTiledImageTexture to get rendered data if desired + let filters = this.filters || []; + for(let fi = 0; fi < filters.length; fi++){ + let filter = this.filters[fi]; + if(filter.apply){ + filter.apply(gl); // filter.apply should write data on top of the backbuffer (bound above) + } + } + gl.flush(); //make sure drawing to the output buffer of the rendering canvas is complete. Is this necessary? + + // draw from the rendering canvas onto the output canvas, clipping/cropping if needed. + this._renderToOutputCanvas(tiledImage, tilesToDraw, i); + + }); + + } + + // Public API required by all Drawer implementations + /** + * Set the context2d imageSmoothingEnabled parameter + * @param {Boolean} enabled + */ + setImageSmoothingEnabled(enabled){ + this._clippingContext.imageSmoothingEnabled = enabled; + this._outputContext.imageSmoothingEnabled = enabled; + } + + /** + * Draw a rect onto the output canvas for debugging purposes + * @param {OpenSeadragon.Rect} rect + */ + drawDebuggingRect(rect){ + let context = this._outputContext; + context.save(); + context.lineWidth = 2 * $.pixelDensityRatio; + context.strokeStyle = this.debugGridColor[0]; + context.fillStyle = this.debugGridColor[0]; + + context.strokeRect( + rect.x * $.pixelDensityRatio, + rect.y * $.pixelDensityRatio, + rect.width * $.pixelDensityRatio, + rect.height * $.pixelDensityRatio + ); + + context.restore(); + } + // private + _getTextureDataFromTile(tile){ + return tile.getCanvasContext().canvas; + } + + /** + * Draw data from the rendering canvas onto the output canvas, with clipping, + * cropping and/or debug info as requested. + * @private + * @param {OpenSeadragon.TiledImage} tiledImage - the tiledImage to draw + * @param {Array} tilesToDraw - array of objects containing tiles that were drawn + */ + _renderToOutputCanvas(tiledImage, tilesToDraw, tiledImageIndex){ // composite onto the output canvas, clipping if necessary this._outputContext.save(); // set composite operation; ignore for first image drawn - this._outputContext.globalCompositeOperation = i === 0 ? null : tiledImage.compositeOperation || this.viewer.compositeOperation; + this._outputContext.globalCompositeOperation = tiledImageIndex === 0 ? null : tiledImage.compositeOperation || this.viewer.compositeOperation; if(tiledImage._croppingPolygons || tiledImage._clip){ this._renderToClippingCanvas(tiledImage); this._outputContext.drawImage(this._clippingCanvas, 0, 0); @@ -342,526 +436,602 @@ $.WebGLDrawer = class WebGLDrawer extends OpenSeadragon.DrawerBase{ this._drawDebugInfo(tilesToDraw, tiledImage, strokeStyle, fillStyle); } + // Fire tiled-image-drawn event now that the data is on the output canvas 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. - */ + * 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), }); } - - }); - - } - - // Public API required by all Drawer implementations - /** - * Set the context2d imageSmoothingEnabled parameter - * @param {Boolean} enabled - */ - setImageSmoothingEnabled(enabled){ - this._clippingContext.imageSmoothingEnabled = enabled; - this._outputContext.imageSmoothingEnabled = enabled; - } - - /** - * Draw a rect onto the output canvas for debugging purposes - * @param {OpenSeadragon.Rect} rect - */ - drawDebuggingRect(rect){ - let context = this._outputContext; - context.save(); - context.lineWidth = 2 * $.pixelDensityRatio; - context.strokeStyle = this.debugGridColor[0]; - context.fillStyle = this.debugGridColor[0]; - - context.strokeRect( - rect.x * $.pixelDensityRatio, - rect.y * $.pixelDensityRatio, - rect.width * $.pixelDensityRatio, - rect.height * $.pixelDensityRatio - ); - - context.restore(); - } - _getTextureDataFromTile(tile){ - return tile.getCanvasContext().canvas; - } - - // Private methods - _drawTile(tile, tiledImage, texture, viewMatrix, imageOpacity){ - - let gl = this._gl; - - // x, y, w, h in viewport coords - let x = tile.positionedBounds.x; - let y = tile.positionedBounds.y; - let w = tile.positionedBounds.width; - let h = tile.positionedBounds.height; - - 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); + // private + _drawTile(tile, tiledImage, textureInfo, viewMatrix, imageOpacity){ - if(tile.opacity !== 1 && tile.x === 0 && tile.y === 0){ - // set opacity for this image - this._gl.uniform1f(this._glLocs.uOpacityMultiplier, imageOpacity * tile.opacity); - } - - gl.uniformMatrix3fv(this._glLocs.uMatrix, false, overallMatrix.values); - gl.bindTexture(gl.TEXTURE_2D, texture); - - if(this.continuousTileRefresh){ - // Upload the image into the texture. - let tileContext = tile.getCanvasContext(); - this._raiseTileDrawingEvent(tiledImage, this._outputContext, tile, tileContext); - this._uploadImageData(tileContext); - } - - - gl.drawArrays(gl.TRIANGLES, 0, 6); - } - - _setupRenderer(){ - - if(!this._gl){ - $.console.error('_setupCanvases must be called before _setupRenderer'); - } - - const vertexShaderProgram = ` - attribute vec2 a_position; - - uniform mat3 u_matrix; - - varying vec2 v_texCoord; - - void main() { - gl_Position = vec4(u_matrix * vec3(a_position, 1), 1); - - // because we're using a unit quad we can just use - // the same data for our texcoords. - v_texCoord = a_position; - } - `; - - const fragmentShaderProgram = ` - precision mediump float; - - // our texture - uniform sampler2D u_image; - - // the texCoords passed in from the vertex shader. - varying vec2 v_texCoord; - - // the opacity multiplier for the image - uniform float u_opacity_multiplier; - - void main() { - gl_FragColor = texture2D(u_image, v_texCoord); - // gl_FragColor *= u_opacity_multiplier; - } - `; - let gl = this._gl; - this._glProgram = this.constructor.initShaderProgram(gl, vertexShaderProgram, fragmentShaderProgram); - gl.useProgram(this._glProgram); - gl.enable(gl.BLEND); - gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - - this._glLocs = { - aPosition: gl.getAttribLocation(this._glProgram, 'a_position'), - uMatrix: gl.getUniformLocation(this._glProgram, 'u_matrix'), - uImage: gl.getUniformLocation(this._glProgram, 'u_image'), - uOpacityMultiplier: gl.getUniformLocation(this._glProgram, 'u_opacity_multiplier') - }; - - // provide texture coordinates for the rectangle. - this._glPositionBuffer = gl.createBuffer(); //keep reference to clear it later - gl.bindBuffer(gl.ARRAY_BUFFER, this._glPositionBuffer); - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ - 0.0, 0.0, - 1.0, 0.0, - 0., 1.0, - 0.0, 1.0, - 1.0, 0.0, - 1.0, 1.0]), gl.STATIC_DRAW); - gl.enableVertexAttribArray(this._glLocs.aPosition); - gl.vertexAttribPointer(this._glLocs.aPosition, 2, gl.FLOAT, false, 0, 0); - - } - - _setupCanvases(){ - let _this = this; - - this._outputCanvas = this.canvas; //output canvas - this._outputContext = this._outputCanvas.getContext('2d'); - - this._renderingCanvas = document.createElement('canvas'); - - this._clippingCanvas = document.createElement('canvas'); - this._clippingContext = this._clippingCanvas.getContext('2d'); - this._renderingCanvas.width = this._clippingCanvas.width = this._outputCanvas.width; - this._renderingCanvas.height = this._clippingCanvas.height = this._outputCanvas.height; - - this._gl = this._renderingCanvas.getContext('webgl'); - - //make the additional canvas elements mirror size changes to the output canvas - this.viewer.addHandler("resize", function(){ - - if(_this._outputCanvas !== _this.viewer.drawer.canvas){ - _this._outputCanvas.style.width = _this.viewer.drawer.canvas.clientWidth + 'px'; - _this._outputCanvas.style.height = _this.viewer.drawer.canvas.clientHeight + 'px'; - } - - let viewportSize = _this._calculateCanvasSize(); - if( _this._outputCanvas.width !== viewportSize.x || - _this._outputCanvas.height !== viewportSize.y ) { - _this._outputCanvas.width = viewportSize.x; - _this._outputCanvas.height = viewportSize.y; - } - - _this._renderingCanvas.style.width = _this._outputCanvas.clientWidth + 'px'; - _this._renderingCanvas.style.height = _this._outputCanvas.clientHeight + 'px'; - _this._renderingCanvas.width = _this._clippingCanvas.width = _this._outputCanvas.width; - _this._renderingCanvas.height = _this._clippingCanvas.height = _this._outputCanvas.height; - _this._gl.viewport(0, 0, _this._renderingCanvas.width, _this._renderingCanvas.height); - }); - } - - - _tileReadyHandler(event){ - let tile = event.tile; - let tileContext = tile.getCanvasContext(); - let canvas = tileContext.canvas; - let texture = this._TextureMap.get(canvas); - - // if this is a new image for us, create a texture - if(!texture){ let gl = this._gl; + let texture = textureInfo.texture; + let textureQuad = textureInfo.position; - // create a gl Texture for this tile and bind the canvas with the image data - texture = gl.createTexture(); - // add it to our _TextureMap - this._TextureMap.set(canvas, texture); + // set the vertices into the non-overlapped portion of the texture + gl.bindBuffer(gl.ARRAY_BUFFER, this._glTextureBuffer); + gl.bufferData(gl.ARRAY_BUFFER, textureQuad, gl.DYNAMIC_DRAW); + // compute offsets for overlap + 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); + + // set opacity for this image + this._gl.uniform1f(this._glLocs.uOpacityMultiplier, tile.opacity); // imageOpacity * + + gl.uniformMatrix3fv(this._glLocs.uMatrix, false, overallMatrix.values); gl.bindTexture(gl.TEXTURE_2D, texture); - // Set the parameters so we can render any size image. + + if(this.continuousTileRefresh){ + // Upload the image into the texture (already bound to TEXTURE_2D above) + let tileContext = tile.getCanvasContext(); + this._raiseTileDrawingEvent(tiledImage, this._outputContext, tile, tileContext); + this._uploadImageData(tileContext, tile, tiledImage); + } + + + gl.drawArrays(gl.TRIANGLES, 0, 6); + } + + _setupRenderer(){ + + if(!this._gl){ + $.console.error('_setupCanvases must be called before _setupRenderer'); + } + + const vertexShaderProgram = ` + attribute vec2 a_output_position; + attribute vec2 a_texture_position; + + uniform mat3 u_matrix; + + varying vec2 v_texCoord; + + void main() { + gl_Position = vec4(u_matrix * vec3(a_output_position, 1), 1); + + v_texCoord = a_texture_position; + } + `; + + const fragmentShaderProgram = ` + precision mediump float; + + // our texture + uniform sampler2D u_image; + + // the texCoords passed in from the vertex shader. + varying vec2 v_texCoord; + + // the opacity multiplier for the image + uniform float u_opacity_multiplier; + + void main() { + gl_FragColor = texture2D(u_image, v_texCoord); + gl_FragColor *= u_opacity_multiplier; + } + `; + let gl = this._gl; + this._glProgram = this.constructor.initShaderProgram(gl, vertexShaderProgram, fragmentShaderProgram); + gl.useProgram(this._glProgram); + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + + this._glLocs = { + aOutputPosition: gl.getAttribLocation(this._glProgram, 'a_output_position'), + aTexturePosition: gl.getAttribLocation(this._glProgram, 'a_texture_position'), + uMatrix: gl.getUniformLocation(this._glProgram, 'u_matrix'), + uImage: gl.getUniformLocation(this._glProgram, 'u_image'), + uOpacityMultiplier: gl.getUniformLocation(this._glProgram, 'u_opacity_multiplier') + }; + + this._glUnitQuad = this._makeQuadVertexBuffer(0, 1, 0, 1); + // provide texture coordinates for the rectangle in output space. + this._glUnitQuadBuffer = gl.createBuffer(); //keep reference to clear it later + gl.bindBuffer(gl.ARRAY_BUFFER, this._glUnitQuadBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this._glUnitQuad, gl.STATIC_DRAW); + gl.enableVertexAttribArray(this._glLocs.aOutputPosition); + gl.vertexAttribPointer(this._glLocs.aOutputPosition, 2, gl.FLOAT, false, 0, 0); + + // provide texture coordinates for the rectangle in image (texture) space. + this._glTextureBuffer = gl.createBuffer(); //keep reference to clear it later + gl.bindBuffer(gl.ARRAY_BUFFER, this._glTextureBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this._glUnitQuad, gl.DYNAMIC_DRAW); // use unit quad to start, will be updated per tile + gl.enableVertexAttribArray(this._glLocs.aTexturePosition); + gl.vertexAttribPointer(this._glLocs.aTexturePosition, 2, gl.FLOAT, false, 0, 0); + + // setup the framebuffer + this._glTiledImageTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this._glTiledImageTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this._renderingCanvas.width, this._renderingCanvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + this._glFrameBuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, // attach texture as COLOR_ATTACHMENT0 + gl.TEXTURE_2D, // attach a 2D texture + this._glTiledImageTexture, // the texture to attach + 0 + ); + + this._glFramebufferToCanvasTransform = Mat3.makeScaling(2, 2).multiply(Mat3.makeTranslation(-0.5, -0.5)).values; + + + } + + _resizeRenderer(){ + let gl = this._gl; + let w = this._renderingCanvas.width; + let h = this._renderingCanvas.height; + gl.viewport(0, 0, w, h); + + //release the old texture + gl.deleteTexture(this._glTiledImageTexture); + //create a new texture and set it up + this._glTiledImageTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this._glTiledImageTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - // Upload the image into the texture. - this._uploadImageData(tileContext); + //bind the frame buffer to the new texture + gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._glTiledImageTexture, 0); } - } + _setupCanvases(){ + let _this = this; - _uploadImageData(tileContext){ - let gl = this._gl; - try{ + this._outputCanvas = this.canvas; //output canvas + this._outputContext = this._outputCanvas.getContext('2d'); + + this._renderingCanvas = document.createElement('canvas'); + + this._clippingCanvas = document.createElement('canvas'); + this._clippingContext = this._clippingCanvas.getContext('2d'); + this._renderingCanvas.width = this._clippingCanvas.width = this._outputCanvas.width; + this._renderingCanvas.height = this._clippingCanvas.height = this._outputCanvas.height; + + this._gl = this._renderingCanvas.getContext('webgl'); + + //make the additional canvas elements mirror size changes to the output canvas + this.viewer.addHandler("resize", function(){ + + if(_this._outputCanvas !== _this.viewer.drawer.canvas){ + _this._outputCanvas.style.width = _this.viewer.drawer.canvas.clientWidth + 'px'; + _this._outputCanvas.style.height = _this.viewer.drawer.canvas.clientHeight + 'px'; + } + + let viewportSize = _this._calculateCanvasSize(); + if( _this._outputCanvas.width !== viewportSize.x || + _this._outputCanvas.height !== viewportSize.y ) { + _this._outputCanvas.width = viewportSize.x; + _this._outputCanvas.height = viewportSize.y; + } + + _this._renderingCanvas.style.width = _this._outputCanvas.clientWidth + 'px'; + _this._renderingCanvas.style.height = _this._outputCanvas.clientHeight + 'px'; + _this._renderingCanvas.width = _this._clippingCanvas.width = _this._outputCanvas.width; + _this._renderingCanvas.height = _this._clippingCanvas.height = _this._outputCanvas.height; + + // important - update the size of the rendering viewport! + _this._resizeRenderer(); + }); + } + + _makeQuadVertexBuffer(left, right, top, bottom){ + return new Float32Array([ + left, bottom, + right, bottom, + left, top, + left, top, + right, bottom, + right, top]); + } + + + _tileReadyHandler(event){ + let tile = event.tile; + let tiledImage = event.tiledImage; + let tileContext = tile.getCanvasContext(); let canvas = tileContext.canvas; - if(!canvas){ - throw('Tile context does not have a canvas', tileContext); + let textureInfo = this._TextureMap.get(canvas); + + // if this is a new image for us, create a texture + if(!textureInfo){ + let gl = this._gl; + + // create a gl Texture for this tile and bind the canvas with the image data + let texture = gl.createTexture(); + let position; + let overlap = tiledImage.source.tileOverlap; + if( overlap > 0){ + // calculate the normalized position of the rect to actually draw + // discarding overlap. + let overlapFraction = this._calculateOverlapFraction(tile, tiledImage); + + let left = tile.x === 0 ? 0 : overlapFraction.x; + let top = tile.y === 0 ? 0 : overlapFraction.y; + let right = tile.isRightMost ? 1 : 1 - overlapFraction.x; + let bottom = tile.isBottomMost ? 1 : 1 - overlapFraction.y; + position = this._makeQuadVertexBuffer(left, right, top, bottom); + } else { + // no overlap: this texture can use the unit quad as it's position data + position = this._glUnitQuad; + } + + let textureInfo = { + texture: texture, + position: position, + }; + + // add it to our _TextureMap + this._TextureMap.set(canvas, textureInfo); + + gl.bindTexture(gl.TEXTURE_2D, texture); + // Set the parameters so we can render any size image. + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + + // Upload the image into the texture. + this._uploadImageData(tileContext); + } + + } + + _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 + }; + } + + _uploadImageData(tileContext){ + + let gl = this._gl; + let canvas = tileContext.canvas; + + // This depends on gl.TEXTURE_2D being bound to the texture + // associated with this canvas before calling this function gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); - } catch(e) { - $.console.error('Error uploading canvas data to webgl', e); + } - } - _imageUnloadedHandler(event){ - let canvas = event.context2D.canvas; - this._cleanupImageData(canvas); - } - - _cleanupImageData(tileCanvas){ - let texture = this._TextureMap.get(tileCanvas); - //remove from the map - this._TextureMap.delete(tileCanvas); - - //release the texture from the GPU - this._gl.deleteTexture(texture); - } - // private - // necessary for clip testing to pass (test uses spyOnce(drawer._setClip)) - _setClip(rect){ - this._clippingContext.beginPath(); - this._clippingContext.rect(rect.x, rect.y, rect.width, rect.height); - this._clippingContext.clip(); - } - _renderToClippingCanvas(item){ - let _this = this; - - this._clippingContext.clearRect(0, 0, this._clippingCanvas.width, this._clippingCanvas.height); - this._clippingContext.save(); - - if(item._clip){ - var box = item.imageToViewportRectangle(item._clip, true); - var rect = this.viewportToDrawerRectangle(box); - this._setClip(rect); + _imageUnloadedHandler(event){ + let canvas = event.context2D.canvas; + this._cleanupImageData(canvas); } - if(item._croppingPolygons){ - let polygons = item._croppingPolygons.map(function (polygon) { - return polygon.map(function (coord) { - let point = item.imageToViewportCoordinates(coord.x, coord.y, true) - .rotate(_this.viewer.viewport.getRotation(true), _this.viewer.viewport.getCenter(true)); - let clipPoint = _this._viewportCoordToDrawerCoord(point); - return clipPoint; - }); - }); + + _cleanupImageData(tileCanvas){ + let textureInfo = this._TextureMap.get(tileCanvas); + //remove from the map + this._TextureMap.delete(tileCanvas); + + //release the texture from the GPU + if(textureInfo){ + this._gl.deleteTexture(textureInfo.texture); + } + + // release the position buffer from the GPU + // TO DO: do this! + } + // private + // necessary for clip testing to pass (test uses spyOnce(drawer._setClip)) + _setClip(rect){ this._clippingContext.beginPath(); - polygons.forEach(function (polygon) { - polygon.forEach(function (coord, i) { - _this._clippingContext[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y); - }); - }); + this._clippingContext.rect(rect.x, rect.y, rect.width, rect.height); this._clippingContext.clip(); } + _renderToClippingCanvas(item){ + let _this = this; - this._clippingContext.drawImage(this._renderingCanvas, 0, 0); + this._clippingContext.clearRect(0, 0, this._clippingCanvas.width, this._clippingCanvas.height); + this._clippingContext.save(); - this._clippingContext.restore(); - } - - // private - _offsetForRotation(options) { - var point = options.point ? - options.point.times($.pixelDensityRatio) : - new $.Point(this._outputCanvas.width / 2, this._outputCanvas.height / 2); - - var context = this._outputContext; - context.save(); - - context.translate(point.x, point.y); - if(this.viewport.flipped){ - context.rotate(Math.PI / 180 * -options.degrees); - context.scale(-1, 1); - } else{ - context.rotate(Math.PI / 180 * options.degrees); - } - context.translate(-point.x, -point.y); - } - - - /** - * @private - * @inner - * This function converts the given point from to the drawer coordinate by - * multiplying it with the pixel density. - * This function does not take rotation into account, thus assuming provided - * point is at 0 degree. - * @param {OpenSeadragon.Point} point - the pixel point to convert - * @returns {OpenSeadragon.Point} Point in drawer coordinate system. - */ - _viewportCoordToDrawerCoord(point) { - var vpPoint = this.viewport.pixelFromPointNoRotate(point, true); - return new $.Point( - vpPoint.x * $.pixelDensityRatio, - vpPoint.y * $.pixelDensityRatio - ); - } - - // private - _drawDebugInfo( tilesToDraw, tiledImage, stroke, fill ) { - - for ( var i = tilesToDraw.length - 1; i >= 0; i-- ) { - var tile = tilesToDraw[ i ].tile; - try { - this._drawDebugInfoOnTile(tile, tilesToDraw.length, i, tiledImage, stroke, fill); - } catch(e) { - $.console.error(e); + if(item._clip){ + var box = item.imageToViewportRectangle(item._clip, true); + var rect = this.viewportToDrawerRectangle(box); + this._setClip(rect); } - } - } - // private - _drawDebugInfoOnTile(tile, count, i, tiledImage, stroke, fill) { - - var context = this._outputContext; - context.save(); - context.lineWidth = 2 * $.pixelDensityRatio; - context.font = 'small-caps bold ' + (13 * $.pixelDensityRatio) + 'px arial'; - context.strokeStyle = stroke; - context.fillStyle = fill; - - if (this.viewport.getRotation(true) % 360 !== 0 ) { - this._offsetForRotation({degrees: this.viewport.getRotation(true)}); - } - if (tiledImage.getRotation(true) % 360 !== 0) { - this._offsetForRotation({ - degrees: tiledImage.getRotation(true), - point: tiledImage.viewport.pixelFromPointNoRotate( - tiledImage._getRotationPoint(true), true) - }); - } - if (tiledImage.viewport.getRotation(true) % 360 === 0 && - tiledImage.getRotation(true) % 360 === 0) { - if(tiledImage._drawer.viewer.viewport.getFlip()) { - tiledImage._drawer._flip(); + if(item._croppingPolygons){ + let polygons = item._croppingPolygons.map(function (polygon) { + return polygon.map(function (coord) { + let point = item.imageToViewportCoordinates(coord.x, coord.y, true) + .rotate(_this.viewer.viewport.getRotation(true), _this.viewer.viewport.getCenter(true)); + let clipPoint = _this._viewportCoordToDrawerCoord(point); + return clipPoint; + }); + }); + this._clippingContext.beginPath(); + polygons.forEach(function (polygon) { + polygon.forEach(function (coord, i) { + _this._clippingContext[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y); + }); + }); + this._clippingContext.clip(); } + + this._clippingContext.drawImage(this._renderingCanvas, 0, 0); + + this._clippingContext.restore(); } - context.strokeRect( - tile.position.x * $.pixelDensityRatio, - tile.position.y * $.pixelDensityRatio, - tile.size.x * $.pixelDensityRatio, - tile.size.y * $.pixelDensityRatio - ); + // private + _offsetForRotation(options) { + var point = options.point ? + options.point.times($.pixelDensityRatio) : + new $.Point(this._outputCanvas.width / 2, this._outputCanvas.height / 2); - var tileCenterX = (tile.position.x + (tile.size.x / 2)) * $.pixelDensityRatio; - var tileCenterY = (tile.position.y + (tile.size.y / 2)) * $.pixelDensityRatio; + var context = this._outputContext; + context.save(); - // Rotate the text the right way around. - context.translate( tileCenterX, tileCenterY ); - context.rotate( Math.PI / 180 * -this.viewport.getRotation(true) ); - context.translate( -tileCenterX, -tileCenterY ); + context.translate(point.x, point.y); + if(this.viewport.flipped){ + context.rotate(Math.PI / 180 * -options.degrees); + context.scale(-1, 1); + } else{ + context.rotate(Math.PI / 180 * options.degrees); + } + context.translate(-point.x, -point.y); + } - if( tile.x === 0 && tile.y === 0 ){ - context.fillText( - "Zoom: " + this.viewport.getZoom(), - tile.position.x * $.pixelDensityRatio, - (tile.position.y - 30) * $.pixelDensityRatio - ); - context.fillText( - "Pan: " + this.viewport.getBounds().toString(), - tile.position.x * $.pixelDensityRatio, - (tile.position.y - 20) * $.pixelDensityRatio + + /** + * @private + * @inner + * This function converts the given point from to the drawer coordinate by + * multiplying it with the pixel density. + * This function does not take rotation into account, thus assuming provided + * point is at 0 degree. + * @param {OpenSeadragon.Point} point - the pixel point to convert + * @returns {OpenSeadragon.Point} Point in drawer coordinate system. + */ + _viewportCoordToDrawerCoord(point) { + var vpPoint = this.viewport.pixelFromPointNoRotate(point, true); + return new $.Point( + vpPoint.x * $.pixelDensityRatio, + vpPoint.y * $.pixelDensityRatio ); } - context.fillText( - "Level: " + tile.level, - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 20) * $.pixelDensityRatio - ); - context.fillText( - "Column: " + tile.x, - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 30) * $.pixelDensityRatio - ); - context.fillText( - "Row: " + tile.y, - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 40) * $.pixelDensityRatio - ); - context.fillText( - "Order: " + i + " of " + count, - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 50) * $.pixelDensityRatio - ); - context.fillText( - "Size: " + tile.size.toString(), - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 60) * $.pixelDensityRatio - ); - context.fillText( - "Position: " + tile.position.toString(), - (tile.position.x + 10) * $.pixelDensityRatio, - (tile.position.y + 70) * $.pixelDensityRatio - ); - if (this.viewport.getRotation(true) % 360 !== 0 ) { - this._restoreRotationChanges(); + // private + _drawDebugInfo( tilesToDraw, tiledImage, stroke, fill ) { + + for ( var i = tilesToDraw.length - 1; i >= 0; i-- ) { + var tile = tilesToDraw[ i ].tile; + try { + this._drawDebugInfoOnTile(tile, tilesToDraw.length, i, tiledImage, stroke, fill); + } catch(e) { + $.console.error(e); + } + } } - if (tiledImage.getRotation(true) % 360 !== 0) { - this._restoreRotationChanges(); + // private + _drawDebugInfoOnTile(tile, count, i, tiledImage, stroke, fill) { + + var context = this._outputContext; + context.save(); + context.lineWidth = 2 * $.pixelDensityRatio; + context.font = 'small-caps bold ' + (13 * $.pixelDensityRatio) + 'px arial'; + context.strokeStyle = stroke; + context.fillStyle = fill; + + if (this.viewport.getRotation(true) % 360 !== 0 ) { + this._offsetForRotation({degrees: this.viewport.getRotation(true)}); + } + if (tiledImage.getRotation(true) % 360 !== 0) { + this._offsetForRotation({ + degrees: tiledImage.getRotation(true), + point: tiledImage.viewport.pixelFromPointNoRotate( + tiledImage._getRotationPoint(true), true) + }); + } + if (tiledImage.viewport.getRotation(true) % 360 === 0 && + tiledImage.getRotation(true) % 360 === 0) { + if(tiledImage._drawer.viewer.viewport.getFlip()) { + tiledImage._drawer._flip(); + } + } + + context.strokeRect( + tile.position.x * $.pixelDensityRatio, + tile.position.y * $.pixelDensityRatio, + tile.size.x * $.pixelDensityRatio, + tile.size.y * $.pixelDensityRatio + ); + + var tileCenterX = (tile.position.x + (tile.size.x / 2)) * $.pixelDensityRatio; + var tileCenterY = (tile.position.y + (tile.size.y / 2)) * $.pixelDensityRatio; + + // Rotate the text the right way around. + context.translate( tileCenterX, tileCenterY ); + context.rotate( Math.PI / 180 * -this.viewport.getRotation(true) ); + context.translate( -tileCenterX, -tileCenterY ); + + if( tile.x === 0 && tile.y === 0 ){ + context.fillText( + "Zoom: " + this.viewport.getZoom(), + tile.position.x * $.pixelDensityRatio, + (tile.position.y - 30) * $.pixelDensityRatio + ); + context.fillText( + "Pan: " + this.viewport.getBounds().toString(), + tile.position.x * $.pixelDensityRatio, + (tile.position.y - 20) * $.pixelDensityRatio + ); + } + context.fillText( + "Level: " + tile.level, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 20) * $.pixelDensityRatio + ); + context.fillText( + "Column: " + tile.x, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 30) * $.pixelDensityRatio + ); + context.fillText( + "Row: " + tile.y, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 40) * $.pixelDensityRatio + ); + context.fillText( + "Order: " + i + " of " + count, + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 50) * $.pixelDensityRatio + ); + context.fillText( + "Size: " + tile.size.toString(), + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 60) * $.pixelDensityRatio + ); + context.fillText( + "Position: " + tile.position.toString(), + (tile.position.x + 10) * $.pixelDensityRatio, + (tile.position.y + 70) * $.pixelDensityRatio + ); + + if (this.viewport.getRotation(true) % 360 !== 0 ) { + this._restoreRotationChanges(); + } + if (tiledImage.getRotation(true) % 360 !== 0) { + this._restoreRotationChanges(); + } + + if (tiledImage.viewport.getRotation(true) % 360 === 0 && + tiledImage.getRotation(true) % 360 === 0) { + if(tiledImage._drawer.viewer.viewport.getFlip()) { + tiledImage._drawer._flip(); + } + } + + context.restore(); } - if (tiledImage.viewport.getRotation(true) % 360 === 0 && - tiledImage.getRotation(true) % 360 === 0) { - if(tiledImage._drawer.viewer.viewport.getFlip()) { - tiledImage._drawer._flip(); + // private + _restoreRotationChanges() { + var context = this._outputContext; + context.restore(); + } + + // modified from https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Adding_2D_content_to_a_WebGL_context + static initShaderProgram(gl, vsSource, fsSource) { + const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); + const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); + + // Create the shader program + + const shaderProgram = gl.createProgram(); + gl.attachShader(shaderProgram, vertexShader); + gl.attachShader(shaderProgram, fragmentShader); + gl.linkProgram(shaderProgram); + + // If creating the shader program failed, alert + + if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { + alert( + `Unable to initialize the shader program: ${gl.getProgramInfoLog( + shaderProgram + )}` + ); + return null; + } + + return shaderProgram; + + function loadShader(gl, type, source) { + const shader = gl.createShader(type); + + // Send the source to the shader object + + gl.shaderSource(shader, source); + + // Compile the shader program + + gl.compileShader(shader); + + // See if it compiled successfully + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + alert( + `An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}` + ); + gl.deleteShader(shader); + return null; + } + + return shader; } } - context.restore(); - } + }; - // private - _restoreRotationChanges() { - var context = this._outputContext; - context.restore(); - } + // $.WebGLDrawer.Filter = class{ + // constructor(gl){ + // this.gl = gl; + // } + // apply(){ - // modified from https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Adding_2D_content_to_a_WebGL_context - static initShaderProgram(gl, vsSource, fsSource) { - const vertexShader = this.loadShader(gl, gl.VERTEX_SHADER, vsSource); - const fragmentShader = this.loadShader(gl, gl.FRAGMENT_SHADER, fsSource); - - // Create the shader program - - const shaderProgram = gl.createProgram(); - gl.attachShader(shaderProgram, vertexShader); - gl.attachShader(shaderProgram, fragmentShader); - gl.linkProgram(shaderProgram); - - // If creating the shader program failed, alert - - if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { - alert( - `Unable to initialize the shader program: ${gl.getProgramInfoLog( - shaderProgram - )}` - ); - return null; - } - - return shaderProgram; - } - - // - // creates a shader of the given type, uploads the source and - // compiles it. - // - static loadShader(gl, type, source) { - const shader = gl.createShader(type); - - // Send the source to the shader object - - gl.shaderSource(shader, source); - - // Compile the shader program - - gl.compileShader(shader); - - // See if it compiled successfully - - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - alert( - `An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}` - ); - gl.deleteShader(shader); - return null; - } - - return shader; - } -}; + // } + // }; }( OpenSeadragon )); diff --git a/test/demo/webgl.html b/test/demo/drawercomparison.html similarity index 95% rename from test/demo/webgl.html rename to test/demo/drawercomparison.html index 5094cc72..ad82b6f6 100644 --- a/test/demo/webgl.html +++ b/test/demo/drawercomparison.html @@ -1,14 +1,13 @@ - OSD-ThreeJS + Drawer Comparison Demo - - + + + +
+ +

WebGLDrawer demonstration

+
+
+
+
+
+ + +
+

Image options (drag and drop to re-order images)

+ + +
+ +

Example code

+
+
+
+ Drawer options are shown below. +
+
+    let viewer = OpenSeadragon({
+        ...
+        drawer: 'webgl',
+        drawerOptions: {
+            webgl: {
+                programs: [],
+            }
+        }
+        ...
+    });
+                
+
+
+
+
+ + + + + + diff --git a/test/demo/webglfiltering.js b/test/demo/webglfiltering.js new file mode 100644 index 00000000..c5357c0b --- /dev/null +++ b/test/demo/webglfiltering.js @@ -0,0 +1,298 @@ +const sources = { + "rainbow":"../data/testpattern.dzi", + "leaves":"../data/iiif_2_0_sizes/info.json", + "bblue":{ + type:'image', + url: "../data/BBlue.png", + }, + "duomo":"https://openseadragon.github.io/example-images/duomo/duomo.dzi", +} +const labels = { + rainbow: 'Rainbow Grid', + leaves: 'Leaves', + bblue: 'Blue B', + duomo: 'Duomo', +} + +// viewer1: context2d drawer +let viewer1 = window.viewer1 = OpenSeadragon({ + id: "webgl", + prefixUrl: "../../build/openseadragon/images/", + minZoomImageRatio:0.01, + maxZoomPixelRatio:100, + smoothTileEdgesMinZoom:1.1, + crossOriginPolicy: 'Anonymous', + ajaxWithCredentials: false, + // maxImageCacheCount: 30, + drawer:OpenSeadragon.WebGL2Drawer, + blendTime:0, + drawerOptions:{ + webgl:{ + programs:[makeRedFilter()], + } + } +}); + +setupImageControls(); + + + +class ColorChanelFilter { + constructor() { + this.red = 1; + this.blue = 1; + this.green = 1; + + this.program = null; + } + createProgram(gl){ + this.program = OpenSeadragon.WebGLDrawer.initShaderProgram(gl, this.getVertexShaderCode(), this.getFragmentShaderCode()); + } + apply(gl, tiledImageTexture, tiledImage){ + + } + getVertexShaderCode(){ + return ` + attribute vec2 a_output_position; + attribute vec2 a_texture_position; + + uniform mat3 u_matrix; + + varying vec2 v_texCoord; + + void main() { + gl_Position = vec4(u_matrix * vec3(a_output_position, 1), 1); + + v_texCoord = a_texture_position; + } + `; + } + getFragmentShaderCode(){ + return ` + precision mediump float; + + // our texture + uniform sampler2D u_image; + + // the texCoords passed in from the vertex shader. + varying vec2 v_texCoord; + + // the opacity multiplier for the image + uniform vec3 color_multiplier; + + void main() { + gl_FragColor = texture2D(u_image, v_texCoord); + gl_FragColor *= vec4(color_multiplier, 1); + } + `; + } + +} + + + + + +function setupImageControls(){ + +$('#image-picker').sortable({ + update: function(event, ui){ + let thisItem = ui.item.find('.toggle').data('item1'); + let items = $('#image-picker input.toggle:checked').toArray().map(item=>$(item).data('item1')); + let newIndex = items.indexOf(thisItem); + if(thisItem){ + viewer1.world.setItemIndex(thisItem, newIndex); + } + } +}); + +Object.keys(sources).forEach((key, index)=>{ + let element = makeImagePickerElement(key, labels[key]) + $('#image-picker').append(element); + if(index === 0){ + element.find('.toggle').prop('checked',true); + } +}) + +$('#image-picker input.toggle').on('change',function(){ + let data = $(this).data(); + if(this.checked){ + addTileSource(viewer1, data.image, this); + } else { + if(data.item1){ + viewer1.world.removeItem(data.item1); + $(this).data({item1: null}); + } + } +}).trigger('change'); + +$('#image-picker input:not(.toggle)').on('change',function(){ + let data = $(this).data(); + let value = $(this).val(); + let tiledImage1 = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item1'); + updateTiledImage(tiledImage1, data, value, this); +}); + +function updateTiledImage(tiledImage, data, value, item){ + if(tiledImage){ + //item = tiledImage + let field = data.field; + if(field == 'x'){ + let bounds = tiledImage.getBoundsNoRotate(); + let position = new OpenSeadragon.Point(Number(value), bounds.y); + tiledImage.setPosition(position); + } else if ( field == 'y'){ + let bounds = tiledImage.getBoundsNoRotate(); + let position = new OpenSeadragon.Point(bounds.x, Number(value)); + tiledImage.setPosition(position); + } else if (field == 'width'){ + tiledImage.setWidth(Number(value)); + } else if (field == 'degrees'){ + tiledImage.setRotation(Number(value)); + } else if (field == 'opacity'){ + tiledImage.setOpacity(Number(value)); + } else if (field == 'flipped'){ + tiledImage.setFlip($(item).prop('checked')); + } else if (field == 'cropped'){ + if( $(item).prop('checked') ){ + let croppingPolygons = [ [{x:200, y:200}, {x:800, y:200}, {x:500, y:800}] ]; + tiledImage.setCroppingPolygons(croppingPolygons); + } else { + tiledImage.resetCroppingPolygons(); + } + } else if (field == 'clipped'){ + if( $(item).prop('checked') ){ + let clipRect = new OpenSeadragon.Rect(2000, 0, 3000, 4000); + tiledImage.setClip(clipRect); + } else { + tiledImage.setClip(null); + } + } + else if (field == 'debug'){ + if( $(item).prop('checked') ){ + tiledImage.debugMode = true; + } else { + tiledImage.debugMode = false; + } + } + } +} + +$('.image-options select[data-field=composite]').append(getCompositeOperationOptions()).on('change',function(){ + let data = $(this).data(); + let tiledImage1 = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item1'); + if(tiledImage1){ + tiledImage1.setCompositeOperation(this.value == 'null' ? null : this.value); + } + let tiledImage2 = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item2'); + if(tiledImage2){ + tiledImage2.setCompositeOperation(this.value == 'null' ? null : this.value); + } +}).trigger('change'); + +$('.image-options select[data-field=wrapping]').append(getWrappingOptions()).on('change',function(){ + let data = $(this).data(); + let tiledImage = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item1'); + if(tiledImage){ + switch(this.value){ + case "None": tiledImage.wrapHorizontal = tiledImage.wrapVertical = false; break; + case "Horizontal": tiledImage.wrapHorizontal = true; tiledImage.wrapVertical = false; break; + case "Vertical": tiledImage.wrapHorizontal = false; tiledImage.wrapVertical = true; break; + case "Both": tiledImage.wrapHorizontal = tiledImage.wrapVertical = true; break; + } + tiledImage.viewer.raiseEvent('opacity-change');//trigger a redraw for the webgl renderer. TODO: fix this hack. + } + tiledImage = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item2'); + if(tiledImage){ + switch(this.value){ + case "None": tiledImage.wrapHorizontal = tiledImage.wrapVertical = false; break; + case "Horizontal": tiledImage.wrapHorizontal = true; tiledImage.wrapVertical = false; break; + case "Vertical": tiledImage.wrapHorizontal = false; tiledImage.wrapVertical = true; break; + case "Both": tiledImage.wrapHorizontal = tiledImage.wrapVertical = true; break; + } + tiledImage.viewer.raiseEvent('opacity-change');//trigger a redraw for the webgl renderer. TODO: fix this hack. + } +}).trigger('change'); + +function getWrappingOptions(){ + let opts = ['None', 'Horizontal', 'Vertical', 'Both']; + let elements = opts.map((opt, i)=>{ + let el = $('