From 386ca85db8297d50897fa94848f2b490aa28392b Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 26 Jun 2023 21:29:08 -0400 Subject: [PATCH] implement native webgl renderer, and many associated changes related to drawing pipeline and testing --- Gruntfile.js | 1 + src/context2ddrawer.js | 47 +- src/drawerbase.js | 32 +- src/htmldrawer.js | 4 - src/imagetilesource.js | 18 +- src/navigator.js | 3 - src/openseadragon.js | 35 +- src/tile.js | 8 +- src/tilecache.js | 34 +- src/tiledimage.js | 155 +++-- src/viewer.js | 20 +- src/webgldrawer.js | 872 +++++++++++++++++++++++++ src/world.js | 6 +- test/demo/threejsdrawer.js | 30 +- test/demo/webgl.html | 91 +-- test/demo/webgl.js | 203 ++++-- test/modules/ajax-tiles.js | 3 + test/modules/basic.js | 19 +- test/modules/controls.js | 3 + test/modules/drawer.js | 14 +- test/modules/events.js | 10 + test/modules/formats.js | 13 +- test/modules/imageloader.js | 3 + test/modules/multi-image.js | 3 + test/modules/navigator.js | 9 + test/modules/overlays.js | 8 + test/modules/referencestrip.js | 3 + test/modules/tiledimage.js | 29 +- test/modules/tilesource-dynamic-url.js | 4 + test/modules/units.js | 3 + test/modules/viewport.js | 3 + test/modules/world.js | 3 + 32 files changed, 1423 insertions(+), 266 deletions(-) create mode 100644 src/webgldrawer.js diff --git a/Gruntfile.js b/Gruntfile.js index e159dd03..850bb9e0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -60,6 +60,7 @@ module.exports = function(grunt) { "src/drawerbase.js", "src/htmldrawer.js", "src/context2ddrawer.js", + "src/webgldrawer.js", "src/viewport.js", "src/tiledimage.js", "src/tilecache.js", diff --git a/src/context2ddrawer.js b/src/context2ddrawer.js index 20133570..0b0c2cec 100644 --- a/src/context2ddrawer.js +++ b/src/context2ddrawer.js @@ -108,9 +108,6 @@ class Context2dDrawer extends $.DrawerBase{ // _this._updateViewportWithTiledImage(tiledImage); _this._drawTiles(tiledImage); } - else { - tiledImage._needsDraw = false; - } }); } @@ -371,11 +368,10 @@ class Context2dDrawer extends $.DrawerBase{ } // Iterate over the tiles to draw, and draw them - for (var i = lastDrawn.length - 1; i >= 0; i--) { + for (var i = 0; i < lastDrawn.length; i++) { tile = lastDrawn[ i ]; - this._drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, + this._drawTile( tile, tiledImage, useSketch, sketchScale, sketchTranslate, shouldRoundPositionAndSize, tiledImage.source ); - tile.beingDrawn = true; if( this.viewer ){ /** @@ -455,6 +451,24 @@ class Context2dDrawer extends $.DrawerBase{ this._drawDebugInfo( tiledImage, lastDrawn ); } + /** + * @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 * @inner @@ -466,7 +480,7 @@ class Context2dDrawer extends $.DrawerBase{ for ( var i = lastDrawn.length - 1; i >= 0; i-- ) { var tile = lastDrawn[ i ]; try { - this.drawDebugInfo(tile, lastDrawn.length, i, tiledImage); + this._drawDebugInfoOnTile(tile, lastDrawn.length, i, tiledImage); } catch(e) { $.console.error(e); } @@ -500,8 +514,7 @@ class Context2dDrawer extends $.DrawerBase{ * @inner * Draws the given tile. * @param {OpenSeadragon.Tile} tile - The tile to draw. - * @param {Function} drawingHandler - Method for firing the drawing event if using canvas. - * drawingHandler({context, tile, rendered}) + * @param {OpenSeadragon.TiledImage} tiledImage - The tiled image being drawn. * @param {Boolean} useSketch - Whether to use the sketch canvas or not. * where rendered is the context with the pre-drawn image. * @param {Float} [scale=1] - Apply a scale to tile position and size. Defaults to 1. @@ -511,13 +524,13 @@ class Context2dDrawer extends $.DrawerBase{ * context. * @param {OpenSeadragon.TileSource} source - The source specification of the tile. */ - _drawTile( tile, drawingHandler, useSketch, scale, translate, shouldRoundPositionAndSize, source) { + _drawTile( tile, tiledImage, useSketch, scale, translate, shouldRoundPositionAndSize, source) { $.console.assert(tile, '[Drawer._drawTile] tile is required'); - $.console.assert(drawingHandler, '[Drawer._drawTile] drawingHandler is required'); + $.console.assert(tiledImage, '[Drawer._drawTile] drawingHandler is required'); var context = this._getContext(useSketch); scale = scale || 1; - this._drawTileToCanvas(tile, context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source); + this._drawTileToCanvas(tile, context, tiledImage, scale, translate, shouldRoundPositionAndSize, source); } @@ -528,7 +541,7 @@ class Context2dDrawer extends $.DrawerBase{ * @function * @param {OpenSeadragon.Tile} tile - the tile to draw to the canvas * @param {Canvas} context - * @param {Function} drawingHandler - Method for firing the drawing event. + * @param {OpenSeadragon.TiledImage} tiledImage - Method for firing the drawing event. * drawingHandler({context, tile, rendered}) * where rendered is the context with the pre-drawn image. * @param {Number} [scale=1] - Apply a scale to position and size @@ -538,7 +551,7 @@ class Context2dDrawer extends $.DrawerBase{ * context. * @param {OpenSeadragon.TileSource} source - The source specification of the tile. */ - _drawTileToCanvas( tile, context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source) { + _drawTileToCanvas( tile, context, tiledImage, scale, translate, shouldRoundPositionAndSize, source) { var position = tile.position.times($.pixelDensityRatio), size = tile.size.times($.pixelDensityRatio), @@ -599,9 +612,7 @@ class Context2dDrawer extends $.DrawerBase{ ); } - // This gives the application a chance to make image manipulation - // changes as we are rendering the image - drawingHandler({context: context, tile: tile, rendered: rendered}); + this._raiseTileDrawingEvent(tiledImage, context, tile, rendered); var sourceWidth, sourceHeight; if (tile.sourceBounds) { @@ -805,7 +816,7 @@ class Context2dDrawer extends $.DrawerBase{ } // private - drawDebugInfo(tile, count, i, tiledImage) { + _drawDebugInfoOnTile(tile, count, i, tiledImage) { var colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length; var context = this.context; diff --git a/src/drawerbase.js b/src/drawerbase.js index fd8e94f8..93dbcf1b 100644 --- a/src/drawerbase.js +++ b/src/drawerbase.js @@ -90,6 +90,7 @@ $.DrawerBase = class DrawerBase{ this.container = $.getElement( options.element ); // TO DO: Does this need to be in DrawerBase, or only in Drawer implementations? + // Original commment: // We force our container to ltr because our drawing math doesn't work in rtl. // This issue only affects our canvas renderer, but we do it always for consistency. // Note that this means overlays you want to be rtl need to be explicitly set to rtl. @@ -199,6 +200,9 @@ $.DrawerBase = class DrawerBase{ * placeholder methods are still in place. */ _checkForAPIOverrides(){ + if(this.createDrawingElement === $.DrawerBase.prototype.createDrawingElement){ + throw("[drawer].createDrawingElement must be implemented by child class"); + } if(this.draw === $.DrawerBase.prototype.draw){ throw("[drawer].draw must be implemented by child class"); } @@ -208,12 +212,38 @@ $.DrawerBase = class DrawerBase{ if(this.destroy === $.DrawerBase.prototype.destroy){ throw("[drawer].destroy must be implemented by child class"); } - if(this.setImageSmoothingEnabled === $.DrawerBase.prototype.setImageSmoothingEnabled){ throw("[drawer].setImageSmoothingEnabled must be implemented by child class"); } } + _raiseTileDrawingEvent(tiledImage, context, tile, rendered){ + /** + * This event is fired just before the tile is drawn giving the application a chance to alter the image. + * + * NOTE: This event is only fired in certain drawing contexts: either the 'context2d' drawer is + * being used, or the 'webgl' drawer with 'drawerOptions.webgl.continuousTileRefresh'. + * + * @event tile-drawing + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.Tile} tile - The Tile being drawn. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {CanvasRenderingContext2D} context - The HTML canvas context being drawn into. + * @property {CanvasRenderingContext2D} rendered - The HTML canvas context containing the tile imagery. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('tile-drawing', { + tiledImage: tiledImage, + context: context, + tile: tile, + rendered: rendered + }); + } + + + // Utility functions diff --git a/src/htmldrawer.js b/src/htmldrawer.js index 7759781c..43759633 100644 --- a/src/htmldrawer.js +++ b/src/htmldrawer.js @@ -84,9 +84,6 @@ class HTMLDrawer extends $.DrawerBase{ if (tiledImage.opacity !== 0 || tiledImage._preload) { _this._drawTiles(tiledImage); } - else { - tiledImage._needsDraw = false; - } }); } @@ -145,7 +142,6 @@ class HTMLDrawer extends $.DrawerBase{ for (var i = lastDrawn.length - 1; i >= 0; i--) { var tile = lastDrawn[ i ]; this._drawTile( tile ); - tile.beingDrawn = true; if( this.viewer ){ /** diff --git a/src/imagetilesource.js b/src/imagetilesource.js index 1ed4f85c..4596b2c0 100644 --- a/src/imagetilesource.js +++ b/src/imagetilesource.js @@ -196,8 +196,8 @@ * Destroys ImageTileSource * @function */ - destroy: function () { - this._freeupCanvasMemory(); + destroy: function (viewer) { + this._freeupCanvasMemory(viewer); }, // private @@ -267,11 +267,23 @@ * and Safari keeps canvas until its height and width will be set to 0). * @function */ - _freeupCanvasMemory: function () { + _freeupCanvasMemory: function (viewer) { for (var i = 0; i < this.levels.length; i++) { if(this.levels[i].context2D){ this.levels[i].context2D.canvas.height = 0; this.levels[i].context2D.canvas.width = 0; + + /** + * Triggered when an image has just been unloaded + * + * @event image-unloaded + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {CanvasRenderingContext2D} context2D - The context that is being unloaded + */ + viewer.raiseEvent("image-unloaded", { + context2D: this.levels[i].context2D + }); } } }, diff --git a/src/navigator.js b/src/navigator.js index 2013d83d..21544b9a 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -170,9 +170,6 @@ $.Navigator = function( options ){ style.border = borderWidth + 'px solid ' + options.displayRegionColor; style.margin = '0px'; style.padding = '0px'; - //TODO: IE doesn't like this property being set - //try{ style.outline = '2px auto #909'; }catch(e){/*ignore*/} - style.background = 'transparent'; // We use square bracket notation on the statement below, because float is a keyword. diff --git a/src/openseadragon.js b/src/openseadragon.js index 7f3858cd..1e6f1d75 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -190,15 +190,15 @@ * Zoom level to use when image is first opened or the home button is clicked. * If 0, adjusts to fit viewer. * - * @property {String|DrawerImplementation|Array} [drawer = ['context2d', 'html']] + * @property {String|DrawerImplementation|Array} [drawer = ['webgl', 'context2d', 'html']] * Which drawer to use. Valid strings are 'context2d' and 'html'. Valid drawer * implementations are constructors of classes that extend OpenSeadragon.DrawerBase. * An array of strings and/or constructors can be used to indicate the priority * of different implementations, which will be tried in order based on browser support. * - * @property {Object} [drawerOptions = {}] - * Options to pass to the selected drawer implementation. See documentation - * for Drawer classes that extend DrawerBase for further information. + * @property {Object} drawerOptions + * Options to pass to the selected drawer implementation. For details + * please @see {@link drawerOptions}. * * @property {Number} [opacity=1] * Default proportional opacity of the tiled images (1=opaque, 0=hidden) @@ -1346,9 +1346,32 @@ function OpenSeadragon( options ){ compositeOperation: null, // to be passed into each TiledImage // DRAWER SETTINGS - drawer: ['context2d', 'html'], // prefer using canvas, fallback to html - drawerOptions: {}, + drawer: ['webgl', 'context2d', 'html'], // prefer using webgl, context2d, fallback to html useCanvas: true, // deprecated - set drawer and drawerOptions + /** + * drawerOptions dictionary. + * @type {Object} drawerOptions + * @property {Object} webgl - options if the WebGLDrawer is used. + * Set 'continuousTileFresh: true' if tile data is modified programmatically + * by filtering plugins or similar. + * @property {Object} context2d - options if the Context2dDrawer is used + * @property {Object} html - options if the HTMLDrawer is used + * @property {Object} custom - options if a custom drawer is used + */ + drawerOptions: { + webgl: { + continuousTileRefresh: false, + }, + context2d: { + + }, + html: { + + }, + custom: { + + } + }, // TILED IMAGE SETTINGS preload: false, // to be passed into each TiledImage diff --git a/src/tile.js b/src/tile.js index 682236fb..e95633a1 100644 --- a/src/tile.js +++ b/src/tile.js @@ -81,6 +81,12 @@ * @memberof OpenSeadragon.Tile# */ this.bounds = bounds; + /** + * Where this tile fits, in normalized coordinates, after positioning + * @member {OpenSeadragon.Rect} positionedBounds + * @memberof OpenSeadragon.Tile# + */ + this.positionedBounds = new OpenSeadragon.Rect(bounds.x, bounds.y, bounds.width, bounds.height); /** * The portion of the tile to use as the source of the drawing operation, in pixels. Note that * this only works when drawing with canvas; when drawing with HTML the entire tile is always used. @@ -324,7 +330,7 @@ * @returns {CanvasRenderingContext2D} */ getCanvasContext: function() { - return this.context2D || this.cacheImageRecord.getRenderedContext(); + return this.context2D || (this.cacheImageRecord && this.cacheImageRecord.getRenderedContext()); }, /** diff --git a/src/tilecache.js b/src/tilecache.js index d890b8a8..7d9e5478 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -236,19 +236,50 @@ $.TileCache.prototype = { var tile = tileRecord.tile; var tiledImage = tileRecord.tiledImage; + // tile.getCanvasContext should always exist in normal usage (with $.Tile) + // but the tile cache test passes in a dummy object + let context2D = tile.getCanvasContext && tile.getCanvasContext(); + tile.unload(); tile.cacheImageRecord = null; var imageRecord = this._imagesLoaded[tile.cacheKey]; + if(!imageRecord){ + return; + } imageRecord.removeTile(tile); if (!imageRecord.getTileCount()) { + imageRecord.destroy(); delete this._imagesLoaded[tile.cacheKey]; this._imagesLoadedCount--; + + if(context2D){ + /** + * Free up canvas memory + * (iOS 12 or higher on 2GB RAM device has only 224MB canvas memory, + * and Safari keeps canvas until its height and width will be set to 0). + */ + context2D.canvas.width = 0; + context2D.canvas.height = 0; + + /** + * Triggered when an image has just been unloaded + * + * @event image-unloaded + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {CanvasRenderingContext2D} context2D - The context that is being unloaded + */ + tiledImage.viewer.raiseEvent("image-unloaded", { + context2D: context2D + }); + } + } /** - * Triggered when a tile has just been unloaded from memory. + * Triggered when a tile has just been unloaded from the cache. * * @event tile-unloaded * @memberof OpenSeadragon.Viewer @@ -260,6 +291,7 @@ $.TileCache.prototype = { tile: tile, tiledImage: tiledImage }); + } }; diff --git a/src/tiledimage.js b/src/tiledimage.js index a2d6c34c..fceb30ae 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -85,7 +85,7 @@ * A set of headers to include when making tile AJAX requests. */ $.TiledImage = function( options ) { - var _this = this; + this._initialized = false; /** * The {@link OpenSeadragon.TileSource} that defines this TiledImage. * @member {OpenSeadragon.TileSource} source @@ -162,7 +162,10 @@ $.TiledImage = function( options ) { _needsDraw: true, // Does the tiledImage need to update the viewport again? _hasOpaqueTile: false, // Do we have even one fully opaque tile? _tilesLoading: 0, // The number of pending tile requests. - _tilesToDraw: [], // info about the tiles currently in the viewport + _tilesToDraw: [], // info about the tiles currently in the viewport, two deep: array[level][tile] + _lastDrawn: [], // array of tiles that were last fetched by the drawer + _isBlending: false, // Are any tiles still being blended? + _wasBlending: false, // Were any tiles blending before the last draw? //configurable settings springStiffness: $.DEFAULT_SETTINGS.springStiffness, animationTime: $.DEFAULT_SETTINGS.animationTime, @@ -220,30 +223,9 @@ $.TiledImage = function( options ) { this.fitBounds(fitBounds, fitBoundsPlacement, true); } - // We need a callback to give image manipulation a chance to happen - this._drawingHandler = function(args) { - /** - * This event is fired just before the tile is drawn giving the application a chance to alter the image. - * - * NOTE: This event is only fired when the drawer is using a <canvas>. - * - * @event tile-drawing - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.Tile} tile - The Tile being drawn. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {OpenSeadragon.Tile} context - The HTML canvas context being drawn into. - * @property {OpenSeadragon.Tile} rendered - The HTML canvas context containing the tile imagery. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - _this.viewer.raiseEvent('tile-drawing', $.extend({ - tiledImage: _this - }, args)); - }; - this._ownAjaxHeaders = {}; this.setAjaxHeaders(ajaxHeaders, false); + this._initialized = true; }; $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{ @@ -311,7 +293,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag if (updated || viewportChanged || !this._fullyLoaded){ let fullyLoadedFlag = this._updateLevelsForViewport(); - this._updateTilesInViewport(); this._setFullyLoaded(fullyLoadedFlag); } @@ -328,10 +309,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag /** * Mark this TiledImage as having been drawn, so that it will only be drawn - * again if something changes about the image + * again if something changes about the image. If the image is still blending, + * this will have no effect. + * @returns {Boolean} whether the item still needs to be drawn due to blending */ setDrawn: function(){ - this._needsDraw = false; + this._needsDraw = this._isBlending || this._wasBlending; + return this._needsDraw; }, /** @@ -341,7 +325,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this.reset(); if (this.source.destroy) { - this.source.destroy(); + this.source.destroy(this.viewer); } }, @@ -539,7 +523,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag imageX = imageX.x; } - var point = this._imageToViewportDelta(imageX, imageY); + var point = this._imageToViewportDelta(imageX, imageY, current); if (current) { point.x += this._xSpring.current.value; point.y += this._ySpring.current.value; @@ -934,9 +918,39 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag return this._flipped; }, set flipped(flipped){ + let changed = this._flipped !== !!flipped; this._flipped = !!flipped; - this._needsDraw = true; - this._raiseBoundsChange(); + if(changed){ + this.update(true); + this._needsDraw = true; + this._raiseBoundsChange(); + } + }, + + get wrapHorizontal(){ + return this._wrapHorizontal; + }, + set wrapHorizontal(wrap){ + let changed = this._wrapHorizontal !== !!wrap; + this._wrapHorizontal = !!wrap; + if(this._initialized && changed){ + this.update(true); + this._needsDraw = true; + // this._raiseBoundsChange(); + } + }, + + get wrapVertical(){ + return this._wrapVertical; + }, + set wrapVertical(wrap){ + let changed = this._wrapVertical !== !!wrap; + this._wrapVertical = !!wrap; + if(this._initialized && changed){ + this.update(true); + this._needsDraw = true; + // this._raiseBoundsChange(); + } }, get debugMode(){ @@ -1058,7 +1072,20 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @returns {Array} Array of Tiles that make up the current view */ getTilesToDraw: function(){ - return this._tilesToDraw; + + let tileArray = this._tilesToDraw.flat(); + // update all tiles (so blending can happen right at the time of drawing) + this._updateTilesInViewport(tileArray); + // _tilesToDraw might have been updated by the update; refresh it + tileArray = this._tilesToDraw.flat(); + + // mark the tiles as being drawn, so that they won't be discarded from + // the tileCache + tileArray.forEach(tileInfo => { + tileInfo.tile.beingDrawn = true; + }); + this._lastDrawn = tileArray; + return tileArray; }, /** @@ -1289,7 +1316,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag var currentTime = $.now(); // reset each tile's beingDrawn flag - this._tilesToDraw.forEach(tileinfo => { + this._lastDrawn.forEach(tileinfo => { tileinfo.tile.beingDrawn = false; }); // clear the list of tiles to draw @@ -1391,7 +1418,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag }; })(level, levelOpacity, currentTime); - this._tilesToDraw = this._tilesToDraw.concat(tiles.map(makeTileInfoObject)); + this._tilesToDraw[level] = tiles.map(makeTileInfoObject); // Stop the loop if lower-res tiles would all be covered by // already drawn tiles @@ -1419,47 +1446,54 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * Update all tiles that contribute to the current view * */ - _updateTilesInViewport: function() { - var _this = this; + _updateTilesInViewport: function(tiles) { + let currentTime = $.now(); + let _this = this; this._tilesLoading = 0; + this._wasBlending = this._isBlending; + this._isBlending = false; this.loadingCoverage = {}; + let lowestLevel = tiles.length ? tiles[0].level : 0; - var drawArea = this.getDrawArea(); + let drawArea = this.getDrawArea(); if(!drawArea){ return; } function updateTile(info){ - var tile = info.tile; + let tile = info.tile; if(tile && tile.loaded){ - var needsDraw = _this._blendTile( + let tileIsBlending = _this._blendTile( tile, tile.x, tile.y, info.level, info.levelOpacity, - info.currentTime + currentTime, + lowestLevel ); - if(needsDraw){ - _this._needsDraw = true; - } + _this._isBlending = _this._isBlending || tileIsBlending; + _this._needsDraw = _this._needsDraw || tileIsBlending || this._wasBlending; } } - // Update each tile in the _tilesToDraw list. As the tiles are updated, + // Update each tile in the _lastDrawn list. As the tiles are updated, // the coverage provided is also updated. If a level provides coverage // as part of this process, discard tiles from lower levels let level = 0; - for(let i = 0; i < this._tilesToDraw.length; i++){ - let tile = this._tilesToDraw[i]; + for(let i = 0; i < tiles.length; i++){ + let tile = tiles[i]; updateTile(tile); if(this._providesCoverage(this.coverage, tile.level)){ level = Math.max(level, tile.level); - // break; } } if(level > 0){ - this._tilesToDraw = this._tilesToDraw.filter(tile => tile.level >= level); + for( let levelKey in this._tilesToDraw ){ + if( levelKey < level ){ + delete this._tilesToDraw[levelKey]; + } + } } }, @@ -1478,10 +1512,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @param {Number} level * @param {Number} levelOpacity * @param {Number} currentTime - * @returns {Boolean} + * @param {Boolean} lowestLevel + * @returns {Boolean} whether the opacity of this tile has changed */ - _blendTile: function(tile, x, y, level, levelOpacity, currentTime ){ - var blendTimeMillis = 1000 * this.blendTime, + _blendTile: function(tile, x, y, level, levelOpacity, currentTime, lowestLevel ){ + let blendTimeMillis = 1000 * this.blendTime, deltaTime, opacity; @@ -1492,20 +1527,23 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag deltaTime = currentTime - tile.blendStart; opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; + // if this tile is at the lowest level being drawn, render at opacity=1 + if(level === lowestLevel){ + opacity = 1; + deltaTime = blendTimeMillis; + } + if ( this.alwaysBlend ) { opacity *= levelOpacity; } - tile.opacity = opacity; if ( opacity === 1 ) { this._setCoverage( this.coverage, level, x, y, true ); this._hasOpaqueTile = true; - } else if ( deltaTime < blendTimeMillis ) { - return true; } - - return false; + // return true if the tile is still blending + return deltaTime < blendTimeMillis; }, /** @@ -1647,6 +1685,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag boundsSize.x *= this._scaleSpring.current.value; boundsSize.y *= this._scaleSpring.current.value; + tile.positionedBounds.x = boundsTL.x; + tile.positionedBounds.y = boundsTL.y; + tile.positionedBounds.width = boundsSize.x; + tile.positionedBounds.height = boundsSize.y; + var positionC = viewport.pixelFromPointNoRotate(boundsTL, true), positionT = viewport.pixelFromPointNoRotate(boundsTL, false), sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true), diff --git a/src/viewer.js b/src/viewer.js index da334094..bcb7cc94 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -446,7 +446,7 @@ $.Viewer = function( options ) { delete this.drawerOptions.useCanvas; } let drawerPriority = Array.isArray(this.drawer) ? this.drawer : [this.drawer]; - let drawersToTry = drawerPriority.filter(d => ['context2d', 'html'].includes(d) || (d.prototype && d.prototype.isOpenSeadragonDrawer) ); + let drawersToTry = drawerPriority.filter(d => ['webgl', 'context2d', 'html'].includes(d) || (d.prototype && d.prototype.isOpenSeadragonDrawer) ); if(drawerPriority.length !== drawersToTry.length){ $.console.error('An invalid drawer was requested.'); } @@ -458,11 +458,19 @@ $.Viewer = function( options ) { this.drawer = null; // TO DO: how to deal with the possibility that none of the requested drawers are supported? for(let i = 0; i < drawersToTry.length; i++){ let Drawer = drawersToTry[i]; + let optsKey = null; // replace text-based option with appropriate constructor if (Drawer === 'context2d'){ Drawer = $.Context2dDrawer; + optsKey = 'context2d'; } else if (Drawer === 'html'){ Drawer = $.HTMLDrawer; + optsKey = 'html'; + } else if (Drawer === 'webgl'){ + Drawer = $.WebGLDrawer; + optsKey = 'webgl'; + } else { + optsKey = 'custom'; } // if the drawer is supported, create it and break the loop if (Drawer.prototype.isSupported()){ @@ -471,7 +479,7 @@ $.Viewer = function( options ) { viewport: this.viewport, element: this.canvas, debugGridColor: this.debugGridColor, - options: this.drawerOptions, + options: this.drawerOptions[optsKey], }); this.drawerOptions.constructor = Drawer; // TO DO: add an event that indicates which drawer was instantiated? @@ -479,6 +487,10 @@ $.Viewer = function( options ) { } // TO DO: add an event that indicates that the selected drawer could not be created? } + if(this.drawer === null){ + $.console.error('No drawer could be created!'); + throw('Error with creating the selected drawer(s)'); + } // Overlay container @@ -1090,7 +1102,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @returns {Boolean} */ isFullPage: function () { - return THIS[ this.hash ].fullPage; + return THIS[this.hash] && THIS[ this.hash ].fullPage; }, @@ -1137,7 +1149,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, return this; } - if ( fullPage ) { + if ( fullPage && this.element ) { this.elementSize = $.getElementSize( this.element ); this.pageScroll = $.getPageScroll(); diff --git a/src/webgldrawer.js b/src/webgldrawer.js new file mode 100644 index 00000000..9c9c7d33 --- /dev/null +++ b/src/webgldrawer.js @@ -0,0 +1,872 @@ +/* + * OpenSeadragon - WebGLDrawer + * + * Copyright (C) 2009 CodePlex Foundation + * 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( $ ){ + + // 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; + } + + 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 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, + ]); + } + + 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, + ]); + } + +} + +/** + * @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); + + this.destroyed = false; + // private members + + 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; + + // 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._setupRenderer(); + + 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){ + return; + } + + let overallMatrix = viewMatrix; + + 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); + + // update the view matrix to account for this image's rotation + let localMatrix = t1.multiply(imageRotationMatrix).multiply(t2); + overallMatrix = viewMatrix.multiply(localMatrix); + } + + 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); + } + } + + // 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; + if(tiledImage._croppingPolygons || tiledImage._clip){ + this._renderToClippingCanvas(tiledImage); + this._outputContext.drawImage(this._clippingCanvas, 0, 0); + + } else { + this._outputContext.drawImage(this._renderingCanvas, 0, 0); + } + this._outputContext.restore(); + if(tiledImage.debugMode){ + let colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length; + let strokeStyle = this.debugGridColor[colorIndex]; + let fillStyle = this.debugGridColor[colorIndex]; + this._drawDebugInfo(tilesToDraw, tiledImage, strokeStyle, fillStyle); + } + + // TO DO: this is necessary for the tests to pass, but doesn't totally make sense for the webgl drawer. + // Iterate over the tiles that were just drawn and fire the tile-drawn event + for(let i = 0; i < tilesToDraw.length; i++){ + let tile = tilesToDraw[i].tile; + + if( this.viewer ){ + /** + * Raised when a tile is drawn to the canvas + * + * @event tile-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 {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'tile-drawn', { + tiledImage: tiledImage, + tile: 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); + + 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; + + // 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); + + 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); + + } + + } + + _uploadImageData(tileContext){ + let gl = this._gl; + try{ + let canvas = tileContext.canvas; + if(!canvas){ + throw('Tile context does not have a canvas', tileContext); + } + 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); + } + 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(); + } + + // 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); + } + } + } + // 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(); + } + + // 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 = 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/src/world.js b/src/world.js index e766045d..30e9fc48 100644 --- a/src/world.js +++ b/src/world.js @@ -257,10 +257,10 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W */ draw: function() { this.viewer.drawer.draw(this._items); - this._items.forEach(function(item){ - item.setDrawn(); - }); this._needsDraw = false; + this._items.forEach(function(item){ + this._needsDraw = item.setDrawn() || this._needsDraw || true; + }); }, /** diff --git a/test/demo/threejsdrawer.js b/test/demo/threejsdrawer.js index 73a257bf..5abcce17 100644 --- a/test/demo/threejsdrawer.js +++ b/test/demo/threejsdrawer.js @@ -1,4 +1,10 @@ // import 'https://cdnjs.cloudflare.com/ajax/libs/three.js/0.149.0/three.min.js'; + +// TO DO LIST: +// TO DO: Viewport flip does not work right +// TO DO: wrapHorizontal and wrapVertical do not work right with scaled TiledImages +// TO DO: wrapping doesn't work right with resolution of wrapped part when zoomed in + import '../lib/three.js'; const THREE = window.THREE; @@ -182,7 +188,7 @@ export class ThreeJSDrawer extends OpenSeadragon.DrawerBase{ } - tiledImages.forEach(tiledImage => tiledImage._needsDraw = false); + //tiledImages.forEach(tiledImage => tiledImage.setDrawn()); } // Public API required by all Drawer implementations @@ -349,7 +355,7 @@ export class ThreeJSDrawer extends OpenSeadragon.DrawerBase{ } _tileUnloadedHandler(event){ - console.log('Tile unloaded',event); + // console.log('Tile unloaded',event); let tile = event.tile; if(!this._tileMap[tile.cacheKey]){ //already cleaned up @@ -581,6 +587,26 @@ export class ThreeJSDrawer extends OpenSeadragon.DrawerBase{ } // private + + /** + * @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) { + let $ = OpenSeadragon; + var vpPoint = this.viewport.pixelFromPointNoRotate(point, true); + return new $.Point( + vpPoint.x * $.pixelDensityRatio, + vpPoint.y * $.pixelDensityRatio + ); + } + _offsetForRotation(options) { var point = options.point ? options.point.times(OpenSeadragon.pixelDensityRatio) : diff --git a/test/demo/webgl.html b/test/demo/webgl.html index 196a65bc..bbee069c 100644 --- a/test/demo/webgl.html +++ b/test/demo/webgl.html @@ -68,7 +68,28 @@
-

Use a WebGL drawer implementation (using three.js) instead of the default context2d drawer

+ +

Compare behavior of Context2d and WebGL (via three.js) drawers

+
+
+

Context2d drawer (default in OSD <= 4.1.0)

+
+
+ +
+

New WebGL drawer

+
+
+
+ + +
+

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

+ + +
+ +

Use a custom plugin drawer - example using three.js

@@ -97,74 +118,6 @@
-

Compare behavior of Context2d and WebGL (via three.js) drawers

-
-
-

Use default OpenSeadragon viewer to pan/zoom

-
-
- -
-

WebGL drawer linked using event listeners

-
-
-
- - -
-

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

- - -
-

HTMLDrawer: legacy pre-HTML5 drawer that uses <img> elements for tiles

diff --git a/test/demo/webgl.js b/test/demo/webgl.js index cdcd71e5..471d750d 100644 --- a/test/demo/webgl.js +++ b/test/demo/webgl.js @@ -1,8 +1,3 @@ -//imports -import { ThreeJSDrawer } from './threejsdrawer.js'; -// import { default as Stats } from "https://cdnjs.cloudflare.com/ajax/libs/stats.js/17/Stats.js"; -//globals -// const canvas = document.querySelector('#three-canvas'); const sources = { "rainbow":"../data/testpattern.dzi", "leaves":"../data/iiif_2_0_sizes/info.json", @@ -25,63 +20,102 @@ var stats = null; // document.body.appendChild( stats.dom ); -//Double viewer setup for comparison - Context2dDrawer and ThreeJSDrawer +//Double viewer setup for comparison - Context2dDrawer and WebGLDrawer -var viewer = window.viewer = OpenSeadragon({ - id: "contentDiv", +let viewer1 = window.viewer1 = OpenSeadragon({ + id: "context2d", prefixUrl: "../../build/openseadragon/images/", - // minZoomImageRatio:0.8, - // maxZoomPixelRatio:0.5, minZoomImageRatio:0.01, maxZoomPixelRatio:100, smoothTileEdgesMinZoom:1.1, crossOriginPolicy: 'Anonymous', ajaxWithCredentials: false, - drawer:'context2d', + // maxImageCacheCount: 30, + drawer:'webgl', + blendTime:0 }); - -// Mirror the interactive viewer with Context2dDrawer onto a separate canvas using ThreeJSDrawer -let threeRenderer = window.threeRenderer = new ThreeJSDrawer({viewer, viewport: viewer.viewport, element:viewer.element, stats: stats}); -//make the test canvas mirror all changes to the viewer canvas -let viewerCanvas = viewer.drawer.canvas; -let canvas = threeRenderer.canvas; -let canvasContainer = $('#three-canvas-container').append(canvas); -viewer.addHandler("resize", function(){ - canvasContainer[0].style.width = viewerCanvas.clientWidth+'px'; - canvasContainer[0].style.height = viewerCanvas.clientHeight+'px'; - // canvas.width = viewerCanvas.width; - // canvas.height = viewerCanvas.height; -}); - - -// Single viewer showing how to use plugin Drawer via configuration -// Also shows sequence mode -var viewer2 = window.viewer2 = OpenSeadragon({ - id: "three-viewer", +let viewer2 = window.viewer2 = OpenSeadragon({ + id: "webgl", prefixUrl: "../../build/openseadragon/images/", minZoomImageRatio:0.01, - drawer: ThreeJSDrawer, - tileSources: [sources['leaves'], sources['rainbow'], sources['duomo']], - sequenceMode: true, - imageSmoothingEnabled: false, + maxZoomPixelRatio:100, + smoothTileEdgesMinZoom:1.1, crossOriginPolicy: 'Anonymous', - ajaxWithCredentials: false + ajaxWithCredentials: false, + // maxImageCacheCount: 30, + drawer:'webgl', + blendTime:0.0 }); -// Single viewer showing how to use plugin Drawer via configuration -// Also shows sequence mode -var viewer3 = window.viewer3 = OpenSeadragon({ - id: "htmldrawer", - drawer:'html', - 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 +var viewer1Leading = false; +var viewer2Leading = false; + +var viewer1Handler = function() { + if (viewer2Leading) { + return; + } + + viewer1Leading = true; + viewer2.viewport.zoomTo(viewer1.viewport.getZoom()); + viewer2.viewport.panTo(viewer1.viewport.getCenter()); + viewer2.viewport.rotateTo(viewer1.viewport.getRotation()); + viewer2.viewport.setFlip(viewer1.viewport.flipped); + viewer1Leading = false; +}; + +var viewer2Handler = function() { + if (viewer1Leading) { + return; + } + + viewer2Leading = true; + viewer1.viewport.zoomTo(viewer2.viewport.getZoom()); + viewer1.viewport.panTo(viewer2.viewport.getCenter()); + viewer1.viewport.rotateTo(viewer2.viewport.getRotation()); + viewer1.viewport.setFlip(viewer1.viewport.flipped); + viewer2Leading = false; +}; + +viewer1.addHandler('zoom', viewer1Handler); +viewer2.addHandler('zoom', viewer2Handler); +viewer1.addHandler('pan', viewer1Handler); +viewer2.addHandler('pan', viewer2Handler); +viewer1.addHandler('rotate', viewer1Handler); +viewer2.addHandler('rotate', viewer2Handler); +viewer1.addHandler('flip', viewer1Handler); +viewer2.addHandler('flip', viewer2Handler); + + +// // Single viewer showing how to use plugin Drawer via configuration +// // Also shows sequence mode +// var viewer3 = window.viewer3 = OpenSeadragon({ +// id: "three-viewer", +// prefixUrl: "../../build/openseadragon/images/", +// minZoomImageRatio:0.01, +// drawer: ThreeJSDrawer, +// tileSources: [sources['leaves'], sources['rainbow'], sources['duomo']], +// sequenceMode: true, +// imageSmoothingEnabled: false, +// crossOriginPolicy: 'Anonymous', +// ajaxWithCredentials: false +// }); + +// // Single viewer showing how to use plugin Drawer via configuration +// // Also shows sequence mode +// var viewer4 = window.viewer4 = 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 +// }); @@ -90,11 +124,18 @@ $('#three-viewer').resizable(true); $('#contentDiv').resizable(true); $('#image-picker').sortable({ update: function(event, ui){ - let thisItem = ui.item.find('.toggle').data('item'); - let items = $('#image-picker input.toggle:checked').toArray().map(item=>$(item).data('item')); + 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){ - viewer.world.setItemIndex(thisItem, newIndex); + viewer1.world.setItemIndex(thisItem, newIndex); + } + + thisItem = ui.item.find('.toggle').data('item2'); + items = $('#image-picker input.toggle:checked').toArray().map(item=>$(item).data('item2')); + newIndex = items.indexOf(thisItem); + if(thisItem){ + viewer2.world.setItemIndex(thisItem, newIndex); } } }); @@ -110,12 +151,13 @@ Object.keys(sources).forEach((key, index)=>{ $('#image-picker input.toggle').on('change',function(){ let data = $(this).data(); if(this.checked){ - addTileSource(data.image, this); - + addTileSource(viewer1, data.image, this); + // addTileSource(viewer2, data.image, this); } else { - if(data.item){ - viewer.world.removeItem(data.item); - $(this).data('item',null); + if(data.item1){ + viewer1.world.removeItem(data.item1); + // viewer2.world.removeItem(data.item2); + $(this).data({item1: null, item2: null}); } } }).trigger('change'); @@ -123,7 +165,13 @@ $('#image-picker input.toggle').on('change',function(){ $('#image-picker input:not(.toggle)').on('change',function(){ let data = $(this).data(); let value = $(this).val(); - let tiledImage = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item'); + let tiledImage1 = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item1'); + let tiledImage2 = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item2'); + updateTiledImage(tiledImage1, data, value, this); + updateTiledImage(tiledImage2, data, value, this); +}); + +function updateTiledImage(tiledImage, data, value, item){ if(tiledImage){ //item = tiledImage let field = data.field; @@ -142,16 +190,16 @@ $('#image-picker input:not(.toggle)').on('change',function(){ } else if (field == 'opacity'){ tiledImage.setOpacity(Number(value)); } else if (field == 'flipped'){ - tiledImage.setFlip($(this).prop('checked')); + tiledImage.setFlip($(item).prop('checked')); } else if (field == 'cropped'){ - if( $(this).prop('checked') ){ + 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( $(this).prop('checked') ){ + if( $(item).prop('checked') ){ let clipRect = new OpenSeadragon.Rect(2000, 0, 3000, 4000); tiledImage.setClip(clipRect); } else { @@ -159,26 +207,40 @@ $('#image-picker input:not(.toggle)').on('change',function(){ } } else if (field == 'debug'){ - if( $(this).prop('checked') ){ + 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 tiledImage = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item'); - if(tiledImage){ - tiledImage.setCompositeOperation(this.value == 'null' ? null : this.value); + 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('item'); + 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; @@ -220,7 +282,7 @@ function getCompositeOperationOptions(){ } -function addTileSource(image, checkbox){ +function addTileSource(viewer, image, checkbox){ let options = $(`#image-picker input[data-image=${image}][type=number]`).toArray().reduce((acc, input)=>{ let field = $(input).data('field'); if(field){ @@ -236,10 +298,11 @@ function addTileSource(image, checkbox){ let tileSource = sources[image]; if(tileSource){ - viewer.addTiledImage({tileSource: tileSource, ...options, index: insertionIndex}); - viewer.world.addOnceHandler('add-item',function(ev){ + viewer&&viewer.addTiledImage({tileSource: tileSource, ...options, index: insertionIndex}); + viewer&&viewer.world.addOnceHandler('add-item',function(ev){ let item = ev.item; - $(checkbox).data('item',item); + let field = viewer === viewer1 ? 'item1' : 'item2'; + $(checkbox).data(field,item); item.source.hasTransparency = ()=>true; //simulate image with transparency, to show seams in default renderer }); } diff --git a/test/modules/ajax-tiles.js b/test/modules/ajax-tiles.js index 39a37876..68e5ab16 100644 --- a/test/modules/ajax-tiles.js +++ b/test/modules/ajax-tiles.js @@ -56,6 +56,9 @@ if (viewer && viewer.close) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; } diff --git a/test/modules/basic.js b/test/modules/basic.js index 49c70365..1315d96d 100644 --- a/test/modules/basic.js +++ b/test/modules/basic.js @@ -19,6 +19,9 @@ if (viewer && viewer.close) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; } @@ -319,8 +322,8 @@ height: 155 } ] } ); - viewer.addOnceHandler('tile-drawn', function() { - assert.ok(OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + viewer.addOnceHandler('tile-drawn', function(event) { + assert.ok(OpenSeadragon.isCanvasTainted(event.tile.getCanvasContext().canvas), "Canvas should be tainted."); done(); }); @@ -339,8 +342,8 @@ height: 155 } ] } ); - viewer.addOnceHandler('tile-drawn', function() { - assert.ok(!OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + viewer.addOnceHandler('tile-drawn', function(event) { + assert.ok(!OpenSeadragon.isCanvasTainted(event.tile.getCanvasContext().canvas), "Canvas should not be tainted."); done(); }); @@ -363,8 +366,8 @@ }, crossOriginPolicy : false } ); - viewer.addOnceHandler('tile-drawn', function() { - assert.ok(OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + viewer.addOnceHandler('tile-drawn', function(event) { + assert.ok(OpenSeadragon.isCanvasTainted(event.tile.getCanvasContext().canvas), "Canvas should be tainted."); done(); }); @@ -387,8 +390,8 @@ crossOriginPolicy : "Anonymous" } } ); - viewer.addOnceHandler('tile-drawn', function() { - assert.ok(!OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + viewer.addOnceHandler('tile-drawn', function(event) { + assert.ok(!OpenSeadragon.isCanvasTainted(event.tile.getCanvasContext().canvas), "Canvas should not be tainted."); done(); }); diff --git a/test/modules/controls.js b/test/modules/controls.js index 7e774d9b..0bf68ea3 100644 --- a/test/modules/controls.js +++ b/test/modules/controls.js @@ -19,6 +19,9 @@ if (viewer && viewer.close) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; } diff --git a/test/modules/drawer.js b/test/modules/drawer.js index d67df286..e4f507a3 100644 --- a/test/modules/drawer.js +++ b/test/modules/drawer.js @@ -13,7 +13,9 @@ if (viewer && viewer.close) { viewer.close(); } - + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; } }); @@ -42,7 +44,8 @@ QUnit.test('rotation', function(assert) { var done = assert.async(); createViewer({ - tileSources: '/test/data/testpattern.dzi' + tileSources: '/test/data/testpattern.dzi', + drawer: 'context2d', // this test only makes sense for certain drawers }); viewer.addHandler('open', function handler(event) { @@ -62,8 +65,8 @@ debugMode: true }); - Util.spyOnce(viewer.drawer, 'drawDebugInfo', function() { - assert.ok(true, 'drawDebugInfo is called'); + Util.spyOnce(viewer.drawer, '_drawDebugInfo', function() { + assert.ok(true, '_drawDebugInfo is called'); done(); }); }); @@ -72,7 +75,8 @@ QUnit.test('sketchCanvas', function(assert) { var done = assert.async(); createViewer({ - tileSources: '/test/data/testpattern.dzi' + tileSources: '/test/data/testpattern.dzi', + drawer: 'context2d' // test only makes sense for this drawer }); var drawer = viewer.drawer; diff --git a/test/modules/events.js b/test/modules/events.js index 010b08b9..698a3ef2 100644 --- a/test/modules/events.js +++ b/test/modules/events.js @@ -20,6 +20,9 @@ if ( viewer && viewer.close ) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; } @@ -1155,6 +1158,7 @@ // ---------- QUnit.test( 'Viewer: event count test with \'tile-drawing\'', function (assert) { var done = assert.async(); + var previousValue = viewer.drawer.continuousTileRefresh; assert.ok(viewer.numberOfHandlers('tile-drawing') === 0, "'tile-drawing' event is empty by default."); @@ -1162,6 +1166,7 @@ viewer.removeHandler( 'tile-drawing', tileDrawing ); assert.ok(viewer.numberOfHandlers('tile-drawing') === 0, "'tile-drawing' deleted: count is 0."); + viewer.drawer.continuousTileRefresh = previousValue; // reset property viewer.close(); done(); }; @@ -1180,11 +1185,14 @@ assert.ok(viewer.numberOfHandlers('tile-drawing') === 1, "'tile-drawing' deleted once: count is 1."); + viewer.drawer.continuousTileRefresh = true; // set to true so the tile-drawing event fires viewer.open( '/test/data/testpattern.dzi' ); } ); QUnit.test( 'Viewer: tile-drawing event', function (assert) { var done = assert.async(); + var previousValue = viewer.drawer.continuousTileRefresh; + var tileDrawing = function ( event ) { viewer.removeHandler( 'tile-drawing', tileDrawing ); assert.ok( event, 'Event handler should be invoked' ); @@ -1194,10 +1202,12 @@ assert.ok(event.tile, "Tile should be set"); assert.ok(event.rendered, "Rendered should be set"); } + viewer.drawer.continuousTileRefresh = previousValue; // reset property viewer.close(); done(); }; + viewer.drawer.continuousTileRefresh = true; // set to true so the tile-drawing event fires viewer.addHandler( 'tile-drawing', tileDrawing ); viewer.open( '/test/data/testpattern.dzi' ); } ); diff --git a/test/modules/formats.js b/test/modules/formats.js index 867bcd17..7a30dd84 100644 --- a/test/modules/formats.js +++ b/test/modules/formats.js @@ -5,15 +5,26 @@ // This module tests whether our various file formats can be opened. // TODO: Add more file formats (with corresponding test data). + var viewer = null; + QUnit.module('Formats', { beforeEach: function () { var example = document.createElement("div"); example.id = "example"; document.getElementById("qunit-fixture").appendChild(example); + }, + afterEach: function () { + if ( viewer && viewer.close ) { + viewer.close(); + } + if (viewer && viewer.destroy){ + viewer.destroy(); + } + + viewer = null; } }); - var viewer = null; // ---------- var testOpenUrl = function(relativeUrl, assert) { diff --git a/test/modules/imageloader.js b/test/modules/imageloader.js index 1e201b10..12842dfd 100644 --- a/test/modules/imageloader.js +++ b/test/modules/imageloader.js @@ -18,6 +18,9 @@ if (viewer && viewer.close) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; } diff --git a/test/modules/multi-image.js b/test/modules/multi-image.js index bc6c6908..9b22b8cf 100644 --- a/test/modules/multi-image.js +++ b/test/modules/multi-image.js @@ -19,6 +19,9 @@ if ( viewer && viewer.close ) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; $("#example").remove(); diff --git a/test/modules/navigator.js b/test/modules/navigator.js index 0859abb9..1209b3f8 100644 --- a/test/modules/navigator.js +++ b/test/modules/navigator.js @@ -41,6 +41,15 @@ } resetTestVariables(); + + if ( viewer && viewer.close ) { + viewer.close(); + } + if (viewer && viewer.destroy){ + viewer.destroy(); + } + + viewer = null; } }); diff --git a/test/modules/overlays.js b/test/modules/overlays.js index 3a7a8877..bafc0d35 100644 --- a/test/modules/overlays.js +++ b/test/modules/overlays.js @@ -16,6 +16,14 @@ }, afterEach: function() { resetTestVariables(); + if ( viewer && viewer.close ) { + viewer.close(); + } + if (viewer && viewer.destroy){ + viewer.destroy(); + } + + viewer = null; } }); diff --git a/test/modules/referencestrip.js b/test/modules/referencestrip.js index 2514dbd2..25ae1b9b 100644 --- a/test/modules/referencestrip.js +++ b/test/modules/referencestrip.js @@ -13,6 +13,9 @@ if (viewer && viewer.close) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; } diff --git a/test/modules/tiledimage.js b/test/modules/tiledimage.js index a580e188..6c7cef56 100644 --- a/test/modules/tiledimage.js +++ b/test/modules/tiledimage.js @@ -20,6 +20,9 @@ if (viewer && viewer.close) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; } @@ -132,6 +135,8 @@ QUnit.test('update', function(assert) { var done = assert.async(); var handlerCount = 0; + var testTileDrawingEvent = viewer.drawerOptions.type === 'context2d'; + let expectedHandlers = testTileDrawingEvent ? 4 : 3; viewer.addHandler('open', function(event) { var image = viewer.world.getItemAt(0); @@ -160,15 +165,18 @@ assert.ok(event.tile, 'update-tile event includes tile'); }); - viewer.addHandler('tile-drawing', function tileDrawingHandler(event) { - viewer.removeHandler('tile-drawing', tileDrawingHandler); - handlerCount++; - assert.equal(event.eventSource, viewer, 'sender of tile-drawing event was viewer'); - assert.equal(event.tiledImage, image, 'tiledImage of update-level event is correct'); - assert.ok(event.tile, 'tile-drawing event includes a tile'); - assert.ok(event.context, 'tile-drawing event includes a context'); - assert.ok(event.rendered, 'tile-drawing event includes a rendered'); - }); + if(testTileDrawingEvent){ + viewer.addHandler('tile-drawing', function tileDrawingHandler(event) { + viewer.removeHandler('tile-drawing', tileDrawingHandler); + handlerCount++; + assert.equal(event.eventSource, viewer, 'sender of tile-drawing event was viewer'); + assert.equal(event.tiledImage, image, 'tiledImage of update-level event is correct'); + assert.ok(event.tile, 'tile-drawing event includes a tile'); + assert.ok(event.context, 'tile-drawing event includes a context'); + assert.ok(event.rendered, 'tile-drawing event includes a rendered'); + }); + } + viewer.addHandler('tile-drawn', function tileDrawnHandler(event) { viewer.removeHandler('tile-drawn', tileDrawnHandler); @@ -177,11 +185,10 @@ assert.equal(event.tiledImage, image, 'tiledImage of update-level event is correct'); assert.ok(event.tile, 'tile-drawn event includes tile'); - assert.equal(handlerCount, 4, 'correct number of handlers called'); + assert.equal(handlerCount, expectedHandlers, 'correct number of handlers called'); done(); }); - //image.draw(); // TO DO: Is this necessary for the test? It will now fail since tiledImage.draw() is not a thing. viewer.drawer.draw( [ image ] ); }); diff --git a/test/modules/tilesource-dynamic-url.js b/test/modules/tilesource-dynamic-url.js index d7678805..f07fedc0 100644 --- a/test/modules/tilesource-dynamic-url.js +++ b/test/modules/tilesource-dynamic-url.js @@ -132,6 +132,10 @@ if (viewer && viewer.close) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } + viewer = null; OpenSeadragon.makeAjaxRequest = OriginalAjax; diff --git a/test/modules/units.js b/test/modules/units.js index ee47ebe1..160cea35 100644 --- a/test/modules/units.js +++ b/test/modules/units.js @@ -22,6 +22,9 @@ if (viewer && viewer.close) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; } diff --git a/test/modules/viewport.js b/test/modules/viewport.js index 6362a296..c805945e 100644 --- a/test/modules/viewport.js +++ b/test/modules/viewport.js @@ -24,6 +24,9 @@ if (viewer && viewer.close) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; } diff --git a/test/modules/world.js b/test/modules/world.js index 9c705345..29830f7f 100644 --- a/test/modules/world.js +++ b/test/modules/world.js @@ -19,6 +19,9 @@ if (viewer && viewer.close) { viewer.close(); } + if (viewer && viewer.destroy){ + viewer.destroy(); + } viewer = null; }