implement native webgl renderer, and many associated changes related to drawing pipeline and testing

This commit is contained in:
Tom 2023-06-26 21:29:08 -04:00
parent 128975ea0f
commit 386ca85db8
32 changed files with 1423 additions and 266 deletions

View File

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

View File

@ -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 <code>rendered</code> 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 <code>rendered</code> 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;

View File

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

View File

@ -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 ){
/**

View File

@ -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
});
}
}
},

View File

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

View File

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

View File

@ -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());
},
/**

View File

@ -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
});
}
};

View File

@ -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 &lt;canvas&gt;.
*
* @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),

View File

@ -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();

872
src/webgldrawer.js Normal file
View File

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

View File

@ -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;
});
},
/**

View File

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

View File

@ -68,7 +68,28 @@
</head>
<body>
<div class="content">
<h2>Use a WebGL drawer implementation (using three.js) instead of the default context2d drawer</h2>
<h2>Compare behavior of <strong>Context2d</strong> and <strong>WebGL</strong> (via three.js) drawers</h2>
<div class="mirrored">
<div>
<h3>Context2d drawer (default in OSD &lt;= 4.1.0)</h3>
<div id="context2d" class="viewer-container"></div>
</div>
<div>
<h3>New WebGL drawer</h3>
<div id="webgl" class="viewer-container"></div>
</div>
</div>
<div id="image-picker">
<h3>Image options (drag and drop to re-order images)</h3>
</div>
<h2>Use a custom plugin drawer - example using three.js</h2>
<div class="mirrored">
<div>
<div class="description">
@ -97,74 +118,6 @@
</div>
<h2>Compare behavior of <strong>Context2d</strong> and <strong>WebGL</strong> (via three.js) drawers</h2>
<div class="mirrored">
<div>
<h3>Use default OpenSeadragon viewer to pan/zoom</h3>
<div id="contentDiv" class="viewer-container"></div>
</div>
<div>
<h3>WebGL drawer linked using event listeners </h3>
<div id="three-canvas-container" class="viewer-container"></div>
</div>
</div>
<div id="image-picker">
<h3>Image options (drag and drop to re-order images)</h3>
<!-- <div class="image-options">
<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>
<label><input type="checkbox" checked data-image="rainbow" class="toggle"> Rainbow Grid</label>
<div class="option-grid">
<label>X: <input type="number" value="0" data-image="rainbow" data-field="x"> </label>
<label>Y: <input type="number" value="0" data-image="rainbow" data-field="y"> </label>
<label>Width: <input type="number" value="1" data-image="rainbow" data-field="width" min="0"> </label>
<label>Degrees: <input type="number" value="0" data-image="rainbow" data-field="degrees"> </label>
<label>Opacity: <input type="number" value="1" data-image="rainbow" data-field="opacity" min="0" max="1" step="0.2"> </label>
<label>Flipped: <input type="checkbox" data-image="rainbow" data-field="flipped"></label>
<label>Cropped: <input type="checkbox" data-image="rainbow" data-field="cropped"></label>
<label>Debug: <input type="checkbox" data-image="rainbow" data-field="debug"></label>
<label>Composite: <select data-image="rainbow" data-field="composite"></select></label>
<label>Wrapping: <select data-image="rainbow" data-field="wrapping"></select></label>
</div>
</div>
<div class="image-options">
<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>
<label><input type="checkbox" data-image="leaves" class="toggle"> Leaves</label>
<div class="option-grid">
<label>X: <input type="number" value="0" data-image="leaves" data-field="x"> </label>
<label>Y: <input type="number" value="0" data-image="leaves" data-field="y"> </label>
<label>Width: <input type="number" value="1" data-image="leaves" data-field="width" min="0"> </label>
<label>Degrees: <input type="number" value="0" data-image="leaves" data-field="degrees"> </label>
<label>Opacity: <input type="number" value="1" data-image="leaves" data-field="opacity" min="0" max="1" step="0.2"> </label>
<label>Flipped: <input type="checkbox" data-image="leaves" data-field="flipped"></label>
<label>Cropped: <input type="checkbox" data-image="leaves" data-field="cropped"></label>
<label>Debug: <input type="checkbox" data-image="leaves" data-field="debug"></label>
<label>Composite: <select data-image="leaves" data-field="composite" ></select></label>
<label>Wrapping: <select data-image="leaves" data-field="wrapping"></select></label>
</div>
</div>
<div class="image-options">
<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>
<label><input type="checkbox" data-image="bblue" class="toggle"> BBlue PNG</label>
<div class="option-grid">
<label>X: <input type="number" value="0" data-image="bblue" data-field="x"> </label>
<label>Y: <input type="number" value="0" data-image="bblue" data-field="y"> </label>
<label>Width: <input type="number" value="1" data-image="bblue" data-field="width" min="0"> </label>
<label>Degrees: <input type="number" value="0" data-image="bblue" data-field="degrees"> </label>
<label>Opacity: <input type="number" value="1" data-image="bblue" data-field="opacity" min="0" max="1" step="0.2"> </label>
<label>Flipped: <input type="checkbox" data-image="bblue" data-field="flipped"></label>
<label>Cropped: <input type="checkbox" data-image="bblue" data-field="cropped"></label>
<label>Debug: <input type="checkbox" data-image="bblue" data-field="debug"></label>
<label>Composite: <select data-image="bblue" data-field="composite"></select></label>
<label>Wrapping: <select data-image="bblue" data-field="wrapping"></select></label>
</div>
</div> -->
</div>
<h2>HTMLDrawer: legacy pre-HTML5 drawer that uses &lt;img&gt; elements for tiles</h2>
<div class="mirrored">

View File

@ -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
});
}

View File

@ -56,6 +56,9 @@
if (viewer && viewer.close) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
}

View File

@ -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();
});

View File

@ -19,6 +19,9 @@
if (viewer && viewer.close) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
}

View File

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

View File

@ -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' );
} );

View File

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

View File

@ -18,6 +18,9 @@
if (viewer && viewer.close) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
}

View File

@ -19,6 +19,9 @@
if ( viewer && viewer.close ) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
$("#example").remove();

View File

@ -41,6 +41,15 @@
}
resetTestVariables();
if ( viewer && viewer.close ) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
}
});

View File

@ -16,6 +16,14 @@
},
afterEach: function() {
resetTestVariables();
if ( viewer && viewer.close ) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
}
});

View File

@ -13,6 +13,9 @@
if (viewer && viewer.close) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
}

View File

@ -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 ] );
});

View File

@ -132,6 +132,10 @@
if (viewer && viewer.close) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
OpenSeadragon.makeAjaxRequest = OriginalAjax;

View File

@ -22,6 +22,9 @@
if (viewer && viewer.close) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
}

View File

@ -24,6 +24,9 @@
if (viewer && viewer.close) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
}

View File

@ -19,6 +19,9 @@
if (viewer && viewer.close) {
viewer.close();
}
if (viewer && viewer.destroy){
viewer.destroy();
}
viewer = null;
}