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 @@