diff --git a/examples/deepzoom/static/jquery.js b/examples/deepzoom/static/jquery.js index 15a1a291..f122b10d 100644 --- a/examples/deepzoom/static/jquery.js +++ b/examples/deepzoom/static/jquery.js @@ -1,12 +1,12 @@ /*! - * jQuery JavaScript Library v3.7.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween + * jQuery JavaScript Library v3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween * https://jquery.com/ * * Copyright OpenJS Foundation and other contributors * Released under the MIT license * https://jquery.org/license * - * Date: 2023-05-11T18:29Z + * Date: 2023-08-28T13:37Z */ ( function( global, factory ) { @@ -147,7 +147,7 @@ function toType( obj ) { -var version = "3.7.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween", +var version = "3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween", rhtmlSuffix = /HTML$/i, @@ -411,9 +411,14 @@ jQuery.extend( { // Do not traverse comment nodes ret += jQuery.text( node ); } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + } + if ( nodeType === 1 || nodeType === 11 ) { return elem.textContent; - } else if ( nodeType === 3 || nodeType === 4 ) { + } + if ( nodeType === 9 ) { + return elem.documentElement.textContent; + } + if ( nodeType === 3 || nodeType === 4 ) { return elem.nodeValue; } @@ -1126,12 +1131,17 @@ function setDocument( node ) { documentElement.msMatchesSelector; // Support: IE 9 - 11+, Edge 12 - 18+ - // Accessing iframe documents after unload throws "permission denied" errors (see trac-13936) - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( preferredDoc != document && + // Accessing iframe documents after unload throws "permission denied" errors + // (see trac-13936). + // Limit the fix to IE & Edge Legacy; despite Edge 15+ implementing `matches`, + // all IE 9+ and Edge Legacy versions implement `msMatchesSelector` as well. + if ( documentElement.msMatchesSelector && + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + preferredDoc != document && ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { // Support: IE 9 - 11+, Edge 12 - 18+ @@ -2694,12 +2704,12 @@ jQuery.find = find; jQuery.expr[ ":" ] = jQuery.expr.pseudos; jQuery.unique = jQuery.uniqueSort; -// These have always been private, but they used to be documented -// as part of Sizzle so let's maintain them in the 3.x line -// for backwards compatibility purposes. +// These have always been private, but they used to be documented as part of +// Sizzle so let's maintain them for now for backwards compatibility purposes. find.compile = compile; find.select = select; find.setDocument = setDocument; +find.tokenize = tokenize; find.escape = jQuery.escapeSelector; find.getText = jQuery.text; @@ -5913,7 +5923,7 @@ function domManip( collection, args, callback, ignored ) { if ( hasScripts ) { doc = scripts[ scripts.length - 1 ].ownerDocument; - // Reenable scripts + // Re-enable scripts jQuery.map( scripts, restoreScript ); // Evaluate executable scripts on first document insertion @@ -6370,7 +6380,7 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); trChild = document.createElement( "div" ); table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; - tr.style.cssText = "border:1px solid"; + tr.style.cssText = "box-sizing:content-box;border:1px solid"; // Support: Chrome 86+ // Height set through cssText does not get applied. @@ -6382,7 +6392,7 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); // In our bodyBackground.html iframe, // display for all div elements is set to "inline", // which causes a problem only in Android 8 Chrome 86. - // Ensuring the div is display: block + // Ensuring the div is `display: block` // gets around this issue. trChild.style.display = "block"; @@ -8451,7 +8461,9 @@ jQuery.fn.extend( { }, hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + return this + .on( "mouseenter", fnOver ) + .on( "mouseleave", fnOut || fnOver ); } } ); diff --git a/examples/deepzoom/static/openseadragon.js b/examples/deepzoom/static/openseadragon.js index cd41170d..dec64da0 100644 --- a/examples/deepzoom/static/openseadragon.js +++ b/examples/deepzoom/static/openseadragon.js @@ -1,6 +1,6 @@ -//! openseadragon 4.1.0 -//! Built on 2023-05-25 -//! Git commit: v4.1.0-0-8849681 +//! openseadragon 5.0.0 +//! Built on 2024-08-14 +//! Git commit: v5.0.0-0-f28b7fc1 //! http://openseadragon.github.io //! License: http://openseadragon.github.io/license/ @@ -8,7 +8,7 @@ * OpenSeadragon * * Copyright (C) 2009 CodePlex Foundation - * Copyright (C) 2010-2022 OpenSeadragon contributors + * Copyright (C) 2010-2024 OpenSeadragon contributors * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -66,7 +66,7 @@ /* * Portions of this source file taken from mattsnider.com: * - * Copyright (c) 2006-2022 Matt Snider + * Copyright (c) 2006-2013 Matt Snider * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -90,7 +90,7 @@ /** * @namespace OpenSeadragon - * @version openseadragon 4.1.0 + * @version openseadragon 5.0.0 * @classdesc The root namespace for OpenSeadragon. All utility methods * and classes are defined on or below this namespace. * @@ -196,6 +196,16 @@ * 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 = ['webgl', 'canvas', 'html']] + * Which drawer to use. Valid strings are 'webgl', 'canvas', 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. For details + * please see {@link OpenSeadragon.DrawerOptions}. + * * @property {Number} [opacity=1] * Default proportional opacity of the tiled images (1=opaque, 0=hidden) * Hidden images do not draw and only load when preloading is allowed. @@ -210,9 +220,9 @@ * For complete list of modes, please @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation/ globalCompositeOperation} * * @property {Boolean} [imageSmoothingEnabled=true] - * Image smoothing for canvas rendering (only if canvas is used). Note: Ignored + * Image smoothing for canvas rendering (only if the canvas drawer is used). Note: Ignored * by some (especially older) browsers which do not support this canvas property. - * This property can be changed in {@link Viewer.Drawer.setImageSmoothingEnabled}. + * This property can be changed in {@link Viewer.DrawerBase.setImageSmoothingEnabled}. * * @property {String|CanvasGradient|CanvasPattern|Function} [placeholderFillStyle=null] * Draws a colored rectangle behind the tile if it is not loaded yet. @@ -236,6 +246,11 @@ * @property {Boolean} [flipped=false] * Initial flip state. * + * @property {Boolean} [overlayPreserveContentDirection=true] + * When the viewport is flipped (by pressing 'f'), the overlay is flipped using ScaleX. + * Normally, this setting (default true) keeps the overlay's content readable by flipping it back. + * To make the content flip with the overlay, set overlayPreserveContentDirection to false. + * * @property {Number} [minZoomLevel=null] * * @property {Number} [maxZoomLevel=null] @@ -296,6 +311,12 @@ * @property {Number} [rotationIncrement=90] * The number of degrees to rotate right or left when the rotate buttons or keyboard shortcuts are activated. * + * @property {Number} [maxTilesPerFrame=1] + * The number of tiles loaded per frame. As the frame rate of the client's machine is usually high (e.g., 50 fps), + * one tile per frame should be a good choice. However, for large screens or lower frame rates, the number of + * loaded tiles per frame can be adjusted here. Reasonable values might be 2 or 3 tiles per frame. + * (Note that the actual frame rate is given by the client's browser and machine). + * * @property {Number} [pixelsPerWheelLine=40] * For pixel-resolution scrolling devices, the number of pixels equal to one scroll line. * @@ -508,7 +529,7 @@ * Milliseconds to wait after each tile retry if tileRetryMax is set. * * @property {Boolean} [useCanvas=true] - * Set to false to not use an HTML canvas element for image rendering even if canvas is supported. + * Deprecated. Use the `drawer` option to specify preferred renderer. * * @property {Number} [minPixelRatio=0.5] * The higher the minPixelRatio, the lower the quality of the image that @@ -744,6 +765,16 @@ * */ + /** + * @typedef {Object} DrawerOptions + * @memberof OpenSeadragon + * @property {Object} webgl - options if the WebGLDrawer is used. No options are currently supported. + * @property {Object} canvas - options if the CanvasDrawer is used. No options are currently supported. + * @property {Object} html - options if the HTMLDrawer is used. No options are currently supported. + * @property {Object} custom - options if a custom drawer is used. No options are currently supported. + */ + + /** * The names for the image resources used for the image navigation buttons. * @@ -825,9 +856,9 @@ function OpenSeadragon( options ){ * @since 1.0.0 */ $.version = { - versionStr: '4.1.0', - major: parseInt('4', 10), - minor: parseInt('1', 10), + versionStr: '5.0.0', + major: parseInt('5', 10), + minor: parseInt('0', 10), revision: parseInt('0', 10) }; @@ -1044,8 +1075,9 @@ function OpenSeadragon( options ){ /** * A ratio comparing the device screen's pixel density to the canvas's backing store pixel density, * clamped to a minimum of 1. Defaults to 1 if canvas isn't supported by the browser. - * @member {Number} pixelDensityRatio + * @function getCurrentPixelDensityRatio * @memberof OpenSeadragon + * @returns {Number} */ $.getCurrentPixelDensityRatio = function() { if ( $.supportsCanvas ) { @@ -1063,6 +1095,8 @@ function OpenSeadragon( options ){ }; /** + * A ratio comparing the device screen's pixel density to the canvas's backing store pixel density, + * clamped to a minimum of 1. Defaults to 1 if canvas isn't supported by the browser. * @member {Number} pixelDensityRatio * @memberof OpenSeadragon */ @@ -1294,6 +1328,7 @@ function OpenSeadragon( options ){ preserveImageSizeOnResize: false, // requires autoResize=true minScrollDeltaTime: 50, rotationIncrement: 90, + maxTilesPerFrame: 1, //DEFAULT CONTROL SETTINGS showSequenceControl: true, //SEQUENCE @@ -1335,15 +1370,36 @@ function OpenSeadragon( options ){ degrees: 0, // INITIAL FLIP STATE - flipped: false, + flipped: false, + overlayPreserveContentDirection: true, // APPEARANCE - opacity: 1, - preload: false, - compositeOperation: null, - imageSmoothingEnabled: true, - placeholderFillStyle: null, - subPixelRoundingForTransparency: null, + opacity: 1, // to be passed into each TiledImage + compositeOperation: null, // to be passed into each TiledImage + + // DRAWER SETTINGS + drawer: ['webgl', 'canvas', 'html'], // prefer using webgl, then canvas (i.e. context2d), then fallback to html + + drawerOptions: { + webgl: { + + }, + canvas: { + + }, + html: { + + }, + custom: { + + } + }, + + // TILED IMAGE SETTINGS + preload: false, // to be passed into each TiledImage + imageSmoothingEnabled: true, // to be passed into each TiledImage + placeholderFillStyle: null, // to be passed into each TiledImage + subPixelRoundingForTransparency: null, // to be passed into each TiledImage //REFERENCE STRIP SETTINGS showReferenceStrip: false, @@ -1366,7 +1422,6 @@ function OpenSeadragon( options ){ imageLoaderLimit: 0, maxImageCacheCount: 200, timeout: 30000, - useCanvas: true, // Use canvas element for drawing if available tileRetryMax: 0, tileRetryDelay: 2500, @@ -1436,16 +1491,6 @@ function OpenSeadragon( options ){ }, - - /** - * TODO: get rid of this. I can't see how it's required at all. Looks - * like an early legacy code artifact. - * @static - * @ignore - */ - SIGNAL: "----seadragon----", - - /** * Returns a function which invokes the method as if it were a method belonging to the object. * @function @@ -2257,25 +2302,12 @@ function OpenSeadragon( options ){ event.stopPropagation(); }, - - /** - * Similar to OpenSeadragon.delegate, but it does not immediately call - * the method on the object, returning a function which can be called - * repeatedly to delegate the method. It also allows additional arguments - * to be passed during construction which will be added during each - * invocation, and each invocation can add additional arguments as well. - * - * @function - * @param {Object} object - * @param {Function} method - * @param [args] any additional arguments are passed as arguments to the - * created callback - * @returns {Function} - */ + // Deprecated createCallback: function( object, method ) { //TODO: This pattern is painful to use and debug. It's much cleaner // to use pinning plus anonymous functions. Get rid of this // pattern! + console.error('The createCallback function is deprecated and will be removed in future versions. Please use alternativeFunction instead.'); var initialArgs = [], i; for ( i = 2; i < arguments.length; i++ ) { @@ -2326,43 +2358,18 @@ function OpenSeadragon( options ){ /** * Create an XHR object * @private - * @param {type} [local] If set to true, the XHR will be file: protocol - * compatible if possible (but may raise a warning in the browser). + * @param {type} [local] Deprecated. Ignored (IE/ActiveXObject file protocol no longer supported). * @returns {XMLHttpRequest} */ - createAjaxRequest: function( local ) { - // IE11 does not support window.ActiveXObject so we just try to - // create one to see if it is supported. - // See: http://msdn.microsoft.com/en-us/library/ie/dn423948%28v=vs.85%29.aspx - var supportActiveX; - try { - /* global ActiveXObject:true */ - supportActiveX = !!new ActiveXObject( "Microsoft.XMLHTTP" ); - } catch( e ) { - supportActiveX = false; - } - - if ( supportActiveX ) { - if ( window.XMLHttpRequest ) { - $.createAjaxRequest = function( local ) { - if ( local ) { - return new ActiveXObject( "Microsoft.XMLHTTP" ); - } - return new XMLHttpRequest(); - }; - } else { - $.createAjaxRequest = function() { - return new ActiveXObject( "Microsoft.XMLHTTP" ); - }; - } - } else if ( window.XMLHttpRequest ) { + createAjaxRequest: function() { + if ( window.XMLHttpRequest ) { $.createAjaxRequest = function() { return new XMLHttpRequest(); }; + return new XMLHttpRequest(); } else { throw new Error( "Browser doesn't support XMLHttpRequest." ); } - return $.createAjaxRequest( local ); }, /** @@ -2398,7 +2405,7 @@ function OpenSeadragon( options ){ } var protocol = $.getUrlProtocol( url ); - var request = $.createAjaxRequest( protocol === "file:" ); + var request = $.createAjaxRequest(); if ( !$.isFunction( onSuccess ) ) { throw new Error( "makeAjaxRequest requires a success callback" ); @@ -2567,17 +2574,6 @@ function OpenSeadragon( options ){ return xmlDoc; }; - } else if ( window.ActiveXObject ) { - - $.parseXml = function( string ) { - var xmlDoc = null; - - xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" ); - xmlDoc.async = false; - xmlDoc.loadXML( string ); - return xmlDoc; - }; - } else { throw new Error( "Browser doesn't support XML DOM." ); } @@ -2614,18 +2610,20 @@ function OpenSeadragon( options ){ * Preexisting formats that are not being updated are left unchanged. * By default, the defined formats are *
{
+ * avif: true,
* bmp: false,
* jpeg: true,
* jpg: true,
* png: true,
* tif: false,
- * wdp: false
+ * wdp: false,
+ * webp: true
* }
*
* @function
* @example
- * // sets webp as supported and png as unsupported
- * setImageFormatsSupported({webp: true, png: false});
+ * // sets bmp as supported and png as unsupported
+ * setImageFormatsSupported({bmp: true, png: false});
* @param {Object} formats An object containing format extensions as
* keys and booleans as values.
*/
@@ -2680,12 +2678,14 @@ function OpenSeadragon( options ){
var FILEFORMATS = {
+ avif: true,
bmp: false,
jpeg: true,
jpg: true,
png: true,
tif: false,
- wdp: false
+ wdp: false,
+ webp: true
},
URLPARAMS = {};
@@ -2700,6 +2700,10 @@ function OpenSeadragon( options ){
//console.error( 'appVersion: ' + navigator.appVersion );
//console.error( 'userAgent: ' + navigator.userAgent );
+ //TODO navigator.appName is deprecated. Should be 'Netscape' for all browsers
+ // but could be dropped at any time
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/appName
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
switch( navigator.appName ){
case "Microsoft Internet Explorer":
if( !!window.attachEvent &&
@@ -2785,8 +2789,8 @@ function OpenSeadragon( options ){
//determine if this browser supports element.style.opacity
$.Browser.opacity = true;
- if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 11 ) {
- $.console.error('Internet Explorer versions < 11 are not supported by OpenSeadragon');
+ if ( $.Browser.vendor === $.BROWSERS.IE ) {
+ $.console.error('Internet Explorer is not supported by OpenSeadragon');
}
})();
@@ -2912,11 +2916,221 @@ function OpenSeadragon( options ){
return OpenSeadragon;
}));
+/*
+ * OpenSeadragon - Mat3
+ *
+ * Copyright (C) 2010-2024 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.
+ *
+ */
+
+
+/*
+ * Portions of this source file are taken from WegGL Fundamentals:
+ *
+ * Copyright 2012, Gregg Tavares.
+ * All rights reserved.
+ *
+ * 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 Gregg Tavares. nor the names of his
+ * 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( $ ){
+
+// Modified from https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
+
+/**
+ *
+ *
+ * @class Mat3
+ * @classdesc A left-to-right matrix representation, useful for affine transforms for
+ * positioning tiles for drawing
+ *
+ * @memberof OpenSeadragon
+ *
+ * @param {Array} [values] - Initial values for the matrix
+ *
+ **/
+class Mat3{
+ constructor(values){
+ if(!values) {
+ values = [
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0
+ ];
+ }
+ this.values = values;
+ }
+
+ /**
+ * @function makeIdentity
+ * @memberof OpenSeadragon.Mat3
+ * @static
+ * @returns {OpenSeadragon.Mat3} an identity matrix
+ */
+ static makeIdentity(){
+ return new Mat3([
+ 1, 0, 0,
+ 0, 1, 0,
+ 0, 0, 1
+ ]);
+ }
+
+ /**
+ * @function makeTranslation
+ * @memberof OpenSeadragon.Mat3
+ * @static
+ * @param {Number} tx The x value of the translation
+ * @param {Number} ty The y value of the translation
+ * @returns {OpenSeadragon.Mat3} A translation matrix
+ */
+ static makeTranslation(tx, ty) {
+ return new Mat3([
+ 1, 0, 0,
+ 0, 1, 0,
+ tx, ty, 1,
+ ]);
+ }
+
+ /**
+ * @function makeRotation
+ * @memberof OpenSeadragon.Mat3
+ * @static
+ * @param {Number} angleInRadians The desired rotation angle, in radians
+ * @returns {OpenSeadragon.Mat3} A rotation matrix
+ */
+ 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,
+ ]);
+ }
+
+ /**
+ * @function makeScaling
+ * @memberof OpenSeadragon.Mat3
+ * @static
+ * @param {Number} sx The x value of the scaling
+ * @param {Number} sy The y value of the scaling
+ * @returns {OpenSeadragon.Mat3} A scaling matrix
+ */
+ static makeScaling(sx, sy) {
+ return new Mat3([
+ sx, 0, 0,
+ 0, sy, 0,
+ 0, 0, 1,
+ ]);
+ }
+
+ /**
+ * @alias multiply
+ * @memberof! OpenSeadragon.Mat3
+ * @param {OpenSeadragon.Mat3} other the matrix to multiply with
+ * @returns {OpenSeadragon.Mat3} The result of matrix multiplication
+ */
+ 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,
+ ]);
+ }
+}
+
+
+$.Mat3 = Mat3;
+
+}( OpenSeadragon ));
+
/*
* OpenSeadragon - full-screen support functions
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -2981,10 +3195,14 @@ function OpenSeadragon( options ){
return document.fullscreenElement;
};
fullScreenApi.requestFullScreen = function( element ) {
- return element.requestFullscreen();
+ return element.requestFullscreen().catch(function (msg) {
+ $.console.error('Fullscreen request failed: ', msg);
+ });
};
fullScreenApi.exitFullScreen = function() {
- document.exitFullscreen();
+ document.exitFullscreen().catch(function (msg) {
+ $.console.error('Error while exiting fullscreen: ', msg);
+ });
};
fullScreenApi.fullScreenEventName = "fullscreenchange";
fullScreenApi.fullScreenErrorEventName = "fullscreenerror";
@@ -3062,7 +3280,7 @@ function OpenSeadragon( options ){
* OpenSeadragon - EventSource
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -3111,6 +3329,7 @@ function OpenSeadragon( options ){
*/
$.EventSource = function() {
this.events = {};
+ this._rejectedEventList = {};
};
/** @lends OpenSeadragon.EventSource.prototype */
@@ -3128,6 +3347,7 @@ $.EventSource.prototype = {
* @param {Number} [times=1] - The number of times to handle the event
* before removing it.
* @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
+ * @returns {Boolean} - True if the handler was added, false if it was rejected
*/
addOnceHandler: function(eventName, handler, userData, times, priority) {
var self = this;
@@ -3140,7 +3360,7 @@ $.EventSource.prototype = {
}
return handler(event);
};
- this.addHandler(eventName, onceHandler, userData, priority);
+ return this.addHandler(eventName, onceHandler, userData, priority);
},
/**
@@ -3150,8 +3370,15 @@ $.EventSource.prototype = {
* @param {OpenSeadragon.EventHandler} handler - Function to call when event is triggered.
* @param {Object} [userData=null] - Arbitrary object to be passed unchanged to the handler.
* @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
+ * @returns {Boolean} - True if the handler was added, false if it was rejected
*/
addHandler: function ( eventName, handler, userData, priority ) {
+
+ if(Object.prototype.hasOwnProperty.call(this._rejectedEventList, eventName)){
+ $.console.error(`Error adding handler for ${eventName}. ${this._rejectedEventList[eventName]}`);
+ return false;
+ }
+
var events = this.events[ eventName ];
if ( !events ) {
this.events[ eventName ] = events = [];
@@ -3166,6 +3393,7 @@ $.EventSource.prototype = {
index--;
}
}
+ return true;
},
/**
@@ -3251,17 +3479,45 @@ $.EventSource.prototype = {
* @function
* @param {String} eventName - Name of event to register.
* @param {Object} eventArgs - Event-specific data.
+ * @returns {Boolean} True if the event was fired, false if it was rejected because of rejectEventHandler(eventName)
*/
raiseEvent: function( eventName, eventArgs ) {
//uncomment if you want to get a log of all events
//$.console.log( eventName );
+ if(Object.prototype.hasOwnProperty.call(this._rejectedEventList, eventName)){
+ $.console.error(`Error adding handler for ${eventName}. ${this._rejectedEventList[eventName]}`);
+ return false;
+ }
+
var handler = this.getHandler( eventName );
if ( handler ) {
- return handler( this, eventArgs || {} );
+ handler( this, eventArgs || {} );
}
- return undefined;
+ return true;
+ },
+
+ /**
+ * Set an event name as being disabled, and provide an optional error message
+ * to be printed to the console
+ * @param {String} eventName - Name of the event
+ * @param {String} [errorMessage] - Optional string to print to the console
+ * @private
+ */
+ rejectEventHandler(eventName, errorMessage = ''){
+ this._rejectedEventList[eventName] = errorMessage;
+ },
+
+ /**
+ * Explicitly allow an event handler to be added for this event type, undoing
+ * the effects of rejectEventHandler
+ * @param {String} eventName - Name of the event
+ * @private
+ */
+ allowEventHandler(eventName){
+ delete this._rejectedEventList[eventName];
}
+
};
}( OpenSeadragon ));
@@ -3270,7 +3526,7 @@ $.EventSource.prototype = {
* OpenSeadragon - MouseTracker
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -3325,16 +3581,16 @@ $.EventSource.prototype = {
* @param {Boolean} [options.startDisabled=false]
* If true, event tracking on the element will not start until
* {@link OpenSeadragon.MouseTracker.setTracking|setTracking} is called.
- * @param {Number} options.clickTimeThreshold
+ * @param {Number} [options.clickTimeThreshold=300]
* The number of milliseconds within which a pointer down-up event combination
* will be treated as a click gesture.
- * @param {Number} options.clickDistThreshold
+ * @param {Number} [options.clickDistThreshold=5]
* The maximum distance allowed between a pointer down event and a pointer up event
* to be treated as a click gesture.
- * @param {Number} options.dblClickTimeThreshold
+ * @param {Number} [options.dblClickTimeThreshold=300]
* The number of milliseconds within which two pointer down-up event combinations
* will be treated as a double-click gesture.
- * @param {Number} options.dblClickDistThreshold
+ * @param {Number} [options.dblClickDistThreshold=20]
* The maximum distance allowed between two pointer click events
* to be treated as a click gesture.
* @param {Number} [options.stopDelay=50]
@@ -3625,7 +3881,7 @@ $.EventSource.prototype = {
getActivePointersListByType: function ( type ) {
var delegate = THIS[ this.hash ],
i,
- len = delegate.activePointersLists.length,
+ len = delegate ? delegate.activePointersLists.length : 0,
list;
for ( i = 0; i < len; i++ ) {
@@ -3635,7 +3891,9 @@ $.EventSource.prototype = {
}
list = new $.MouseTracker.GesturePointList( type );
- delegate.activePointersLists.push( list );
+ if(delegate){
+ delegate.activePointersLists.push( list );
+ }
return list;
},
@@ -4382,10 +4640,9 @@ $.EventSource.prototype = {
/**
* Detect available mouse wheel event name.
*/
- $.MouseTracker.wheelEventName = ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version > 8 ) ||
- ( 'onwheel' in document.createElement( 'div' ) ) ? 'wheel' : // Modern browsers support 'wheel'
- document.onmousewheel !== undefined ? 'mousewheel' : // Webkit and IE support at least 'mousewheel'
- 'DOMMouseScroll'; // Assume old Firefox
+ $.MouseTracker.wheelEventName = ( 'onwheel' in document.createElement( 'div' ) ) ? 'wheel' : // Modern browsers support 'wheel'
+ document.onmousewheel !== undefined ? 'mousewheel' : // Webkit (and unsupported IE) support at least 'mousewheel'
+ 'DOMMouseScroll'; // Assume old Firefox (deprecated)
/**
* Detect browser pointer device event model(s) and build appropriate list of events to subscribe to.
@@ -4398,7 +4655,7 @@ $.EventSource.prototype = {
}
if ( window.PointerEvent ) {
- // IE11 and other W3C Pointer Event implementations (see http://www.w3.org/TR/pointerevents)
+ // W3C Pointer Event implementations (see http://www.w3.org/TR/pointerevents)
$.MouseTracker.havePointerEvents = true;
$.MouseTracker.subscribeEvents.push( "pointerenter", "pointerleave", "pointerover", "pointerout", "pointerdown", "pointerup", "pointermove", "pointercancel" );
// Pointer events capture support
@@ -4937,7 +5194,6 @@ $.EventSource.prototype = {
/**
* Gets a W3C Pointer Events model compatible pointer type string from a DOM pointer event.
- * IE10 used a long integer value, but the W3C specification (and IE11+) use a string "mouse", "touch", "pen", etc.
*
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
@@ -4945,14 +5201,7 @@ $.EventSource.prototype = {
* @inner
*/
function getPointerType( event ) {
- if ( $.MouseTracker.havePointerEvents ) {
- // Note: IE pointer events bug - sends invalid pointerType on lostpointercapture events
- // and possibly other events. We rely on sane, valid property values in DOM events, so for
- // IE, when the pointerType is missing, we'll default to 'mouse'...should be right most of the time
- return event.pointerType || (( $.Browser.vendor === $.BROWSERS.IE ) ? 'mouse' : '');
- } else {
- return 'mouse';
- }
+ return $.MouseTracker.havePointerEvents && event.pointerType ? event.pointerType : 'mouse';
}
@@ -5338,7 +5587,7 @@ $.EventSource.prototype = {
// y-index scrolling.
// event.deltaMode: 0=pixel, 1=line, 2=page
// TODO: Deltas in pixel mode should be accumulated then a scroll value computed after $.DEFAULT_SETTINGS.pixelsPerWheelLine threshold reached
- nDelta = event.deltaY < 0 ? 1 : -1;
+ nDelta = event.deltaY ? (event.deltaY < 0 ? 1 : -1) : 0;
eventInfo = {
originalEvent: event,
@@ -5820,15 +6069,14 @@ $.EventSource.prototype = {
};
// Most browsers implicitly capture touch pointer events
- // Note no IE versions have element.hasPointerCapture() so no implicit
- // pointer capture possible
+ // Note no IE versions (unsupported) have element.hasPointerCapture() so
+ // no implicit pointer capture possible
// var implicitlyCaptured = ($.MouseTracker.havePointerEvents &&
// event.target.hasPointerCapture &&
// $.Browser.vendor !== $.BROWSERS.IE) ?
// event.target.hasPointerCapture(event.pointerId) : false;
var implicitlyCaptured = $.MouseTracker.havePointerEvents &&
- gPoint.type === 'touch' &&
- $.Browser.vendor !== $.BROWSERS.IE;
+ gPoint.type === 'touch';
//$.console.log('pointerdown ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
@@ -7046,7 +7294,7 @@ $.EventSource.prototype = {
* OpenSeadragon - Control
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -7238,11 +7486,7 @@ $.Control.prototype = {
* @param {Number} opactiy - a value between 1 and 0 inclusively.
*/
setOpacity: function( opacity ) {
- if ( this.element[ $.SIGNAL ] && $.Browser.vendor === $.BROWSERS.IE ) {
- $.setElementOpacity( this.element, opacity, true );
- } else {
- $.setElementOpacity( this.wrapper, opacity, true );
- }
+ $.setElementOpacity( this.wrapper, opacity, true );
}
};
@@ -7252,7 +7496,7 @@ $.Control.prototype = {
* OpenSeadragon - ControlDock
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -7309,7 +7553,9 @@ $.Control.prototype = {
if( this.element ){
this.element = $.getElement( this.element );
this.element.appendChild( this.container );
- this.element.style.position = 'relative';
+ if( $.getElementStyle(this.element).position === 'static' ){
+ this.element.style.position = 'relative';
+ }
this.container.style.width = '100%';
this.container.style.height = '100%';
}
@@ -7481,7 +7727,7 @@ $.Control.prototype = {
/*
* OpenSeadragon - Placement
*
- * Copyright (C) 2010-2016 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -7621,7 +7867,7 @@ $.Control.prototype = {
* OpenSeadragon - Viewer
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -7708,6 +7954,21 @@ $.Viewer = function( options ) {
delete options.config;
}
+ // Move deprecated drawer options from the base options object into a sub-object
+ // This is an array to make it easy to add additional properties to convert to
+ // drawer options later if it makes sense to set at the drawer level rather than
+ // per tiled image (for example, subPixelRoundingForTransparency).
+ let drawerOptionList = [
+ 'useCanvas', // deprecated
+ ];
+ options.drawerOptions = Object.assign({},
+ drawerOptionList.reduce((drawerOptions, option) => {
+ drawerOptions[option] = options[option];
+ delete options[option];
+ return drawerOptions;
+ }, {}),
+ options.drawerOptions);
+
//Public properties
//Allow the options object to override global defaults
$.extend( true, this, {
@@ -7817,6 +8078,7 @@ $.Viewer = function( options ) {
$.console.warn("Hash " + this.hash + " has already been used.");
}
+
//Private state properties
THIS[ this.hash ] = {
fsBoundsDelta: new $.Point( 1, 1 ),
@@ -8002,24 +8264,25 @@ $.Viewer = function( options ) {
// Create the viewport
this.viewport = new $.Viewport({
- containerSize: THIS[ this.hash ].prevContainerSize,
- springStiffness: this.springStiffness,
- animationTime: this.animationTime,
- minZoomImageRatio: this.minZoomImageRatio,
- maxZoomPixelRatio: this.maxZoomPixelRatio,
- visibilityRatio: this.visibilityRatio,
- wrapHorizontal: this.wrapHorizontal,
- wrapVertical: this.wrapVertical,
- defaultZoomLevel: this.defaultZoomLevel,
- minZoomLevel: this.minZoomLevel,
- maxZoomLevel: this.maxZoomLevel,
- viewer: this,
- degrees: this.degrees,
- flipped: this.flipped,
- navigatorRotate: this.navigatorRotate,
- homeFillsViewer: this.homeFillsViewer,
- margins: this.viewportMargins,
- silenceMultiImageWarnings: this.silenceMultiImageWarnings
+ containerSize: THIS[ this.hash ].prevContainerSize,
+ springStiffness: this.springStiffness,
+ animationTime: this.animationTime,
+ minZoomImageRatio: this.minZoomImageRatio,
+ maxZoomPixelRatio: this.maxZoomPixelRatio,
+ visibilityRatio: this.visibilityRatio,
+ wrapHorizontal: this.wrapHorizontal,
+ wrapVertical: this.wrapVertical,
+ defaultZoomLevel: this.defaultZoomLevel,
+ minZoomLevel: this.minZoomLevel,
+ maxZoomLevel: this.maxZoomLevel,
+ viewer: this,
+ degrees: this.degrees,
+ flipped: this.flipped,
+ overlayPreserveContentDirection: this.overlayPreserveContentDirection,
+ navigatorRotate: this.navigatorRotate,
+ homeFillsViewer: this.homeFillsViewer,
+ margins: this.viewportMargins,
+ silenceMultiImageWarnings: this.silenceMultiImageWarnings
});
this.viewport._setContentBounds(this.world.getHomeBounds(), this.world.getContentFactor());
@@ -8037,13 +8300,41 @@ $.Viewer = function( options ) {
maxImageCacheCount: this.maxImageCacheCount
});
- // Create the drawer
- this.drawer = new $.Drawer({
- viewer: this,
- viewport: this.viewport,
- element: this.canvas,
- debugGridColor: this.debugGridColor
- });
+ //Create the drawer based on selected options
+ if (Object.prototype.hasOwnProperty.call(this.drawerOptions, 'useCanvas') ){
+ $.console.error('useCanvas is deprecated, use the "drawer" option to indicate preferred drawer(s)');
+
+ // for backwards compatibility, use HTMLDrawer if useCanvas is defined and is falsey
+ if (!this.drawerOptions.useCanvas){
+ this.drawer = $.HTMLDrawer;
+ }
+
+ delete this.drawerOptions.useCanvas;
+ }
+ let drawerCandidates = Array.isArray(this.drawer) ? this.drawer : [this.drawer];
+ if (drawerCandidates.length === 0){
+ // if an empty array was passed in, throw a warning and use the defaults
+ // note: if the drawer option is not specified, the defaults will already be set so this won't apply
+ drawerCandidates = [$.DEFAULT_SETTINGS.drawer].flat(); // ensure it is a list
+ $.console.warn('No valid drawers were selected. Using the default value.');
+ }
+
+
+ this.drawer = null;
+ for (const drawerCandidate of drawerCandidates){
+ let success = this.requestDrawer(drawerCandidate, {mainDrawer: true, redrawImmediately: false});
+ if(success){
+ break;
+ }
+ }
+
+ if (!this.drawer){
+ $.console.error('No drawer could be created!');
+ throw('Error with creating the selected drawer(s)');
+ }
+
+ // Pass the imageSmoothingEnabled option along to the drawer
+ this.drawer.setImageSmoothingEnabled(this.imageSmoothingEnabled);
// Overlay container
this.overlaysContainer = $.makeNeutralElement( "div" );
@@ -8089,6 +8380,10 @@ $.Viewer = function( options ) {
displayRegionColor: this.navigatorDisplayRegionColor,
crossOriginPolicy: this.crossOriginPolicy,
animationTime: this.animationTime,
+ drawer: this.drawer.getType(),
+ loadTilesWithAjax: this.loadTilesWithAjax,
+ ajaxHeaders: this.ajaxHeaders,
+ ajaxWithCredentials: this.ajaxWithCredentials,
});
}
@@ -8115,11 +8410,6 @@ $.Viewer = function( options ) {
beginControlsAutoHide( _this );
} );
- // Initial canvas options
- if ( this.imageSmoothingEnabled !== undefined && !this.imageSmoothingEnabled){
- this.drawer.setImageSmoothingEnabled(this.imageSmoothingEnabled);
- }
-
// Register the viewer
$._viewers.set(this.element, this);
};
@@ -8507,6 +8797,73 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
this.removeAllHandlers();
},
+ /**
+ * Request a drawer for this viewer, as a supported string or drawer constructor.
+ * @param {String | OpenSeadragon.DrawerBase} drawerCandidate The type of drawer to try to construct.
+ * @param { Object } options
+ * @param { Boolean } [options.mainDrawer] Whether to use this as the viewer's main drawer. Default = true.
+ * @param { Boolean } [options.redrawImmediately] Whether to immediately draw a new frame. Only used if options.mainDrawer = true. Default = true.
+ * @param { Object } [options.drawerOptions] Options for this drawer. Defaults to viewer.drawerOptions.
+ * for this viewer type. See {@link OpenSeadragon.Options}.
+ * @returns {Object | Boolean} The drawer that was created, or false if the requested drawer is not supported
+ */
+ requestDrawer(drawerCandidate, options){
+ const defaultOpts = {
+ mainDrawer: true,
+ redrawImmediately: true,
+ drawerOptions: null
+ };
+ options = $.extend(true, defaultOpts, options);
+ const mainDrawer = options.mainDrawer;
+ const redrawImmediately = options.redrawImmediately;
+ const drawerOptions = options.drawerOptions;
+
+ const oldDrawer = this.drawer;
+
+ let Drawer = null;
+
+ //if the candidate inherits from a drawer base, use it
+ if (drawerCandidate && drawerCandidate.prototype instanceof $.DrawerBase) {
+ Drawer = drawerCandidate;
+ drawerCandidate = 'custom';
+ } else if (typeof drawerCandidate === "string") {
+ Drawer = $.determineDrawer(drawerCandidate);
+ }
+
+ if(!Drawer){
+ $.console.warn('Unsupported drawer! Drawer must be an existing string type, or a class that extends OpenSeadragon.DrawerBase.');
+ }
+
+ // if the drawer is supported, create it and return true
+ if (Drawer && Drawer.isSupported()) {
+
+ // first destroy the previous drawer
+ if(oldDrawer && mainDrawer){
+ oldDrawer.destroy();
+ }
+
+ // create the new drawer
+ const newDrawer = new Drawer({
+ viewer: this,
+ viewport: this.viewport,
+ element: this.canvas,
+ debugGridColor: this.debugGridColor,
+ options: drawerOptions || this.drawerOptions[drawerCandidate],
+ });
+
+ if(mainDrawer){
+ this.drawer = newDrawer;
+ if(redrawImmediately){
+ this.forceRedraw();
+ }
+ }
+
+ return newDrawer;
+ }
+
+ return false;
+ },
+
/**
* @function
* @returns {Boolean}
@@ -8659,7 +9016,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;
},
@@ -8706,7 +9063,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
return this;
}
- if ( fullPage ) {
+ if ( fullPage && this.element ) {
this.elementSize = $.getElementSize( this.element );
this.pageScroll = $.getPageScroll();
@@ -9223,6 +9580,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
minZoomImageRatio: _this.minZoomImageRatio,
wrapHorizontal: _this.wrapHorizontal,
wrapVertical: _this.wrapVertical,
+ maxTilesPerFrame: _this.maxTilesPerFrame,
immediateRender: _this.immediateRender,
blendTime: _this.blendTime,
alwaysBlend: _this.alwaysBlend,
@@ -9709,7 +10067,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
* viewport which the location coordinates will be treated as relative
* to.
* @param {function} [onDraw] - If supplied the callback is called when the overlay
- * needs to be drawn. It it the responsibility of the callback to do any drawing/positioning.
+ * needs to be drawn. It is the responsibility of the callback to do any drawing/positioning.
* It is passed position, size and element.
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:add-overlay
@@ -10021,7 +10379,6 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
width: this.referenceStripWidth,
tileSources: this.tileSources,
prefixUrl: this.prefixUrl,
- useCanvas: this.useCanvas,
viewer: this
});
@@ -10170,7 +10527,6 @@ function getTileSourceImplementation( viewer, tileSource, imgOptions, successCal
ajaxHeaders: imgOptions.ajaxHeaders ?
imgOptions.ajaxHeaders : viewer.ajaxHeaders,
splitHashDataForPost: viewer.splitHashDataForPost,
- useCanvas: viewer.useCanvas,
success: function( event ) {
successCallback( event.tileSource );
}
@@ -10188,9 +10544,6 @@ function getTileSourceImplementation( viewer, tileSource, imgOptions, successCal
if (tileSource.ajaxWithCredentials === undefined) {
tileSource.ajaxWithCredentials = viewer.ajaxWithCredentials;
}
- if (tileSource.useCanvas === undefined) {
- tileSource.useCanvas = viewer.useCanvas;
- }
if ( $.isFunction( tileSource.getTileUrl ) ) {
//Custom tile source
@@ -10793,10 +11146,11 @@ function onCanvasDragEnd( event ) {
*/
this.raiseEvent('canvas-drag-end', canvasDragEndEventArgs);
- gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
+ gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
if (!canvasDragEndEventArgs.preventDefaultAction && this.viewport) {
if ( !THIS[ this.hash ].draggingToZoom &&
+ gestureSettings.dragToPan &&
gestureSettings.flickEnabled &&
event.speed >= gestureSettings.flickMinSpeed) {
var amplitudeX = 0;
@@ -11323,7 +11677,7 @@ function updateOnce( viewer ) {
var viewportChange = viewer.viewport.update();
- var animated = viewer.world.update() || viewportChange;
+ var animated = viewer.world.update(viewportChange) || viewportChange;
if (viewportChange) {
/**
@@ -11413,7 +11767,6 @@ function updateOnce( viewer ) {
function drawWorld( viewer ) {
viewer.imageLoader.clear();
- viewer.drawer.clear();
viewer.world.draw();
/**
@@ -11567,13 +11920,31 @@ function onFlip() {
this.viewport.toggleFlip();
}
+/**
+ * Find drawer
+ */
+$.determineDrawer = function( id ){
+ for (let property in OpenSeadragon) {
+ const drawer = OpenSeadragon[ property ],
+ proto = drawer.prototype;
+ if( proto &&
+ proto instanceof OpenSeadragon.DrawerBase &&
+ $.isFunction( proto.getType ) &&
+ proto.getType.call( drawer ) === id
+ ){
+ return drawer;
+ }
+ }
+ return null;
+};
+
}( OpenSeadragon ));
/*
* OpenSeadragon - Navigator
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -11741,9 +12112,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.
@@ -11752,7 +12120,6 @@ $.Navigator = function( options ){
style['float'] = 'left'; //Webkit
style.cssFloat = 'left'; //Firefox
- style.styleFloat = 'left'; //IE
style.zIndex = 999999999;
style.cursor = 'default';
style.boxSizing = 'content-box';
@@ -11881,8 +12248,9 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /*
this.viewport.resize( containerSize, true );
this.viewport.goHome(true);
this.oldContainerSize = containerSize;
- this.drawer.clear();
+ this.world.update();
this.world.draw();
+ this.update(this.viewer.viewport);
}
}
},
@@ -11929,7 +12297,7 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /*
/**
* Used to update the navigator minimap's viewport rectangle when a change in the viewer's viewport occurs.
* @function
- * @param {OpenSeadragon.Viewport} The viewport this navigator is tracking.
+ * @param {OpenSeadragon.Viewport} [viewport] The viewport to display. Default: the viewport this navigator is tracking.
*/
update: function( viewport ) {
@@ -11940,6 +12308,10 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /*
topleft,
bottomright;
+ if(!viewport){
+ viewport = this.viewer.viewport;
+ }
+
viewerSize = $.getElementSize( this.viewer.element );
if ( this._resizeWithViewer && viewerSize.x && viewerSize.y && !viewerSize.equals( this.oldViewerSize ) ) {
this.oldViewerSize = viewerSize;
@@ -12246,7 +12618,7 @@ function setElementTransform( element, rule ) {
* OpenSeadragon - getString/setString
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -12368,7 +12740,7 @@ $.extend( $, /** @lends OpenSeadragon */{
* OpenSeadragon - Point
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -12614,7 +12986,7 @@ $.Point.prototype = {
* OpenSeadragon - TileSource
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -12982,6 +13354,7 @@ $.TileSource.prototype = {
point.y >= 0 && point.y <= 1 / this.aspectRatio;
$.console.assert(validPoint, "[TileSource.getTileAtPoint] must be called with a valid point.");
+
var widthScaled = this.dimensions.x * this.getLevelScale(level);
var pixelX = point.x * widthScaled;
var pixelY = point.y * widthScaled;
@@ -13180,13 +13553,13 @@ $.TileSource.prototype = {
},
/**
- * Responsible determining if a the particular TileSource supports the
+ * Responsible for determining if the particular TileSource supports the
* data format ( and allowed to apply logic against the url the data was
* loaded from, if any ). Overriding implementations are expected to do
* something smart with data and / or url to determine support. Also
- * understand that iteration order of TileSources is not guarunteed so
+ * understand that iteration order of TileSources is not guaranteed so
* please make sure your data or url is expressive enough to ensure a simple
- * and sufficient mechanisim for clear determination.
+ * and sufficient mechanism for clear determination.
* @function
* @param {String|Object|Array|Document} data
* @param {String} url - the url the data was loaded
@@ -13387,7 +13760,7 @@ $.TileSource.prototype = {
};
// Load the tile with an AJAX request if the loadWithAjax option is
- // set. Otherwise load the image by setting the source proprety of the image object.
+ // set. Otherwise load the image by setting the source property of the image object.
if (context.loadWithAjax) {
dataStore.request = $.makeAjaxRequest({
url: context.src,
@@ -13604,7 +13977,7 @@ $.TileSource.determineType = function( tileSource, data, url ){
* OpenSeadragon - DziTileSource
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -13983,7 +14356,7 @@ function configureFromObject( tileSource, configuration ){
* OpenSeadragon - IIIFTileSource
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2023 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -14126,7 +14499,7 @@ $.IIIFTileSource = function( options ){
if( this.sizes ) {
var sizeLength = this.sizes.length;
if ( (sizeLength === options.maxLevel) || (sizeLength === options.maxLevel + 1) ) {
- this.levelSizes = this.sizes;
+ this.levelSizes = this.sizes.slice().sort(( size1, size2 ) => size1.width - size2.width);
// Need to take into account that the list may or may not include the full resolution size
if( sizeLength === options.maxLevel ) {
this.levelSizes.push( {width: this.width, height: this.height} );
@@ -14599,7 +14972,7 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea
* OpenSeadragon - OsmTileSource
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -14746,7 +15119,7 @@ $.extend( $.OsmTileSource.prototype, $.TileSource.prototype, /** @lends OpenSead
* OpenSeadragon - TmsTileSource
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -15036,7 +15409,7 @@ $.extend( $.TmsTileSource.prototype, $.TileSource.prototype, /** @lends OpenSead
* OpenSeadragon - LegacyTileSource
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -15328,7 +15701,7 @@ function configureFromObject( tileSource, configuration ){
* OpenSeadragon - ImageTileSource
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -15368,8 +15741,8 @@ function configureFromObject( tileSource, configuration ){
* 1. viewer.open({type: 'image', url: fooUrl});
* 2. viewer.open(new OpenSeadragon.ImageTileSource({url: fooUrl}));
*
- * With the first syntax, the crossOriginPolicy, ajaxWithCredentials and
- * useCanvas options are inherited from the viewer if they are not
+ * With the first syntax, the crossOriginPolicy and ajaxWithCredentials
+ * options are inherited from the viewer if they are not
* specified directly in the options object.
*
* @memberof OpenSeadragon
@@ -15384,16 +15757,13 @@ function configureFromObject( tileSource, configuration ){
* domains.
* @param {String|Boolean} [options.ajaxWithCredentials=false] Whether to set
* the withCredentials XHR flag for AJAX requests (when loading tile sources).
- * @param {Boolean} [options.useCanvas=true] Set to false to prevent any use
- * of the canvas API.
*/
$.ImageTileSource = function (options) {
options = $.extend({
buildPyramid: true,
crossOriginPolicy: false,
- ajaxWithCredentials: false,
- useCanvas: true
+ ajaxWithCredentials: false
}, options);
$.TileSource.apply(this, [options]);
@@ -15524,9 +15894,11 @@ function configureFromObject( tileSource, configuration ){
/**
* Destroys ImageTileSource
* @function
+ * @param {OpenSeadragon.Viewer} viewer the viewer that is calling
+ * destroy on the ImageTileSource
*/
- destroy: function () {
- this._freeupCanvasMemory();
+ destroy: function (viewer) {
+ this._freeupCanvasMemory(viewer);
},
// private
@@ -15540,7 +15912,7 @@ function configureFromObject( tileSource, configuration ){
height: this._image.naturalHeight
}];
- if (!this.buildPyramid || !$.supportsCanvas || !this.useCanvas) {
+ if (!this.buildPyramid || !$.supportsCanvas) {
// We don't need the image anymore. Allows it to be GC.
delete this._image;
return levels;
@@ -15596,11 +15968,27 @@ function configureFromObject( tileSource, configuration ){
* 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;
+
+ if(viewer){
+ /**
+ * 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
+ * @private
+ */
+ viewer.raiseEvent("image-unloaded", {
+ context2D: this.levels[i].context2D
+ });
+ }
+
}
}
},
@@ -15612,7 +16000,7 @@ function configureFromObject( tileSource, configuration ){
* OpenSeadragon - TileSourceCollection
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -15655,7 +16043,7 @@ $.TileSourceCollection = function(tileSize, tileSources, rows, layout) {
* OpenSeadragon - Button
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -15820,13 +16208,6 @@ $.Button = function( options ) {
this.imgDown.style.visibility =
"hidden";
- if ($.Browser.vendor === $.BROWSERS.FIREFOX && $.Browser.version < 3) {
- this.imgGroup.style.top =
- this.imgHover.style.top =
- this.imgDown.style.top =
- "";
- }
-
this.element.appendChild( this.imgRest );
this.element.appendChild( this.imgGroup );
this.element.appendChild( this.imgHover );
@@ -16200,7 +16581,7 @@ function outTo( button, newState ) {
* OpenSeadragon - ButtonGroup
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -16359,7 +16740,7 @@ $.ButtonGroup.prototype = {
* OpenSeadragon - Rect
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -16923,7 +17304,7 @@ $.Rect.prototype = {
* OpenSeadragon - ReferenceStrip
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -16967,7 +17348,7 @@ var THIS = {};
*
* This idea is a reexpression of the idea of dzi collections
* which allows a clearer algorithm to reuse the tile sources already
- * supported by OpenSeadragon, in heterogenious or homogenious
+ * supported by OpenSeadragon, in heterogeneous or homogeneous
* sequences just like mixed groups already supported by the viewer
* for the purpose of image sequnces.
*
@@ -17114,7 +17495,6 @@ $.ReferenceStrip = function ( options ) {
element.style.display = 'inline';
element.style['float'] = 'left'; //Webkit
element.style.cssFloat = 'left'; //Firefox
- element.style.styleFloat = 'left'; //IE
element.style.padding = '2px';
$.setElementTouchActionNone( element );
$.setElementPointerEventsNone( element );
@@ -17376,7 +17756,7 @@ function loadPanels( strip, viewerSize, scroll ) {
animationTime: 0,
loadTilesWithAjax: strip.viewer.loadTilesWithAjax,
ajaxHeaders: strip.viewer.ajaxHeaders,
- useCanvas: strip.useCanvas
+ drawer: 'canvas', //always use canvas for the reference strip
} );
// Allow pointer events to pass through miniViewer's canvas/container
// elements so implicit pointer capture works on touch devices
@@ -17534,7 +17914,7 @@ function onKeyPress( event ) {
* OpenSeadragon - DisplayRect
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -17606,7 +17986,7 @@ $.extend( $.DisplayRect.prototype, $.Rect.prototype );
* OpenSeadragon - Spring
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -17810,12 +18190,13 @@ $.Spring.prototype = {
/**
* @function
- * @returns true if the value got updated, false otherwise
+ * @returns true if the spring is still updating its value, false if it is
+ * already at the target value.
*/
update: function() {
this.current.time = $.now();
- var startValue, targetValue;
+ let startValue, targetValue;
if (this._exponential) {
startValue = this.start._logValue;
targetValue = this.target._logValue;
@@ -17824,24 +18205,25 @@ $.Spring.prototype = {
targetValue = this.target.value;
}
- var currentValue = (this.current.time >= this.target.time) ?
- targetValue :
- startValue +
- ( targetValue - startValue ) *
- transform(
- this.springStiffness,
- ( this.current.time - this.start.time ) /
- ( this.target.time - this.start.time )
- );
-
- var oldValue = this.current.value;
- if (this._exponential) {
- this.current.value = Math.exp(currentValue);
+ if(this.current.time >= this.target.time){
+ this.current.value = this.target.value;
} else {
- this.current.value = currentValue;
+ let currentValue = startValue +
+ ( targetValue - startValue ) *
+ transform(
+ this.springStiffness,
+ ( this.current.time - this.start.time ) /
+ ( this.target.time - this.start.time )
+ );
+
+ if (this._exponential) {
+ this.current.value = Math.exp(currentValue);
+ } else {
+ this.current.value = currentValue;
+ }
}
- return oldValue !== this.current.value;
+ return this.current.value !== this.target.value;
},
/**
@@ -17868,7 +18250,7 @@ function transform( stiffness, x ) {
* OpenSeadragon - ImageLoader
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -17903,6 +18285,8 @@ function transform( stiffness, x ) {
/**
* @class ImageJob
* @classdesc Handles downloading of a single image.
+ *
+ * @memberof OpenSeadragon
* @param {Object} options - Options for this ImageJob.
* @param {String} [options.src] - URL of image to download.
* @param {Tile} [options.tile] - Tile that belongs the data to.
@@ -17953,6 +18337,7 @@ $.ImageJob.prototype = {
/**
* Starts the image job.
* @method
+ * @memberof OpenSeadragon.ImageJob#
*/
start: function() {
this.tries++;
@@ -17979,6 +18364,7 @@ $.ImageJob.prototype = {
* @param {*} data data that has been downloaded
* @param {XMLHttpRequest} request reference to the request if used
* @param {string} errorMessage description upon failure
+ * @memberof OpenSeadragon.ImageJob#
*/
finish: function(data, request, errorMessage ) {
this.data = data;
@@ -18132,7 +18518,7 @@ function completeJob(loader, job, callback) {
* OpenSeadragon - Tile
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -18211,6 +18597,12 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
* @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.
@@ -18404,64 +18796,6 @@ $.Tile.prototype = {
return !!this.context2D || this.getUrl().match('.png');
},
- /**
- * Renders the tile in an html container.
- * @function
- * @param {Element} container
- */
- drawHTML: function( container ) {
- if (!this.cacheImageRecord) {
- $.console.warn(
- '[Tile.drawHTML] attempting to draw tile %s when it\'s not cached',
- this.toString());
- return;
- }
-
- if ( !this.loaded ) {
- $.console.warn(
- "Attempting to draw tile %s when it's not yet loaded.",
- this.toString()
- );
- return;
- }
-
- //EXPERIMENTAL - trying to figure out how to scale the container
- // content during animation of the container size.
-
- if ( !this.element ) {
- var image = this.getImage();
- if (!image) {
- return;
- }
-
- this.element = $.makeNeutralElement( "div" );
- this.imgElement = image.cloneNode();
- this.imgElement.style.msInterpolationMode = "nearest-neighbor";
- this.imgElement.style.width = "100%";
- this.imgElement.style.height = "100%";
-
- this.style = this.element.style;
- this.style.position = "absolute";
- }
- if ( this.element.parentNode !== container ) {
- container.appendChild( this.element );
- }
- if ( this.imgElement.parentNode !== this.element ) {
- this.element.appendChild( this.imgElement );
- }
-
- this.style.top = this.position.y + "px";
- this.style.left = this.position.x + "px";
- this.style.height = this.size.y + "px";
- this.style.width = this.size.x + "px";
-
- if (this.flipped) {
- this.style.transform = "scaleX(-1)";
- }
-
- $.setElementOpacity( this.element, this.opacity );
- },
-
/**
* The Image object for this tile.
* @member {Object} image
@@ -18512,114 +18846,7 @@ $.Tile.prototype = {
* @returns {CanvasRenderingContext2D}
*/
getCanvasContext: function() {
- return this.context2D || this.cacheImageRecord.getRenderedContext();
- },
-
- /**
- * Renders the tile in a canvas-based context.
- * @function
- * @param {Canvas} context
- * @param {Function} drawingHandler - 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
- * @param {OpenSeadragon.Point} [translate] - A translation vector
- * @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
- * position and size of tiles supporting alpha channel in non-transparency
- * context.
- * @param {OpenSeadragon.TileSource} source - The source specification of the tile.
- */
- drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source) {
-
- var position = this.position.times($.pixelDensityRatio),
- size = this.size.times($.pixelDensityRatio),
- rendered;
-
- if (!this.context2D && !this.cacheImageRecord) {
- $.console.warn(
- '[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached',
- this.toString());
- return;
- }
-
- rendered = this.getCanvasContext();
-
- if ( !this.loaded || !rendered ){
- $.console.warn(
- "Attempting to draw tile %s when it's not yet loaded.",
- this.toString()
- );
-
- return;
- }
-
- context.save();
- context.globalAlpha = this.opacity;
-
- if (typeof scale === 'number' && scale !== 1) {
- // draw tile at a different scale
- position = position.times(scale);
- size = size.times(scale);
- }
-
- if (translate instanceof $.Point) {
- // shift tile position slightly
- position = position.plus(translate);
- }
-
- //if we are supposed to be rendering fully opaque rectangle,
- //ie its done fading or fading is turned off, and if we are drawing
- //an image with an alpha channel, then the only way
- //to avoid seeing the tile underneath is to clear the rectangle
- if (context.globalAlpha === 1 && this.hasTransparency) {
- if (shouldRoundPositionAndSize) {
- // Round to the nearest whole pixel so we don't get seams from overlap.
- position.x = Math.round(position.x);
- position.y = Math.round(position.y);
- size.x = Math.round(size.x);
- size.y = Math.round(size.y);
- }
-
- //clearing only the inside of the rectangle occupied
- //by the png prevents edge flikering
- context.clearRect(
- position.x,
- position.y,
- size.x,
- size.y
- );
- }
-
- // This gives the application a chance to make image manipulation
- // changes as we are rendering the image
- drawingHandler({context: context, tile: this, rendered: rendered});
-
- var sourceWidth, sourceHeight;
- if (this.sourceBounds) {
- sourceWidth = Math.min(this.sourceBounds.width, rendered.canvas.width);
- sourceHeight = Math.min(this.sourceBounds.height, rendered.canvas.height);
- } else {
- sourceWidth = rendered.canvas.width;
- sourceHeight = rendered.canvas.height;
- }
-
- context.translate(position.x + size.x / 2, 0);
- if (this.flipped) {
- context.scale(-1, 1);
- }
- context.drawImage(
- rendered.canvas,
- 0,
- 0,
- sourceWidth,
- sourceHeight,
- -size.x / 2,
- position.y,
- size.x,
- size.y
- );
-
- context.restore();
+ return this.context2D || (this.cacheImageRecord && this.cacheImageRecord.getRenderedContext());
},
/**
@@ -18691,7 +18918,7 @@ $.Tile.prototype = {
* OpenSeadragon - Overlay
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -18818,6 +19045,7 @@ $.Tile.prototype = {
}
this.element = options.element;
+ this.element.innerHTML = "rendered
is the context with the pre-drawn image.
- * @param {Float} [scale=1] - Apply a scale to tile position and size. Defaults to 1.
- * @param {OpenSeadragon.Point} [translate] A translation vector to offset tile position
- * @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
- * position and size of tiles supporting alpha channel in non-transparency
- * context.
- * @param {OpenSeadragon.TileSource} source - The source specification of the tile.
*/
- drawTile: function( tile, drawingHandler, useSketch, scale, translate, shouldRoundPositionAndSize, source) {
- $.console.assert(tile, '[Drawer.drawTile] tile is required');
- $.console.assert(drawingHandler, '[Drawer.drawTile] drawingHandler is required');
+ _drawTile( tile ) {
+ $.console.assert(tile, '[Drawer._drawTile] tile is required');
- if (this.useCanvas) {
- var context = this._getContext(useSketch);
- scale = scale || 1;
- tile.drawCanvas(context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source);
- } else {
- tile.drawHTML( this.canvas );
+ let container = this.canvas;
+
+ if (!tile.cacheImageRecord) {
+ $.console.warn(
+ '[Drawer._drawTileToHTML] attempting to draw tile %s when it\'s not cached',
+ tile.toString());
+ return;
}
- },
- _getContext: function( useSketch ) {
- var context = this.context;
- if ( useSketch ) {
- if (this.sketchCanvas === null) {
- this.sketchCanvas = document.createElement( "canvas" );
- var sketchCanvasSize = this._calculateSketchCanvasSize();
- this.sketchCanvas.width = sketchCanvasSize.x;
- this.sketchCanvas.height = sketchCanvasSize.y;
- this.sketchContext = this.sketchCanvas.getContext( "2d" );
+ if ( !tile.loaded ) {
+ $.console.warn(
+ "Attempting to draw tile %s when it's not yet loaded.",
+ tile.toString()
+ );
+ return;
+ }
- // If the viewport is not currently rotated, the sketchCanvas
- // will have the same size as the main canvas. However, if
- // the viewport get rotated later on, we will need to resize it.
- if (this.viewport.getRotation() === 0) {
- var self = this;
- this.viewer.addHandler('rotate', function resizeSketchCanvas() {
- if (self.viewport.getRotation() === 0) {
- return;
- }
- self.viewer.removeHandler('rotate', resizeSketchCanvas);
- var sketchCanvasSize = self._calculateSketchCanvasSize();
- self.sketchCanvas.width = sketchCanvasSize.x;
- self.sketchCanvas.height = sketchCanvasSize.y;
- });
- }
- this._updateImageSmoothingEnabled(this.sketchContext);
+ //EXPERIMENTAL - trying to figure out how to scale the container
+ // content during animation of the container size.
+
+ if ( !tile.element ) {
+ var image = tile.getImage();
+ if (!image) {
+ return;
}
- context = this.sketchContext;
+
+ tile.element = $.makeNeutralElement( "div" );
+ tile.imgElement = image.cloneNode();
+ tile.imgElement.style.msInterpolationMode = "nearest-neighbor";
+ tile.imgElement.style.width = "100%";
+ tile.imgElement.style.height = "100%";
+
+ tile.style = tile.element.style;
+ tile.style.position = "absolute";
}
- return context;
- },
- // private
- saveContext: function( useSketch ) {
- if (!this.useCanvas) {
- return;
+ if ( tile.element.parentNode !== container ) {
+ container.appendChild( tile.element );
+ }
+ if ( tile.imgElement.parentNode !== tile.element ) {
+ tile.element.appendChild( tile.imgElement );
}
- this._getContext( useSketch ).save();
- },
+ tile.style.top = tile.position.y + "px";
+ tile.style.left = tile.position.x + "px";
+ tile.style.height = tile.size.y + "px";
+ tile.style.width = tile.size.x + "px";
- // private
- restoreContext: function( useSketch ) {
- if (!this.useCanvas) {
- return;
+ if (tile.flipped) {
+ tile.style.transform = "scaleX(-1)";
}
- this._getContext( useSketch ).restore();
- },
+ $.setElementOpacity( tile.element, tile.opacity );
+ }
- // private
- setClip: function(rect, useSketch) {
- if (!this.useCanvas) {
- return;
- }
+}
- var context = this._getContext( useSketch );
- context.beginPath();
- context.rect(rect.x, rect.y, rect.width, rect.height);
- context.clip();
- },
+$.HTMLDrawer = HTMLDrawer;
- // private
- drawRectangle: function(rect, fillStyle, useSketch) {
- if (!this.useCanvas) {
- return;
- }
- var context = this._getContext( useSketch );
- context.save();
- context.fillStyle = fillStyle;
- context.fillRect(rect.x, rect.y, rect.width, rect.height);
- context.restore();
- },
+}( OpenSeadragon ));
- /**
- * Blends the sketch canvas in the main canvas.
- * @param {Object} options The options
- * @param {Float} options.opacity The opacity of the blending.
- * @param {Float} [options.scale=1] The scale at which tiles were drawn on
- * the sketch. Default is 1.
- * Use scale to draw at a lower scale and then enlarge onto the main canvas.
- * @param {OpenSeadragon.Point} [options.translate] A translation vector
- * that was used to draw the tiles
- * @param {String} [options.compositeOperation] - How the image is
- * composited onto other images; see compositeOperation in
- * {@link OpenSeadragon.Options} for possible values.
- * @param {OpenSeadragon.Rect} [options.bounds] The part of the sketch
- * canvas to blend in the main canvas. If specified, options.scale and
- * options.translate get ignored.
+/*
+ * OpenSeadragon - CanvasDrawer
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2024 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( $ ){
+
+ const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc
+/**
+ * @class OpenSeadragon.CanvasDrawer
+ * @extends OpenSeadragon.DrawerBase
+ * @classdesc Default implementation of CanvasDrawer for an {@link OpenSeadragon.Viewer}.
+ * @param {Object} options - Options for this Drawer.
+ * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer.
+ * @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport.
+ * @param {Element} options.element - Parent element.
+ * @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details.
+ */
+
+class CanvasDrawer extends OpenSeadragon.DrawerBase{
+ constructor(options){
+ super(options);
+
+ /**
+ * The HTML element (canvas) that this drawer uses for drawing
+ * @member {Element} canvas
+ * @memberof OpenSeadragon.CanvasDrawer#
+ */
+
+ /**
+ * The parent element of this Drawer instance, passed in when the Drawer was created.
+ * The parent of {@link OpenSeadragon.WebGLDrawer#canvas}.
+ * @member {Element} container
+ * @memberof OpenSeadragon.CanvasDrawer#
+ */
+
+ /**
+ * 2d drawing context for {@link OpenSeadragon.CanvasDrawer#canvas}.
+ * @member {Object} context
+ * @memberof OpenSeadragon.CanvasDrawer#
+ * @private
+ */
+ this.context = this.canvas.getContext( '2d' );
+
+ // Sketch canvas used to temporarily draw tiles which cannot be drawn directly
+ // to the main canvas due to opacity. Lazily initialized.
+ this.sketchCanvas = null;
+ this.sketchContext = null;
+
+ // Image smoothing for canvas rendering (only if canvas is used).
+ // Canvas default is "true", so this will only be changed if user specifies "false" in the options or via setImageSmoothinEnabled.
+ this._imageSmoothingEnabled = true;
+
+ // Since the tile-drawn and tile-drawing events are fired by this drawer, make sure handlers can be added for them
+ this.viewer.allowEventHandler("tile-drawn");
+ this.viewer.allowEventHandler("tile-drawing");
+
+ }
+
+ /**
+ * @returns {Boolean} true if canvas is supported by the browser, otherwise false
*/
- blendSketch: function(opacity, scale, translate, compositeOperation) {
- var options = opacity;
- if (!$.isPlainObject(options)) {
- options = {
- opacity: opacity,
- scale: scale,
- translate: translate,
- compositeOperation: compositeOperation
+ static isSupported(){
+ return $.supportsCanvas;
+ }
+
+ getType(){
+ return 'canvas';
+ }
+
+ /**
+ * create the HTML element (e.g. canvas, div) 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;
+ }
+
+ /**
+ * Draws the TiledImages
+ */
+ draw(tiledImages) {
+ this._prepareNewFrame(); // prepare to draw a new frame
+ if(this.viewer.viewport.getFlip() !== this._viewportFlipped){
+ this._flip();
+ }
+ for(const tiledImage of tiledImages){
+ if (tiledImage.opacity !== 0) {
+ this._drawTiles(tiledImage);
+ }
+ }
+ }
+
+ /**
+ * @returns {Boolean} True - rotation is supported.
+ */
+ canRotate() {
+ return true;
+ }
+
+ /**
+ * Destroy the drawer (unload current loaded tiles)
+ */
+ destroy() {
+ //force unloading of current canvas (1x1 will be gc later, trick not necessarily needed)
+ this.canvas.width = 1;
+ this.canvas.height = 1;
+ this.sketchCanvas = null;
+ this.sketchContext = null;
+ this.container.removeChild(this.canvas);
+ }
+
+ /**
+ * @param {TiledImage} tiledImage the tiled image that is calling the function
+ * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams.
+ * @private
+ */
+ minimumOverlapRequired(tiledImage) {
+ return true;
+ }
+
+
+ /**
+ * Turns image smoothing on or off for this viewer. Note: Ignored in some (especially older) browsers that do not support this property.
+ *
+ * @function
+ * @param {Boolean} [imageSmoothingEnabled] - Whether or not the image is
+ * drawn smoothly on the canvas; see imageSmoothingEnabled in
+ * {@link OpenSeadragon.Options} for more explanation.
+ */
+ setImageSmoothingEnabled(imageSmoothingEnabled){
+ this._imageSmoothingEnabled = !!imageSmoothingEnabled;
+ this._updateImageSmoothingEnabled(this.context);
+ this.viewer.forceRedraw();
+ }
+
+ /**
+ * Draw a rectangle onto the canvas
+ * @param {OpenSeadragon.Rect} rect
+ */
+ drawDebuggingRect(rect) {
+ var context = this.context;
+ 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();
+ }
+
+ /**
+ * Test whether the current context is flipped or not
+ * @private
+ */
+ get _viewportFlipped(){
+ return this.context.getTransform().a < 0;
+ }
+
+ /**
+ * Fires the tile-drawing event.
+ * @private
+ */
+ _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 when the 'canvas' drawer is being used
+ *
+ * @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
+ });
+ }
+
+ /**
+ * Clears the Drawer so it's ready to draw another frame.
+ * @private
+ *
+ */
+ _prepareNewFrame() {
+ var viewportSize = this._calculateCanvasSize();
+ if( this.canvas.width !== viewportSize.x ||
+ this.canvas.height !== viewportSize.y ) {
+ this.canvas.width = viewportSize.x;
+ this.canvas.height = viewportSize.y;
+ this._updateImageSmoothingEnabled(this.context);
+ if ( this.sketchCanvas !== null ) {
+ var sketchCanvasSize = this._calculateSketchCanvasSize();
+ this.sketchCanvas.width = sketchCanvasSize.x;
+ this.sketchCanvas.height = sketchCanvasSize.y;
+ this._updateImageSmoothingEnabled(this.sketchContext);
+ }
+ }
+ this._clear();
+ }
+
+ /**
+ * @private
+ * @param {Boolean} useSketch Whether to clear sketch canvas or main canvas
+ * @param {OpenSeadragon.Rect} [bounds] The rectangle to clear
+ */
+ _clear(useSketch, bounds){
+ var context = this._getContext(useSketch);
+ if (bounds) {
+ context.clearRect(bounds.x, bounds.y, bounds.width, bounds.height);
+ } else {
+ var canvas = context.canvas;
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ }
+ }
+
+ /**
+ * Draws a TiledImage.
+ * @private
+ *
+ */
+ _drawTiles( tiledImage ) {
+ var lastDrawn = tiledImage.getTilesToDraw().map(info => info.tile);
+ if (tiledImage.opacity === 0 || (lastDrawn.length === 0 && !tiledImage.placeholderFillStyle)) {
+ return;
+ }
+
+ var tile = lastDrawn[0];
+ var useSketch;
+
+ if (tile) {
+ useSketch = tiledImage.opacity < 1 ||
+ (tiledImage.compositeOperation && tiledImage.compositeOperation !== 'source-over') ||
+ (!tiledImage._isBottomItem() &&
+ tiledImage.source.hasTransparency(tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData));
+ }
+
+ var sketchScale;
+ var sketchTranslate;
+
+ var zoom = this.viewport.getZoom(true);
+ var imageZoom = tiledImage.viewportToImageZoom(zoom);
+
+ if (lastDrawn.length > 1 &&
+ imageZoom > tiledImage.smoothTileEdgesMinZoom &&
+ !tiledImage.iOSDevice &&
+ tiledImage.getRotation(true) % 360 === 0 ){ // TODO: support tile edge smoothing with tiled image rotation.
+ // When zoomed in a lot (>100%) the tile edges are visible.
+ // So we have to composite them at ~100% and scale them up together.
+ // Note: Disabled on iOS devices per default as it causes a native crash
+ useSketch = true;
+ sketchScale = tile.getScaleForEdgeSmoothing();
+ sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale,
+ this._getCanvasSize(false),
+ this._getCanvasSize(true));
+ }
+
+ var bounds;
+ if (useSketch) {
+ if (!sketchScale) {
+ // Except when edge smoothing, we only clean the part of the
+ // sketch canvas we are going to use for performance reasons.
+ bounds = this.viewport.viewportToViewerElementRectangle(
+ tiledImage.getClippedBounds(true))
+ .getIntegerBoundingBox();
+
+ bounds = bounds.times($.pixelDensityRatio);
+ }
+ this._clear(true, bounds);
+ }
+
+ // When scaling, we must rotate only when blending the sketch canvas to
+ // avoid interpolation
+ if (!sketchScale) {
+ this._setRotations(tiledImage, useSketch);
+ }
+
+ var usedClip = false;
+ if ( tiledImage._clip ) {
+ this._saveContext(useSketch);
+
+ var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true);
+ box = box.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true));
+ var clipRect = this.viewportToDrawerRectangle(box);
+ if (sketchScale) {
+ clipRect = clipRect.times(sketchScale);
+ }
+ if (sketchTranslate) {
+ clipRect = clipRect.translate(sketchTranslate);
+ }
+ this._setClip(clipRect, useSketch);
+
+ usedClip = true;
+ }
+
+ if (tiledImage._croppingPolygons) {
+ var self = this;
+ if(!usedClip){
+ this._saveContext(useSketch);
+ }
+ try {
+ var polygons = tiledImage._croppingPolygons.map(function (polygon) {
+ return polygon.map(function (coord) {
+ var point = tiledImage
+ .imageToViewportCoordinates(coord.x, coord.y, true)
+ .rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true));
+ var clipPoint = self.viewportCoordToDrawerCoord(point);
+ if (sketchScale) {
+ clipPoint = clipPoint.times(sketchScale);
+ }
+ if (sketchTranslate) { // mostly fixes #2312
+ clipPoint = clipPoint.plus(sketchTranslate);
+ }
+ return clipPoint;
+ });
+ });
+ this._clipWithPolygons(polygons, useSketch);
+ } catch (e) {
+ $.console.error(e);
+ }
+ usedClip = true;
+ }
+ tiledImage._hasOpaqueTile = false;
+ if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) {
+ let placeholderRect = this.viewportToDrawerRectangle(tiledImage.getBoundsNoRotate(true));
+ if (sketchScale) {
+ placeholderRect = placeholderRect.times(sketchScale);
+ }
+ if (sketchTranslate) {
+ placeholderRect = placeholderRect.translate(sketchTranslate);
+ }
+
+ let fillStyle = null;
+ if ( typeof tiledImage.placeholderFillStyle === "function" ) {
+ fillStyle = tiledImage.placeholderFillStyle(tiledImage, this.context);
+ }
+ else {
+ fillStyle = tiledImage.placeholderFillStyle;
+ }
+
+ this._drawRectangle(placeholderRect, fillStyle, useSketch);
+ }
+
+ var subPixelRoundingRule = determineSubPixelRoundingRule(tiledImage.subPixelRoundingForTransparency);
+
+ var shouldRoundPositionAndSize = false;
+
+ if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) {
+ shouldRoundPositionAndSize = true;
+ } else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) {
+ var isAnimating = this.viewer && this.viewer.isAnimating();
+ shouldRoundPositionAndSize = !isAnimating;
+ }
+
+ // Iterate over the tiles to draw, and draw them
+ for (var i = 0; i < lastDrawn.length; i++) {
+ tile = lastDrawn[ i ];
+ this._drawTile( tile, tiledImage, useSketch, sketchScale,
+ sketchTranslate, shouldRoundPositionAndSize, tiledImage.source );
+
+ if( this.viewer ){
+ /**
+ * Raised when a tile is drawn to the canvas. Only valid for
+ * context2d and html drawers.
+ *
+ * @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
+ });
+ }
+ }
+
+ if ( usedClip ) {
+ this._restoreContext( useSketch );
+ }
+
+ if (!sketchScale) {
+ if (tiledImage.getRotation(true) % 360 !== 0) {
+ this._restoreRotationChanges(useSketch);
+ }
+ if (this.viewport.getRotation(true) % 360 !== 0) {
+ this._restoreRotationChanges(useSketch);
+ }
+ }
+
+ if (useSketch) {
+ if (sketchScale) {
+ this._setRotations(tiledImage);
+ }
+ this.blendSketch({
+ opacity: tiledImage.opacity,
+ scale: sketchScale,
+ translate: sketchTranslate,
+ compositeOperation: tiledImage.compositeOperation,
+ bounds: bounds
+ });
+ if (sketchScale) {
+ if (tiledImage.getRotation(true) % 360 !== 0) {
+ this._restoreRotationChanges(false);
+ }
+ if (this.viewport.getRotation(true) % 360 !== 0) {
+ this._restoreRotationChanges(false);
+ }
+ }
+ }
+
+ this._drawDebugInfo( tiledImage, lastDrawn );
+
+ // Fire tiled-image-drawn event.
+
+ this._raiseTiledImageDrawnEvent(tiledImage, lastDrawn);
+
+ }
+
+ /**
+ * Draws special debug information for a TiledImage if in debug mode.
+ * @private
+ * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame.
+ */
+ _drawDebugInfo( tiledImage, lastDrawn ) {
+ if( tiledImage.debugMode ) {
+ for ( var i = lastDrawn.length - 1; i >= 0; i-- ) {
+ var tile = lastDrawn[ i ];
+ try {
+ this._drawDebugInfoOnTile(tile, lastDrawn.length, i, tiledImage);
+ } catch(e) {
+ $.console.error(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * This function will create multiple polygon paths on the drawing context by provided polygons,
+ * then clip the context to the paths.
+ * @private
+ * @param {OpenSeadragon.Point[][]} polygons - an array of polygons. A polygon is an array of OpenSeadragon.Point
+ * @param {Boolean} useSketch - Whether to use the sketch canvas or not.
+ */
+ _clipWithPolygons (polygons, useSketch) {
+ var context = this._getContext(useSketch);
+ context.beginPath();
+ for(const polygon of polygons){
+ for(const [i, coord] of polygon.entries() ){
+ context[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y);
+ }
+ }
+
+ context.clip();
+ }
+
+ /**
+ * Draws the given tile.
+ * @private
+ * @param {OpenSeadragon.Tile} tile - The tile to draw.
+ * @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.
+ * @param {OpenSeadragon.Point} [translate] A translation vector to offset tile position
+ * @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
+ * position and size of tiles supporting alpha channel in non-transparency
+ * context.
+ * @param {OpenSeadragon.TileSource} source - The source specification of the tile.
+ */
+ _drawTile( tile, tiledImage, useSketch, scale, translate, shouldRoundPositionAndSize, source) {
+ $.console.assert(tile, '[Drawer._drawTile] tile is required');
+ $.console.assert(tiledImage, '[Drawer._drawTile] drawingHandler is required');
+
+ var context = this._getContext(useSketch);
+ scale = scale || 1;
+ this._drawTileToCanvas(tile, context, tiledImage, scale, translate, shouldRoundPositionAndSize, source);
+
+ }
+
+ /**
+ * Renders the tile in a canvas-based context.
+ * @private
+ * @function
+ * @param {OpenSeadragon.Tile} tile - the tile to draw to the canvas
+ * @param {Canvas} context
+ * @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
+ * @param {OpenSeadragon.Point} [translate] - A translation vector
+ * @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
+ * position and size of tiles supporting alpha channel in non-transparency
+ * context.
+ * @param {OpenSeadragon.TileSource} source - The source specification of the tile.
+ */
+ _drawTileToCanvas( tile, context, tiledImage, scale, translate, shouldRoundPositionAndSize, source) {
+
+ var position = tile.position.times($.pixelDensityRatio),
+ size = tile.size.times($.pixelDensityRatio),
+ rendered;
+
+ if (!tile.context2D && !tile.cacheImageRecord) {
+ $.console.warn(
+ '[Drawer._drawTileToCanvas] attempting to draw tile %s when it\'s not cached',
+ tile.toString());
+ return;
+ }
+
+ rendered = tile.getCanvasContext();
+
+ if ( !tile.loaded || !rendered ){
+ $.console.warn(
+ "Attempting to draw tile %s when it's not yet loaded.",
+ tile.toString()
+ );
+
+ return;
+ }
+
+ context.save();
+
+ if (typeof scale === 'number' && scale !== 1) {
+ // draw tile at a different scale
+ position = position.times(scale);
+ size = size.times(scale);
+ }
+
+ if (translate instanceof $.Point) {
+ // shift tile position slightly
+ position = position.plus(translate);
+ }
+
+ //if we are supposed to be rendering fully opaque rectangle,
+ //ie its done fading or fading is turned off, and if we are drawing
+ //an image with an alpha channel, then the only way
+ //to avoid seeing the tile underneath is to clear the rectangle
+ if (context.globalAlpha === 1 && tile.hasTransparency) {
+ if (shouldRoundPositionAndSize) {
+ // Round to the nearest whole pixel so we don't get seams from overlap.
+ position.x = Math.round(position.x);
+ position.y = Math.round(position.y);
+ size.x = Math.round(size.x);
+ size.y = Math.round(size.y);
+ }
+
+ //clearing only the inside of the rectangle occupied
+ //by the png prevents edge flikering
+ context.clearRect(
+ position.x,
+ position.y,
+ size.x,
+ size.y
+ );
+ }
+
+ this._raiseTileDrawingEvent(tiledImage, context, tile, rendered);
+
+ var sourceWidth, sourceHeight;
+ if (tile.sourceBounds) {
+ sourceWidth = Math.min(tile.sourceBounds.width, rendered.canvas.width);
+ sourceHeight = Math.min(tile.sourceBounds.height, rendered.canvas.height);
+ } else {
+ sourceWidth = rendered.canvas.width;
+ sourceHeight = rendered.canvas.height;
+ }
+
+ context.translate(position.x + size.x / 2, 0);
+ if (tile.flipped) {
+ context.scale(-1, 1);
+ }
+ context.drawImage(
+ rendered.canvas,
+ 0,
+ 0,
+ sourceWidth,
+ sourceHeight,
+ -size.x / 2,
+ position.y,
+ size.x,
+ size.y
+ );
+
+ context.restore();
+ }
+
+ /**
+ * Get the context of the main or sketch canvas
+ * @private
+ * @param {Boolean} useSketch
+ * @returns {CanvasRenderingContext2D}
+ */
+ _getContext( useSketch ) {
+ var context = this.context;
+ if ( useSketch ) {
+ if (this.sketchCanvas === null) {
+ this.sketchCanvas = document.createElement( "canvas" );
+ var sketchCanvasSize = this._calculateSketchCanvasSize();
+ this.sketchCanvas.width = sketchCanvasSize.x;
+ this.sketchCanvas.height = sketchCanvasSize.y;
+ this.sketchContext = this.sketchCanvas.getContext( "2d" );
+
+ // If the viewport is not currently rotated, the sketchCanvas
+ // will have the same size as the main canvas. However, if
+ // the viewport get rotated later on, we will need to resize it.
+ if (this.viewport.getRotation() === 0) {
+ var self = this;
+ this.viewer.addHandler('rotate', function resizeSketchCanvas() {
+ if (self.viewport.getRotation() === 0) {
+ return;
+ }
+ self.viewer.removeHandler('rotate', resizeSketchCanvas);
+ var sketchCanvasSize = self._calculateSketchCanvasSize();
+ self.sketchCanvas.width = sketchCanvasSize.x;
+ self.sketchCanvas.height = sketchCanvasSize.y;
+ });
+ }
+ this._updateImageSmoothingEnabled(this.sketchContext);
+ }
+ context = this.sketchContext;
+ }
+ return context;
+ }
+
+ /**
+ * Save the context of the main or sketch canvas
+ * @private
+ * @param {Boolean} useSketch
+ */
+ _saveContext( useSketch ) {
+ this._getContext( useSketch ).save();
+ }
+
+ /**
+ * Restore the context of the main or sketch canvas
+ * @private
+ * @param {Boolean} useSketch
+ */
+ _restoreContext( useSketch ) {
+ this._getContext( useSketch ).restore();
+ }
+
+ // private
+ _setClip(rect, useSketch) {
+ var context = this._getContext( useSketch );
+ context.beginPath();
+ context.rect(rect.x, rect.y, rect.width, rect.height);
+ context.clip();
+ }
+
+ // private
+ // used to draw a placeholder rectangle
+ _drawRectangle(rect, fillStyle, useSketch) {
+ var context = this._getContext( useSketch );
+ context.save();
+ context.fillStyle = fillStyle;
+ context.fillRect(rect.x, rect.y, rect.width, rect.height);
+ context.restore();
+ }
+
+ /**
+ * Blends the sketch canvas in the main canvas.
+ * @param {Object} options The options
+ * @param {Float} options.opacity The opacity of the blending.
+ * @param {Float} [options.scale=1] The scale at which tiles were drawn on
+ * the sketch. Default is 1.
+ * Use scale to draw at a lower scale and then enlarge onto the main canvas.
+ * @param {OpenSeadragon.Point} [options.translate] A translation vector
+ * that was used to draw the tiles
+ * @param {String} [options.compositeOperation] - How the image is
+ * composited onto other images; see compositeOperation in
+ * {@link OpenSeadragon.Options} for possible values.
+ * @param {OpenSeadragon.Rect} [options.bounds] The part of the sketch
+ * canvas to blend in the main canvas. If specified, options.scale and
+ * options.translate get ignored.
+ */
+ blendSketch(opacity, scale, translate, compositeOperation) {
+ var options = opacity;
+ if (!$.isPlainObject(options)) {
+ options = {
+ opacity: opacity,
+ scale: scale,
+ translate: translate,
+ compositeOperation: compositeOperation
+ };
+ }
+
+ opacity = options.opacity;
+ compositeOperation = options.compositeOperation;
+ var bounds = options.bounds;
+
+ this.context.save();
+ this.context.globalAlpha = opacity;
+ if (compositeOperation) {
+ this.context.globalCompositeOperation = compositeOperation;
+ }
+ if (bounds) {
+ // Internet Explorer, Microsoft Edge, and Safari have problems
+ // when you call context.drawImage with negative x or y
+ // or x + width or y + height greater than the canvas width or height respectively.
+ if (bounds.x < 0) {
+ bounds.width += bounds.x;
+ bounds.x = 0;
+ }
+ if (bounds.x + bounds.width > this.canvas.width) {
+ bounds.width = this.canvas.width - bounds.x;
+ }
+ if (bounds.y < 0) {
+ bounds.height += bounds.y;
+ bounds.y = 0;
+ }
+ if (bounds.y + bounds.height > this.canvas.height) {
+ bounds.height = this.canvas.height - bounds.y;
+ }
+
+ this.context.drawImage(
+ this.sketchCanvas,
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height
+ );
+ } else {
+ scale = options.scale || 1;
+ translate = options.translate;
+ var position = translate instanceof $.Point ?
+ translate : new $.Point(0, 0);
+
+ var widthExt = 0;
+ var heightExt = 0;
+ if (translate) {
+ var widthDiff = this.sketchCanvas.width - this.canvas.width;
+ var heightDiff = this.sketchCanvas.height - this.canvas.height;
+ widthExt = Math.round(widthDiff / 2);
+ heightExt = Math.round(heightDiff / 2);
+ }
+ this.context.drawImage(
+ this.sketchCanvas,
+ position.x - widthExt * scale,
+ position.y - heightExt * scale,
+ (this.canvas.width + 2 * widthExt) * scale,
+ (this.canvas.height + 2 * heightExt) * scale,
+ -widthExt,
+ -heightExt,
+ this.canvas.width + 2 * widthExt,
+ this.canvas.height + 2 * heightExt
+ );
+ }
+ this.context.restore();
+ }
+
+ // private
+ _drawDebugInfoOnTile(tile, count, i, tiledImage) {
+
+ var colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length;
+ var context = this.context;
+ context.save();
+ context.lineWidth = 2 * $.pixelDensityRatio;
+ context.font = 'small-caps bold ' + (13 * $.pixelDensityRatio) + 'px arial';
+ context.strokeStyle = this.debugGridColor[colorIndex];
+ context.fillStyle = this.debugGridColor[colorIndex];
+
+ this._setRotations(tiledImage);
+
+ if(this._viewportFlipped){
+ this._flip({point: tile.position.plus(tile.size.divide(2))});
+ }
+
+ 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 );
+ const angleInDegrees = this.viewport.getRotation(true);
+ context.rotate( Math.PI / 180 * -angleInDegrees );
+ 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();
+ }
+
+ context.restore();
+ }
+
+ // private
+ _updateImageSmoothingEnabled(context){
+ context.msImageSmoothingEnabled = this._imageSmoothingEnabled;
+ context.imageSmoothingEnabled = this._imageSmoothingEnabled;
+ }
+
+ /**
+ * Get the canvas size
+ * @private
+ * @param {Boolean} sketch If set to true return the size of the sketch canvas
+ * @returns {OpenSeadragon.Point} The size of the canvas
+ */
+ _getCanvasSize(sketch) {
+ var canvas = this._getContext(sketch).canvas;
+ return new $.Point(canvas.width, canvas.height);
+ }
+
+ /**
+ * Get the canvas center
+ * @private
+ * @param {Boolean} sketch If set to true return the center point of the sketch canvas
+ * @returns {OpenSeadragon.Point} The center point of the canvas
+ */
+ _getCanvasCenter() {
+ return new $.Point(this.canvas.width / 2, this.canvas.height / 2);
+ }
+
+ /**
+ * Set rotations for viewport & tiledImage
+ * @private
+ * @param {OpenSeadragon.TiledImage} tiledImage
+ * @param {Boolean} [useSketch=false]
+ */
+ _setRotations(tiledImage, useSketch = false) {
+ var saveContext = false;
+ if (this.viewport.getRotation(true) % 360 !== 0) {
+ this._offsetForRotation({
+ degrees: this.viewport.getRotation(true),
+ useSketch: useSketch,
+ saveContext: saveContext
+ });
+ saveContext = false;
+ }
+ if (tiledImage.getRotation(true) % 360 !== 0) {
+ this._offsetForRotation({
+ degrees: tiledImage.getRotation(true),
+ point: this.viewport.pixelFromPointNoRotate(
+ tiledImage._getRotationPoint(true), true),
+ useSketch: useSketch,
+ saveContext: saveContext
+ });
+ }
+ }
+
+ // private
+ _offsetForRotation(options) {
+ var point = options.point ?
+ options.point.times($.pixelDensityRatio) :
+ this._getCanvasCenter();
+
+ var context = this._getContext(options.useSketch);
+ context.save();
+
+ context.translate(point.x, point.y);
+ context.rotate(Math.PI / 180 * options.degrees);
+ context.translate(-point.x, -point.y);
+ }
+
+ // private
+ _flip(options) {
+ options = options || {};
+ var point = options.point ?
+ options.point.times($.pixelDensityRatio) :
+ this._getCanvasCenter();
+ var context = this._getContext(options.useSketch);
+
+ context.translate(point.x, 0);
+ context.scale(-1, 1);
+ context.translate(-point.x, 0);
+ }
+
+ // private
+ _restoreRotationChanges(useSketch) {
+ var context = this._getContext(useSketch);
+ context.restore();
+ }
+
+ // private
+ _calculateCanvasSize() {
+ var pixelDensityRatio = $.pixelDensityRatio;
+ var viewportSize = this.viewport.getContainerSize();
+ return {
+ // canvas width and height are integers
+ x: Math.round(viewportSize.x * pixelDensityRatio),
+ y: Math.round(viewportSize.y * pixelDensityRatio)
+ };
+ }
+
+ // private
+ _calculateSketchCanvasSize() {
+ var canvasSize = this._calculateCanvasSize();
+ if (this.viewport.getRotation() === 0) {
+ return canvasSize;
+ }
+ // If the viewport is rotated, we need a larger sketch canvas in order
+ // to support edge smoothing.
+ var sketchCanvasSize = Math.ceil(Math.sqrt(
+ canvasSize.x * canvasSize.x +
+ canvasSize.y * canvasSize.y));
+ return {
+ x: sketchCanvasSize,
+ y: sketchCanvasSize
+ };
+ }
+}
+$.CanvasDrawer = CanvasDrawer;
+
+
+/**
+ * Defines the value for subpixel rounding to fallback to in case of missing or
+ * invalid value.
+ * @private
+ */
+var DEFAULT_SUBPIXEL_ROUNDING_RULE = $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;
+
+/**
+ * Checks whether the input value is an invalid subpixel rounding enum value.
+ * @private
+ *
+ * @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to check.
+ * @returns {Boolean} Returns true if the input value is none of the expected
+ * {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS}, {@link SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST} or {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} value.
+ */
+function isSubPixelRoundingRuleUnknown(value) {
+ return value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS &&
+ value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST &&
+ value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;
+}
+
+/**
+ * Ensures the returned value is always a valid subpixel rounding enum value,
+ * defaulting to {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} if input is missing or invalid.
+ * @private
+ * @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to normalize.
+ * @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns a valid subpixel rounding enum value.
+ */
+function normalizeSubPixelRoundingRule(value) {
+ if (isSubPixelRoundingRuleUnknown(value)) {
+ return DEFAULT_SUBPIXEL_ROUNDING_RULE;
+ }
+ return value;
+}
+
+/**
+ * Ensures the returned value is always a valid subpixel rounding enum value,
+ * defaulting to 'NEVER' if input is missing or invalid.
+ * @private
+ *
+ * @param {Object} subPixelRoundingRules - A subpixel rounding enum values dictionary [{@link BROWSERS}] --> {@link SUBPIXEL_ROUNDING_OCCURRENCES}.
+ * @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns the determined subpixel rounding enum value for the
+ * current browser.
+ */
+function determineSubPixelRoundingRule(subPixelRoundingRules) {
+ if (typeof subPixelRoundingRules === 'number') {
+ return normalizeSubPixelRoundingRule(subPixelRoundingRules);
+ }
+
+ if (!subPixelRoundingRules || !$.Browser) {
+ return DEFAULT_SUBPIXEL_ROUNDING_RULE;
+ }
+
+ var subPixelRoundingRule = subPixelRoundingRules[$.Browser.vendor];
+
+ if (isSubPixelRoundingRuleUnknown(subPixelRoundingRule)) {
+ subPixelRoundingRule = subPixelRoundingRules['*'];
+ }
+
+ return normalizeSubPixelRoundingRule(subPixelRoundingRule);
+}
+
+}( OpenSeadragon ));
+
+
+/*
+ * OpenSeadragon - WebGLDrawer
+ *
+ * Copyright (C) 2009 CodePlex Foundation
+ * Copyright (C) 2010-2024 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( $ ){
+
+ const OpenSeadragon = $; // alias for JSDoc
+
+ /**
+ * @class OpenSeadragon.WebGLDrawer
+ * @classdesc Default implementation of WebGLDrawer for an {@link OpenSeadragon.Viewer}. The WebGLDrawer
+ * loads tile data as textures to the graphics card as soon as it is available (via the tile-ready event),
+ * and unloads the data (via the image-unloaded event). The drawer utilizes a context-dependent two pass drawing pipeline.
+ * For the first pass, tile composition for a given TiledImage is always done using a canvas with a WebGL context.
+ * This allows tiles to be stitched together without seams or artifacts, without requiring a tile source with overlap. If overlap is present,
+ * overlapping pixels are discarded. The second pass copies all pixel data from the WebGL context onto an output canvas
+ * with a Context2d context. This allows applications to have access to pixel data and other functionality provided by
+ * Context2d, regardless of whether the CanvasDrawer or the WebGLDrawer is used. Certain options, including compositeOperation,
+ * clip, croppingPolygons, and debugMode are implemented using Context2d operations; in these scenarios, each TiledImage is
+ * drawn onto the output canvas immediately after the tile composition step (pass 1). Otherwise, for efficiency, all TiledImages
+ * are copied over to the output canvas at once, after all tiles have been composited for all images.
+ * @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.
+ */
+
+ OpenSeadragon.WebGLDrawer = class WebGLDrawer extends OpenSeadragon.DrawerBase{
+ constructor(options){
+ super(options);
+
+ /**
+ * The HTML element (canvas) that this drawer uses for drawing
+ * @member {Element} canvas
+ * @memberof OpenSeadragon.WebGLDrawer#
+ */
+
+ /**
+ * The parent element of this Drawer instance, passed in when the Drawer was created.
+ * The parent of {@link OpenSeadragon.WebGLDrawer#canvas}.
+ * @member {Element} container
+ * @memberof OpenSeadragon.WebGLDrawer#
+ */
+
+ // private members
+ this._destroyed = false;
+ this._TextureMap = new Map();
+ this._TileMap = new Map();
+
+ this._gl = null;
+ this._firstPass = null;
+ this._secondPass = null;
+ this._glFrameBuffer = null;
+ this._renderToTexture = null;
+ this._glFramebufferToCanvasTransform = null;
+ this._outputCanvas = null;
+ this._outputContext = null;
+ this._clippingCanvas = null;
+ this._clippingContext = null;
+ this._renderingCanvas = null;
+ this._backupCanvasDrawer = null;
+
+ // Add listeners for events that require modifying the scene or camera
+ this._boundToTileReady = ev => this._tileReadyHandler(ev);
+ this._boundToImageUnloaded = ev => this._imageUnloadedHandler(ev);
+ this.viewer.addHandler("tile-ready", this._boundToTileReady);
+ this.viewer.addHandler("image-unloaded", this._boundToImageUnloaded);
+
+ // Reject listening for the tile-drawing and tile-drawn events, which this drawer does not fire
+ this.viewer.rejectEventHandler("tile-drawn", "The WebGLDrawer does not raise the tile-drawn event");
+ this.viewer.rejectEventHandler("tile-drawing", "The WebGLDrawer does not raise the tile-drawing event");
+
+ // this.viewer and this.canvas are part of the public DrawerBase API
+ // and are defined by the parent DrawerBase class. Additional setup is done by
+ // the private _setupCanvases and _setupRenderer functions.
+ 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._secondPass.bufferOutputPosition);
+ gl.deleteFramebuffer(this._glFrameBuffer);
+
+ // 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();
+ }
+
+ // unbind our event listeners from the viewer
+ this.viewer.removeHandler("tile-ready", this._boundToTileReady);
+ this.viewer.removeHandler("image-unloaded", this._boundToImageUnloaded);
+
+ // set our webgl context reference to null to enable garbage collection
+ this._gl = null;
+
+ if(this._backupCanvasDrawer){
+ this._backupCanvasDrawer.destroy();
+ this._backupCanvasDrawer = null;
+ }
+
+ this.container.removeChild(this.canvas);
+ if(this.viewer.drawer === this){
+ this.viewer.drawer = null;
+ }
+
+ // set our destroyed flag to true
+ this._destroyed = true;
+ }
+
+ // Public API required by all Drawer implementations
+ /**
+ *
+ * @returns {Boolean} true
+ */
+ canRotate(){
+ return true;
+ }
+
+ // Public API required by all Drawer implementations
+ /**
+ * @returns {Boolean} true if canvas and webgl are supported
+ */
+ static isSupported(){
+ let canvasElement = document.createElement( 'canvas' );
+ let webglContext = $.isFunction( canvasElement.getContext ) &&
+ canvasElement.getContext( 'webgl' );
+ let ext = webglContext && webglContext.getExtension('WEBGL_lose_context');
+ if(ext){
+ ext.loseContext();
+ }
+ return !!( webglContext );
+ }
+
+ /**
+ *
+ * @returns 'webgl'
+ */
+ getType(){
+ return 'webgl';
+ }
+
+ /**
+ * @param {TiledImage} tiledImage the tiled image that is calling the function
+ * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams.
+ * @private
+ */
+ minimumOverlapRequired(tiledImage) {
+ // return true if the tiled image is tainted, since the backup canvas drawer will be used.
+ return tiledImage.isTainted();
+ }
+
+ /**
+ * create the HTML element (canvas in this case) that the image will be drawn into
+ * @private
+ * @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;
+ }
+
+ /**
+ * Get the backup renderer (CanvasDrawer) to use if data cannot be used by webgl
+ * Lazy loaded
+ * @private
+ * @returns {CanvasDrawer}
+ */
+ _getBackupCanvasDrawer(){
+ if(!this._backupCanvasDrawer){
+ this._backupCanvasDrawer = this.viewer.requestDrawer('canvas', {mainDrawer: false});
+ this._backupCanvasDrawer.canvas.style.setProperty('visibility', 'hidden');
+ }
+
+ return this._backupCanvasDrawer;
+ }
+
+ /**
+ *
+ * @param {Array} tiledImages Array of TiledImage objects to draw
+ */
+ draw(tiledImages){
+ let gl = this._gl;
+ let view = {
+ 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(-view.center.x, -view.center.y);
+ let scaleMatrix = $.Mat3.makeScaling(2 / view.bounds.width * flipMultiplier, -2 / view.bounds.height);
+ let rotMatrix = $.Mat3.makeRotation(-view.rotation);
+ let viewMatrix = scaleMatrix.multiply(rotMatrix).multiply(posMatrix);
+
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer
+
+ // clear the output canvas
+ this._outputContext.clearRect(0, 0, this._outputCanvas.width, this._outputCanvas.height);
+
+
+ let renderingBufferHasImageData = false;
+
+ //iterate over tiled images and draw each one using a two-pass rendering pipeline if needed
+ tiledImages.forEach( (tiledImage, tiledImageIndex) => {
+
+ if(tiledImage.isTainted()){
+ // first, draw any data left in the rendering buffer onto the output canvas
+ if(renderingBufferHasImageData){
+ this._outputContext.drawImage(this._renderingCanvas, 0, 0);
+ // clear the buffer
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer
+ renderingBufferHasImageData = false;
+ }
+
+ // next, use the backup canvas drawer to draw this tainted image
+ const canvasDrawer = this._getBackupCanvasDrawer();
+ canvasDrawer.draw([tiledImage]);
+ this._outputContext.drawImage(canvasDrawer.canvas, 0, 0);
+
+ } else {
+ let tilesToDraw = tiledImage.getTilesToDraw();
+
+ if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) {
+ this._drawPlaceholder(tiledImage);
+ }
+
+ if(tilesToDraw.length === 0 || tiledImage.getOpacity() === 0){
+ return;
+ }
+ let firstTile = tilesToDraw[0];
+
+ let useContext2dPipeline = ( tiledImage.compositeOperation ||
+ this.viewer.compositeOperation ||
+ tiledImage._clip ||
+ tiledImage._croppingPolygons ||
+ tiledImage.debugMode
+ );
+
+ let useTwoPassRendering = useContext2dPipeline || (tiledImage.opacity < 1) || firstTile.hasTransparency;
+
+ // using the context2d pipeline requires a clean rendering (back) buffer to start
+ if(useContext2dPipeline){
+ // if the rendering buffer has image data currently, write it to the output canvas now and clear it
+
+ if(renderingBufferHasImageData){
+ this._outputContext.drawImage(this._renderingCanvas, 0, 0);
+ }
+
+ // clear the buffer
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer
+ }
+
+ // First rendering pass: compose tiles that make up this tiledImage
+ gl.useProgram(this._firstPass.shaderProgram);
+
+ // bind to the framebuffer for render-to-texture if using two-pass rendering, otherwise back buffer (null)
+ if(useTwoPassRendering){
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
+ // clear the buffer to draw a new image
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ } else {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ // no need to clear, just draw on top of the existing pixels
+ }
+
+ let overallMatrix = viewMatrix;
+
+ let imageRotation = tiledImage.getRotation(true);
+ // if needed, handle the tiledImage being rotated
+ if( imageRotation % 360 !== 0){
+ let imageRotationMatrix = $.Mat3.makeRotation(-imageRotation * Math.PI / 180);
+ let imageCenter = tiledImage.getBoundsNoRotate(true).getCenter();
+ let t1 = $.Mat3.makeTranslation(imageCenter.x, imageCenter.y);
+ let t2 = $.Mat3.makeTranslation(-imageCenter.x, -imageCenter.y);
+
+ // update the view matrix to account for this image's rotation
+ let localMatrix = t1.multiply(imageRotationMatrix).multiply(t2);
+ overallMatrix = viewMatrix.multiply(localMatrix);
+ }
+
+ let maxTextures = this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS);
+ if(maxTextures <= 0){
+ // This can apparently happen on some systems if too many WebGL contexts have been created
+ // in which case maxTextures can be null, leading to out of bounds errors with the array.
+ // For example, when viewers were created and not destroyed in the test suite, this error
+ // occurred in the TravisCI tests, though it did not happen when testing locally either in
+ // a browser or on the command line via grunt test.
+
+ throw(new Error(`WegGL error: bad value for gl parameter MAX_TEXTURE_IMAGE_UNITS (${maxTextures}). This could happen
+ if too many contexts have been created and not released, or there is another problem with the graphics card.`));
+ }
+
+ let texturePositionArray = new Float32Array(maxTextures * 12); // 6 vertices (2 triangles) x 2 coordinates per vertex
+ let textureDataArray = new Array(maxTextures);
+ let matrixArray = new Array(maxTextures);
+ let opacityArray = new Array(maxTextures);
+
+ // iterate over tiles and add data for each one to the buffers
+ for(let tileIndex = 0; tileIndex < tilesToDraw.length; tileIndex++){
+ let tile = tilesToDraw[tileIndex].tile;
+ let indexInDrawArray = tileIndex % maxTextures;
+ let numTilesToDraw = indexInDrawArray + 1;
+ let tileContext = tile.getCanvasContext();
+
+ let textureInfo = tileContext ? this._TextureMap.get(tileContext.canvas) : null;
+ if(!textureInfo){
+ // tile was not processed in the tile-ready event (this can happen
+ // if this drawer was created after the tile was downloaded)
+ this._tileReadyHandler({tile: tile, tiledImage: tiledImage});
+
+ // retry getting textureInfo
+ textureInfo = tileContext ? this._TextureMap.get(tileContext.canvas) : null;
+ }
+
+ if(textureInfo){
+ this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray);
+ } else {
+ // console.log('No tile info', tile);
+ }
+ if( (numTilesToDraw === maxTextures) || (tileIndex === tilesToDraw.length - 1)){
+ // We've filled up the buffers: time to draw this set of tiles
+
+ // bind each tile's texture to the appropriate gl.TEXTURE#
+ for(let i = 0; i <= numTilesToDraw; i++){
+ gl.activeTexture(gl.TEXTURE0 + i);
+ gl.bindTexture(gl.TEXTURE_2D, textureDataArray[i]);
+ }
+
+ // set the buffer data for the texture coordinates to use for each tile
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferTexturePosition);
+ gl.bufferData(gl.ARRAY_BUFFER, texturePositionArray, gl.DYNAMIC_DRAW);
+
+ // set the transform matrix uniform for each tile
+ matrixArray.forEach( (matrix, index) => {
+ gl.uniformMatrix3fv(this._firstPass.uTransformMatrices[index], false, matrix);
+ });
+ // set the opacity uniform for each tile
+ gl.uniform1fv(this._firstPass.uOpacities, new Float32Array(opacityArray));
+
+ // bind vertex buffers and (re)set attributes before calling gl.drawArrays()
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferOutputPosition);
+ gl.vertexAttribPointer(this._firstPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferTexturePosition);
+ gl.vertexAttribPointer(this._firstPass.aTexturePosition, 2, gl.FLOAT, false, 0, 0);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferIndex);
+ gl.vertexAttribPointer(this._firstPass.aIndex, 1, gl.FLOAT, false, 0, 0);
+
+ // Draw! 6 vertices per tile (2 triangles per rectangle)
+ gl.drawArrays(gl.TRIANGLES, 0, 6 * numTilesToDraw );
+ }
+ }
+
+ if(useTwoPassRendering){
+ // Second rendering pass: Render the tiled image from the framebuffer into the back buffer
+ gl.useProgram(this._secondPass.shaderProgram);
+
+ // set the rendering target to the back buffer (null)
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+
+ // bind the rendered texture from the first pass to use during this second pass
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture);
+
+ // set opacity to the value for the current tiledImage
+ this._gl.uniform1f(this._secondPass.uOpacityMultiplier, tiledImage.opacity);
+
+ // bind buffers and set attributes before calling gl.drawArrays
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferTexturePosition);
+ gl.vertexAttribPointer(this._secondPass.aTexturePosition, 2, gl.FLOAT, false, 0, 0);
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferOutputPosition);
+ gl.vertexAttribPointer(this._firstPass.aOutputPosition, 2, gl.FLOAT, false, 0, 0);
+
+ // Draw the quad (two triangles)
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
+
+ }
+
+ renderingBufferHasImageData = true;
+
+ if(useContext2dPipeline){
+ // draw from the rendering canvas onto the output canvas, clipping/cropping if needed.
+ this._applyContext2dPipeline(tiledImage, tilesToDraw, tiledImageIndex);
+ renderingBufferHasImageData = false;
+ // clear the buffer
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer
+ }
+
+ // after drawing the first TiledImage, fire the tiled-image-drawn event (for testing)
+ if(tiledImageIndex === 0){
+ this._raiseTiledImageDrawnEvent(tiledImage, tilesToDraw.map(info=>info.tile));
+ }
+ }
+
+
+
+ });
+
+ if(renderingBufferHasImageData){
+ this._outputContext.drawImage(this._renderingCanvas, 0, 0);
+ }
+
+ }
+
+ // Public API required by all Drawer implementations
+ /**
+ * Required by DrawerBase, but has no effect on WebGLDrawer.
+ * @param {Boolean} enabled
+ */
+ setImageSmoothingEnabled(enabled){
+ // noop - this property does not impact WebGLDrawer
+ }
+
+ /**
+ * Draw a rect onto the output canvas for debugging purposes
+ * @param {OpenSeadragon.Rect} rect
+ */
+ drawDebuggingRect(rect){
+ let context = this._outputContext;
+ context.save();
+ context.lineWidth = 2 * $.pixelDensityRatio;
+ context.strokeStyle = this.debugGridColor[0];
+ context.fillStyle = this.debugGridColor[0];
+
+ context.strokeRect(
+ rect.x * $.pixelDensityRatio,
+ rect.y * $.pixelDensityRatio,
+ rect.width * $.pixelDensityRatio,
+ rect.height * $.pixelDensityRatio
+ );
+
+ context.restore();
+ }
+
+ // private
+ _getTextureDataFromTile(tile){
+ return tile.getCanvasContext().canvas;
+ }
+
+ /**
+ * Draw data from the rendering canvas onto the output canvas, with clipping,
+ * cropping and/or debug info as requested.
+ * @private
+ * @param {OpenSeadragon.TiledImage} tiledImage - the tiledImage to draw
+ * @param {Array} tilesToDraw - array of objects containing tiles that were drawn
+ */
+ _applyContext2dPipeline(tiledImage, tilesToDraw, tiledImageIndex){
+ // composite onto the output canvas, clipping if necessary
+ this._outputContext.save();
+
+ // set composite operation; ignore for first image drawn
+ this._outputContext.globalCompositeOperation = tiledImageIndex === 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){
+ const flipped = this.viewer.viewport.getFlip();
+ if(flipped){
+ this._flip();
+ }
+ this._drawDebugInfo(tilesToDraw, tiledImage, flipped);
+ if(flipped){
+ this._flip();
+ }
+ }
+
+
+ }
+
+ // private
+ _getTileData(tile, tiledImage, textureInfo, viewMatrix, index, texturePositionArray, textureDataArray, matrixArray, opacityArray){
+
+ let texture = textureInfo.texture;
+ let textureQuad = textureInfo.position;
+
+ // set the position of this texture
+ texturePositionArray.set(textureQuad, index * 12);
+
+ // compute offsets that account for tile overlap; needed for calculating the transform matrix appropriately
+ let overlapFraction = this._calculateOverlapFraction(tile, tiledImage);
+ let xOffset = tile.positionedBounds.width * overlapFraction.x;
+ let yOffset = tile.positionedBounds.height * overlapFraction.y;
+
+ // x, y, w, h in viewport coords
+ let x = tile.positionedBounds.x + (tile.x === 0 ? 0 : xOffset);
+ let y = tile.positionedBounds.y + (tile.y === 0 ? 0 : yOffset);
+ let right = tile.positionedBounds.x + tile.positionedBounds.width - (tile.isRightMost ? 0 : xOffset);
+ let bottom = tile.positionedBounds.y + tile.positionedBounds.height - (tile.isBottomMost ? 0 : yOffset);
+ let w = right - x;
+ let h = bottom - y;
+
+ let matrix = new $.Mat3([
+ w, 0, 0,
+ 0, h, 0,
+ x, y, 1,
+ ]);
+
+ if(tile.flipped){
+ // flip the tile around the center of the unit quad
+ let t1 = $.Mat3.makeTranslation(0.5, 0);
+ let t2 = $.Mat3.makeTranslation(-0.5, 0);
+
+ // update the view matrix to account for this image's rotation
+ let localMatrix = t1.multiply($.Mat3.makeScaling(-1, 1)).multiply(t2);
+ matrix = matrix.multiply(localMatrix);
+ }
+
+ let overallMatrix = viewMatrix.multiply(matrix);
+
+ opacityArray[index] = tile.opacity;
+ textureDataArray[index] = texture;
+ matrixArray[index] = overallMatrix.values;
+
+ }
+
+ // private
+ _setupRenderer(){
+ let gl = this._gl;
+ if(!gl){
+ $.console.error('_setupCanvases must be called before _setupRenderer');
+ }
+ this._unitQuad = this._makeQuadVertexBuffer(0, 1, 0, 1); // used a few places; create once and store the result
+
+ this._makeFirstPassShaderProgram();
+ this._makeSecondPassShaderProgram();
+
+ // set up the texture to render to in the first pass, and which will be used for rendering the second pass
+ this._renderToTexture = gl.createTexture();
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this._renderingCanvas.width, this._renderingCanvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+
+ // set up the framebuffer for render-to-texture
+ this._glFrameBuffer = gl.createFramebuffer();
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
+ gl.framebufferTexture2D(
+ gl.FRAMEBUFFER,
+ gl.COLOR_ATTACHMENT0, // attach texture as COLOR_ATTACHMENT0
+ gl.TEXTURE_2D, // attach a 2D texture
+ this._renderToTexture, // the texture to attach
+ 0
+ );
+
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+ }
+
+ //private
+ _makeFirstPassShaderProgram(){
+ let numTextures = this._glNumTextures = this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS);
+ let makeMatrixUniforms = () => {
+ return [...Array(numTextures).keys()].map(index => `uniform mat3 u_matrix_${index};`).join('\n');
+ };
+ let makeConditionals = () => {
+ return [...Array(numTextures).keys()].map(index => `${index > 0 ? 'else ' : ''}if(int(a_index) == ${index}) { transform_matrix = u_matrix_${index}; }`).join('\n');
+ };
+
+ const vertexShaderProgram = `
+ attribute vec2 a_output_position;
+ attribute vec2 a_texture_position;
+ attribute float a_index;
+
+ ${makeMatrixUniforms()} // create a uniform mat3 for each potential tile to draw
+
+ varying vec2 v_texture_position;
+ varying float v_image_index;
+
+ void main() {
+
+ mat3 transform_matrix; // value will be set by the if/elses in makeConditional()
+
+ ${makeConditionals()}
+
+ gl_Position = vec4(transform_matrix * vec3(a_output_position, 1), 1);
+
+ v_texture_position = a_texture_position;
+ v_image_index = a_index;
+ }
+ `;
+
+ const fragmentShaderProgram = `
+ precision mediump float;
+
+ // our textures
+ uniform sampler2D u_images[${numTextures}];
+ // our opacities
+ uniform float u_opacities[${numTextures}];
+
+ // the varyings passed in from the vertex shader.
+ varying vec2 v_texture_position;
+ varying float v_image_index;
+
+ void main() {
+ // can't index directly with a variable, need to use a loop iterator hack
+ for(int i = 0; i < ${numTextures}; ++i){
+ if(i == int(v_image_index)){
+ gl_FragColor = texture2D(u_images[i], v_texture_position) * u_opacities[i];
+ }
+ }
+ }
+ `;
+
+ let gl = this._gl;
+
+ let program = this.constructor.initShaderProgram(gl, vertexShaderProgram, fragmentShaderProgram);
+ gl.useProgram(program);
+
+ // get locations of attributes and uniforms, and create buffers for each attribute
+ this._firstPass = {
+ shaderProgram: program,
+ aOutputPosition: gl.getAttribLocation(program, 'a_output_position'),
+ aTexturePosition: gl.getAttribLocation(program, 'a_texture_position'),
+ aIndex: gl.getAttribLocation(program, 'a_index'),
+ uTransformMatrices: [...Array(this._glNumTextures).keys()].map(i=>gl.getUniformLocation(program, `u_matrix_${i}`)),
+ uImages: gl.getUniformLocation(program, 'u_images'),
+ uOpacities: gl.getUniformLocation(program, 'u_opacities'),
+ bufferOutputPosition: gl.createBuffer(),
+ bufferTexturePosition: gl.createBuffer(),
+ bufferIndex: gl.createBuffer(),
+ };
+
+ gl.uniform1iv(this._firstPass.uImages, [...Array(numTextures).keys()]);
+
+ // provide coordinates for the rectangle in output space, i.e. a unit quad for each one.
+ let outputQuads = new Float32Array(numTextures * 12);
+ for(let i = 0; i < numTextures; ++i){
+ outputQuads.set(Float32Array.from(this._unitQuad), i * 12);
+ }
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferOutputPosition);
+ gl.bufferData(gl.ARRAY_BUFFER, outputQuads, gl.STATIC_DRAW); // bind data statically here, since it's unchanging
+ gl.enableVertexAttribArray(this._firstPass.aOutputPosition);
+
+ // provide texture coordinates for the rectangle in image (texture) space. Data will be set later.
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferTexturePosition);
+ gl.enableVertexAttribArray(this._firstPass.aTexturePosition);
+
+ // for each vertex, provide an index into the array of textures/matrices to use for the correct tile
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._firstPass.bufferIndex);
+ let indices = [...Array(this._glNumTextures).keys()].map(i => Array(6).fill(i)).flat(); // repeat each index 6 times, for the 6 vertices per tile (2 triangles)
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(indices), gl.STATIC_DRAW); // bind data statically here, since it's unchanging
+ gl.enableVertexAttribArray(this._firstPass.aIndex);
+
+ }
+
+ // private
+ _makeSecondPassShaderProgram(){
+ const vertexShaderProgram = `
+ attribute vec2 a_output_position;
+ attribute vec2 a_texture_position;
+
+ uniform mat3 u_matrix;
+
+ varying vec2 v_texture_position;
+
+ void main() {
+ gl_Position = vec4(u_matrix * vec3(a_output_position, 1), 1);
+
+ v_texture_position = a_texture_position;
+ }
+ `;
+
+ const fragmentShaderProgram = `
+ precision mediump float;
+
+ // our texture
+ uniform sampler2D u_image;
+
+ // the texCoords passed in from the vertex shader.
+ varying vec2 v_texture_position;
+
+ // the opacity multiplier for the image
+ uniform float u_opacity_multiplier;
+
+ void main() {
+ gl_FragColor = texture2D(u_image, v_texture_position);
+ gl_FragColor *= u_opacity_multiplier;
+ }
+ `;
+
+ let gl = this._gl;
+
+ let program = this.constructor.initShaderProgram(gl, vertexShaderProgram, fragmentShaderProgram);
+ gl.useProgram(program);
+
+ // get locations of attributes and uniforms, and create buffers for each attribute
+ this._secondPass = {
+ shaderProgram: program,
+ aOutputPosition: gl.getAttribLocation(program, 'a_output_position'),
+ aTexturePosition: gl.getAttribLocation(program, 'a_texture_position'),
+ uMatrix: gl.getUniformLocation(program, 'u_matrix'),
+ uImage: gl.getUniformLocation(program, 'u_image'),
+ uOpacityMultiplier: gl.getUniformLocation(program, 'u_opacity_multiplier'),
+ bufferOutputPosition: gl.createBuffer(),
+ bufferTexturePosition: gl.createBuffer(),
};
+
+
+ // provide coordinates for the rectangle in output space, i.e. a unit quad for each one.
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferOutputPosition);
+ gl.bufferData(gl.ARRAY_BUFFER, this._unitQuad, gl.STATIC_DRAW); // bind data statically here since it's unchanging
+ gl.enableVertexAttribArray(this._secondPass.aOutputPosition);
+
+ // provide texture coordinates for the rectangle in image (texture) space.
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._secondPass.bufferTexturePosition);
+ gl.bufferData(gl.ARRAY_BUFFER, this._unitQuad, gl.DYNAMIC_DRAW); // bind data statically here since it's unchanging
+ gl.enableVertexAttribArray(this._secondPass.aTexturePosition);
+
+ // set the matrix that transforms the framebuffer to clip space
+ let matrix = $.Mat3.makeScaling(2, 2).multiply($.Mat3.makeTranslation(-0.5, -0.5));
+ gl.uniformMatrix3fv(this._secondPass.uMatrix, false, matrix.values);
}
- if (!this.useCanvas || !this.sketchCanvas) {
- return;
+
+ // private
+ _resizeRenderer(){
+ let gl = this._gl;
+ let w = this._renderingCanvas.width;
+ let h = this._renderingCanvas.height;
+ gl.viewport(0, 0, w, h);
+
+ //release the old texture
+ gl.deleteTexture(this._renderToTexture);
+ //create a new texture and set it up
+ this._renderToTexture = gl.createTexture();
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+
+ //bind the frame buffer to the new texture
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer);
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._renderToTexture, 0);
+
}
- opacity = options.opacity;
- compositeOperation = options.compositeOperation;
- var bounds = options.bounds;
- this.context.save();
- this.context.globalAlpha = opacity;
- if (compositeOperation) {
- this.context.globalCompositeOperation = compositeOperation;
+ // private
+ _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;
+
+ // important - update the size of the rendering viewport!
+ _this._resizeRenderer();
+ });
}
- if (bounds) {
- // Internet Explorer, Microsoft Edge, and Safari have problems
- // when you call context.drawImage with negative x or y
- // or x + width or y + height greater than the canvas width or height respectively.
- if (bounds.x < 0) {
- bounds.width += bounds.x;
- bounds.x = 0;
+
+ // private
+ _makeQuadVertexBuffer(left, right, top, bottom){
+ return new Float32Array([
+ left, bottom,
+ right, bottom,
+ left, top,
+ left, top,
+ right, bottom,
+ right, top]);
+ }
+
+ // private
+ _tileReadyHandler(event){
+ let tile = event.tile;
+ let tiledImage = event.tiledImage;
+
+ // If a tiledImage is already known to be tainted, don't try to upload any
+ // textures to webgl, because they won't be used even if it succeeds
+ if(tiledImage.isTainted()){
+ return;
}
- if (bounds.x + bounds.width > this.canvas.width) {
- bounds.width = this.canvas.width - bounds.x;
+
+ let tileContext = tile.getCanvasContext();
+ let canvas = tileContext && tileContext.canvas;
+ // if the tile doesn't provide a canvas, or is tainted by cross-origin
+ // data, marked the TiledImage as tainted so the canvas drawer can be
+ // used instead, and return immediately - tainted data cannot be uploaded to webgl
+ if(!canvas || $.isCanvasTainted(canvas)){
+ const wasTainted = tiledImage.isTainted();
+ if(!wasTainted){
+ tiledImage.setTainted(true);
+ $.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?');
+ this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.');
+ }
+ return;
}
- if (bounds.y < 0) {
- bounds.height += bounds.y;
- bounds.y = 0;
+
+ let textureInfo = this._TextureMap.get(canvas);
+
+ // if this is a new image for us, create a texture
+ if(!textureInfo){
+ let gl = this._gl;
+
+ // create a gl Texture for this tile and bind the canvas with the image data
+ let texture = gl.createTexture();
+ let position;
+ let overlap = tiledImage.source.tileOverlap;
+
+ // deal with tiles where there is padding, i.e. the pixel data doesn't take up the entire provided canvas
+ let sourceWidthFraction, sourceHeightFraction;
+ if (tile.sourceBounds) {
+ sourceWidthFraction = Math.min(tile.sourceBounds.width, canvas.width) / canvas.width;
+ sourceHeightFraction = Math.min(tile.sourceBounds.height, canvas.height) / canvas.height;
+ } else {
+ sourceWidthFraction = 1;
+ sourceHeightFraction = 1;
+ }
+
+ if( overlap > 0){
+ // calculate the normalized position of the rect to actually draw
+ // discarding overlap.
+ let overlapFraction = this._calculateOverlapFraction(tile, tiledImage);
+
+ let left = (tile.x === 0 ? 0 : overlapFraction.x) * sourceWidthFraction;
+ let top = (tile.y === 0 ? 0 : overlapFraction.y) * sourceHeightFraction;
+ let right = (tile.isRightMost ? 1 : 1 - overlapFraction.x) * sourceWidthFraction;
+ let bottom = (tile.isBottomMost ? 1 : 1 - overlapFraction.y) * sourceHeightFraction;
+ position = this._makeQuadVertexBuffer(left, right, top, bottom);
+ } else if (sourceWidthFraction === 1 && sourceHeightFraction === 1) {
+ // no overlap and no padding: this texture can use the unit quad as its position data
+ position = this._unitQuad;
+ } else {
+ position = this._makeQuadVertexBuffer(0, sourceWidthFraction, 0, sourceHeightFraction);
+ }
+
+ let textureInfo = {
+ texture: texture,
+ position: position,
+ };
+
+ // add it to our _TextureMap
+ this._TextureMap.set(canvas, textureInfo);
+ gl.activeTexture(gl.TEXTURE0);
+ 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);
+
}
- if (bounds.y + bounds.height > this.canvas.height) {
- bounds.height = this.canvas.height - bounds.y;
+
+ }
+
+ // private
+ _calculateOverlapFraction(tile, tiledImage){
+ let overlap = tiledImage.source.tileOverlap;
+ let nativeWidth = tile.sourceBounds.width; // in pixels
+ let nativeHeight = tile.sourceBounds.height; // in pixels
+ let overlapWidth = (tile.x === 0 ? 0 : overlap) + (tile.isRightMost ? 0 : overlap); // in pixels
+ let overlapHeight = (tile.y === 0 ? 0 : overlap) + (tile.isBottomMost ? 0 : overlap); // in pixels
+ let widthOverlapFraction = overlap / (nativeWidth + overlapWidth); // as a fraction of image including overlap
+ let heightOverlapFraction = overlap / (nativeHeight + overlapHeight); // as a fraction of image including overlap
+ return {
+ x: widthOverlapFraction,
+ y: heightOverlapFraction
+ };
+ }
+
+ // private
+ _uploadImageData(tileContext){
+
+ let gl = this._gl;
+ let canvas = tileContext.canvas;
+
+ try{
+ if(!canvas){
+ throw('Tile context does not have a canvas', tileContext);
+ }
+ // This depends on gl.TEXTURE_2D being bound to the texture
+ // associated with this canvas before calling this function
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
+ } catch (e){
+ $.console.error('Error uploading image data to WebGL', e);
}
+ }
- this.context.drawImage(
- this.sketchCanvas,
- bounds.x,
- bounds.y,
- bounds.width,
- bounds.height,
- bounds.x,
- bounds.y,
- bounds.width,
- bounds.height
- );
- } else {
- scale = options.scale || 1;
- translate = options.translate;
- var position = translate instanceof $.Point ?
- translate : new $.Point(0, 0);
+ // private
+ _imageUnloadedHandler(event){
+ let canvas = event.context2D.canvas;
+ this._cleanupImageData(canvas);
+ }
- var widthExt = 0;
- var heightExt = 0;
- if (translate) {
- var widthDiff = this.sketchCanvas.width - this.canvas.width;
- var heightDiff = this.sketchCanvas.height - this.canvas.height;
- widthExt = Math.round(widthDiff / 2);
- heightExt = Math.round(heightDiff / 2);
+ // private
+ _cleanupImageData(tileCanvas){
+ let textureInfo = this._TextureMap.get(tileCanvas);
+ //remove from the map
+ this._TextureMap.delete(tileCanvas);
+
+ //release the texture from the GPU
+ if(textureInfo){
+ this._gl.deleteTexture(textureInfo.texture);
}
- this.context.drawImage(
- this.sketchCanvas,
- position.x - widthExt * scale,
- position.y - heightExt * scale,
- (this.canvas.width + 2 * widthExt) * scale,
- (this.canvas.height + 2 * heightExt) * scale,
- -widthExt,
- -heightExt,
- this.canvas.width + 2 * widthExt,
- this.canvas.height + 2 * heightExt
- );
+
}
- this.context.restore();
- },
- // private
- drawDebugInfo: function(tile, count, i, tiledImage) {
- if ( !this.useCanvas ) {
- return;
+ // private
+ _setClip(){
+ // no-op: called by _renderToClippingCanvas when tiledImage._clip is truthy
+ // so that tests will pass.
}
- var colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length;
- var context = this.context;
- context.save();
- context.lineWidth = 2 * $.pixelDensityRatio;
- context.font = 'small-caps bold ' + (13 * $.pixelDensityRatio) + 'px arial';
- context.strokeStyle = this.debugGridColor[colorIndex];
- context.fillStyle = this.debugGridColor[colorIndex];
+ // private
+ _renderToClippingCanvas(item){
+
+ this._clippingContext.clearRect(0, 0, this._clippingCanvas.width, this._clippingCanvas.height);
+ this._clippingContext.save();
+ if(this.viewer.viewport.getFlip()){
+ const point = new $.Point(this.canvas.width / 2, this.canvas.height / 2);
+ this._clippingContext.translate(point.x, 0);
+ this._clippingContext.scale(-1, 1);
+ this._clippingContext.translate(-point.x, 0);
+ }
+
+ if(item._clip){
+ const polygon = [
+ {x: item._clip.x, y: item._clip.y},
+ {x: item._clip.x + item._clip.width, y: item._clip.y},
+ {x: item._clip.x + item._clip.width, y: item._clip.y + item._clip.height},
+ {x: item._clip.x, y: item._clip.y + item._clip.height},
+ ];
+ let clipPoints = polygon.map(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();
+ clipPoints.forEach( (coord, i) => {
+ this._clippingContext[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y);
+ });
+ this._clippingContext.clip();
+ this._setClip();
+ }
+ if(item._croppingPolygons){
+ let polygons = item._croppingPolygons.map(polygon => {
+ return polygon.map(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((polygon) => {
+ polygon.forEach( (coord, i) => {
+ this._clippingContext[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y);
+ });
+ });
+ this._clippingContext.clip();
+ }
- if (this.viewport.getRotation(true) % 360 !== 0 ) {
- this._offsetForRotation({degrees: this.viewport.getRotation(true)});
+ if(this.viewer.viewport.getFlip()){
+ const point = new $.Point(this.canvas.width / 2, this.canvas.height / 2);
+ this._clippingContext.translate(point.x, 0);
+ this._clippingContext.scale(-1, 1);
+ this._clippingContext.translate(-point.x, 0);
+ }
+
+ this._clippingContext.drawImage(this._renderingCanvas, 0, 0);
+
+ this._clippingContext.restore();
}
- if (tiledImage.getRotation(true) % 360 !== 0) {
- this._offsetForRotation({
- degrees: tiledImage.getRotation(true),
- point: tiledImage.viewport.pixelFromPointNoRotate(
- tiledImage._getRotationPoint(true), true)
- });
+
+ /**
+ * Set rotations for viewport & tiledImage
+ * @private
+ * @param {OpenSeadragon.TiledImage} tiledImage
+ */
+ _setRotations(tiledImage) {
+ var saveContext = false;
+ if (this.viewport.getRotation(true) % 360 !== 0) {
+ this._offsetForRotation({
+ degrees: this.viewport.getRotation(true),
+ saveContext: saveContext
+ });
+ saveContext = false;
+ }
+ if (tiledImage.getRotation(true) % 360 !== 0) {
+ this._offsetForRotation({
+ degrees: tiledImage.getRotation(true),
+ point: this.viewport.pixelFromPointNoRotate(
+ tiledImage._getRotationPoint(true), true),
+ saveContext: saveContext
+ });
+ }
+ }
+
+ // private
+ _offsetForRotation(options) {
+ var point = options.point ?
+ options.point.times($.pixelDensityRatio) :
+ this._getCanvasCenter();
+
+ var context = this._outputContext;
+ context.save();
+
+ context.translate(point.x, point.y);
+ context.rotate(Math.PI / 180 * options.degrees);
+ context.translate(-point.x, -point.y);
}
- if (tiledImage.viewport.getRotation(true) % 360 === 0 &&
- tiledImage.getRotation(true) % 360 === 0) {
- if(tiledImage._drawer.viewer.viewport.getFlip()) {
- tiledImage._drawer._flip();
+
+ // private
+ _flip(options) {
+ options = options || {};
+ var point = options.point ?
+ options.point.times($.pixelDensityRatio) :
+ this._getCanvasCenter();
+ var context = this._outputContext;
+
+ context.translate(point.x, 0);
+ context.scale(-1, 1);
+ context.translate(-point.x, 0);
+ }
+
+ // private
+ _drawDebugInfo( tilesToDraw, tiledImage, flipped ) {
+
+ for ( var i = tilesToDraw.length - 1; i >= 0; i-- ) {
+ var tile = tilesToDraw[ i ].tile;
+ try {
+ this._drawDebugInfoOnTile(tile, tilesToDraw.length, i, tiledImage, flipped);
+ } catch(e) {
+ $.console.error(e);
+ }
}
}
- context.strokeRect(
- tile.position.x * $.pixelDensityRatio,
- tile.position.y * $.pixelDensityRatio,
- tile.size.x * $.pixelDensityRatio,
- tile.size.y * $.pixelDensityRatio
- );
+ // private
+ _drawDebugInfoOnTile(tile, count, i, tiledImage, flipped) {
- var tileCenterX = (tile.position.x + (tile.size.x / 2)) * $.pixelDensityRatio;
- var tileCenterY = (tile.position.y + (tile.size.y / 2)) * $.pixelDensityRatio;
+ var colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length;
+ var context = this.context;
+ context.save();
+ context.lineWidth = 2 * $.pixelDensityRatio;
+ context.font = 'small-caps bold ' + (13 * $.pixelDensityRatio) + 'px arial';
+ context.strokeStyle = this.debugGridColor[colorIndex];
+ context.fillStyle = this.debugGridColor[colorIndex];
- // Rotate the text the right way around.
- context.translate( tileCenterX, tileCenterY );
- context.rotate( Math.PI / 180 * -this.viewport.getRotation(true) );
- context.translate( -tileCenterX, -tileCenterY );
+ this._setRotations(tiledImage);
- if( tile.x === 0 && tile.y === 0 ){
- context.fillText(
- "Zoom: " + this.viewport.getZoom(),
+ if(flipped){
+ this._flip({point: tile.position.plus(tile.size.divide(2))});
+ }
+
+ context.strokeRect(
tile.position.x * $.pixelDensityRatio,
- (tile.position.y - 30) * $.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 );
+ const angleInDegrees = this.viewport.getRotation(true);
+ context.rotate( Math.PI / 180 * -angleInDegrees );
+ 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(
- "Pan: " + this.viewport.getBounds().toString(),
- tile.position.x * $.pixelDensityRatio,
- (tile.position.y - 20) * $.pixelDensityRatio
+ "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
);
- }
- 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 (this.viewport.getRotation(true) % 360 !== 0 ) {
+ this._restoreRotationChanges();
+ }
+ if (tiledImage.getRotation(true) % 360 !== 0) {
+ this._restoreRotationChanges();
+ }
+
+ context.restore();
}
- if (tiledImage.viewport.getRotation(true) % 360 === 0 &&
- tiledImage.getRotation(true) % 360 === 0) {
- if(tiledImage._drawer.viewer.viewport.getFlip()) {
- tiledImage._drawer._flip();
+ _drawPlaceholder(tiledImage){
+
+ const bounds = tiledImage.getBounds(true);
+ const rect = this.viewportToDrawerRectangle(tiledImage.getBounds(true));
+ const context = this._outputContext;
+
+ let fillStyle;
+ if ( typeof tiledImage.placeholderFillStyle === "function" ) {
+ fillStyle = tiledImage.placeholderFillStyle(tiledImage, context);
+ }
+ else {
+ fillStyle = tiledImage.placeholderFillStyle;
}
- }
- context.restore();
- },
+ this._offsetForRotation({degrees: this.viewer.viewport.getRotation(true)});
+ context.fillStyle = fillStyle;
+ context.translate(rect.x, rect.y);
+ context.rotate(Math.PI / 180 * bounds.degrees);
+ context.translate(-rect.x, -rect.y);
+ context.fillRect(rect.x, rect.y, rect.width, rect.height);
+ this._restoreRotationChanges();
- // private
- debugRect: function(rect) {
- if ( this.useCanvas ) {
- var context = this.context;
- 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
- );
+ /**
+ * Get the canvas center
+ * @private
+ * @returns {OpenSeadragon.Point} The center point of the canvas
+ */
+ _getCanvasCenter() {
+ return new $.Point(this.canvas.width / 2, this.canvas.height / 2);
+ }
+ // private
+ _restoreRotationChanges() {
+ var context = this._outputContext;
context.restore();
}
- },
- /**
- * Turns image smoothing on or off for this viewer. Note: Ignored in some (especially older) browsers that do not support this property.
- *
- * @function
- * @param {Boolean} [imageSmoothingEnabled] - Whether or not the image is
- * drawn smoothly on the canvas; see imageSmoothingEnabled in
- * {@link OpenSeadragon.Options} for more explanation.
- */
- setImageSmoothingEnabled: function(imageSmoothingEnabled){
- if ( this.useCanvas ) {
- this._imageSmoothingEnabled = imageSmoothingEnabled;
- this._updateImageSmoothingEnabled(this.context);
- this.viewer.forceRedraw();
- }
- },
+ // 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) {
- // private
- _updateImageSmoothingEnabled: function(context){
- context.msImageSmoothingEnabled = this._imageSmoothingEnabled;
- context.imageSmoothingEnabled = this._imageSmoothingEnabled;
- },
+ function loadShader(gl, type, source) {
+ const shader = gl.createShader(type);
- /**
- * Get the canvas size
- * @param {Boolean} sketch If set to true return the size of the sketch canvas
- * @returns {OpenSeadragon.Point} The size of the canvas
- */
- getCanvasSize: function(sketch) {
- var canvas = this._getContext(sketch).canvas;
- return new $.Point(canvas.width, canvas.height);
- },
+ // Send the source to the shader object
- getCanvasCenter: function() {
- return new $.Point(this.canvas.width / 2, this.canvas.height / 2);
- },
+ gl.shaderSource(shader, source);
- // private
- _offsetForRotation: function(options) {
- var point = options.point ?
- options.point.times($.pixelDensityRatio) :
- this.getCanvasCenter();
+ // Compile the shader program
- var context = this._getContext(options.useSketch);
- context.save();
+ gl.compileShader(shader);
- context.translate(point.x, point.y);
- if(this.viewer.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);
- },
+ // See if it compiled successfully
- // private
- _flip: function(options) {
- options = options || {};
- var point = options.point ?
- options.point.times($.pixelDensityRatio) :
- this.getCanvasCenter();
- var context = this._getContext(options.useSketch);
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ $.console.error(
+ `An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}`
+ );
+ gl.deleteShader(shader);
+ return null;
+ }
- context.translate(point.x, 0);
- context.scale(-1, 1);
- context.translate(-point.x, 0);
- },
+ return shader;
+ }
- // private
- _restoreRotationChanges: function(useSketch) {
- var context = this._getContext(useSketch);
- context.restore();
- },
+ const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
+ const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
- // private
- _calculateCanvasSize: function() {
- var pixelDensityRatio = $.pixelDensityRatio;
- var viewportSize = this.viewport.getContainerSize();
- return {
- // canvas width and height are integers
- x: Math.round(viewportSize.x * pixelDensityRatio),
- y: Math.round(viewportSize.y * pixelDensityRatio)
- };
- },
+ // Create the shader program
- // private
- _calculateSketchCanvasSize: function() {
- var canvasSize = this._calculateCanvasSize();
- if (this.viewport.getRotation() === 0) {
- return canvasSize;
+ 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)) {
+ $.console.error(
+ `Unable to initialize the shader program: ${gl.getProgramInfoLog(
+ shaderProgram
+ )}`
+ );
+ return null;
+ }
+
+ return shaderProgram;
}
- // If the viewport is rotated, we need a larger sketch canvas in order
- // to support edge smoothing.
- var sketchCanvasSize = Math.ceil(Math.sqrt(
- canvasSize.x * canvasSize.x +
- canvasSize.y * canvasSize.y));
- return {
- x: sketchCanvasSize,
- y: sketchCanvasSize
- };
- }
-};
+
+ };
+
+
}( OpenSeadragon ));
@@ -19935,7 +22394,7 @@ $.Drawer.prototype = {
* OpenSeadragon - Viewport
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -21066,7 +23525,7 @@ $.Viewport.prototype = {
/**
* Update the zoom, degrees, and center (X and Y) springs.
* @function
- * @returns {Boolean} True if any change has been made, false otherwise.
+ * @returns {Boolean} True if the viewport is still animating, false otherwise.
*/
update: function() {
var _this = this;
@@ -21098,7 +23557,13 @@ $.Viewport.prototype = {
this._oldZoom = this.zoomSpring.current.value;
this._oldDegrees = this.degreesSpring.current.value;
- return changed;
+ var isAnimating = changed ||
+ !this.zoomSpring.isAtTargetValue() ||
+ !this.centerSpringX.isAtTargetValue() ||
+ !this.centerSpringY.isAtTargetValue() ||
+ !this.degreesSpring.isAtTargetValue();
+
+ return isAnimating;
},
// private - pass true to use spring, or a number for degrees for immediate rotation
@@ -21638,7 +24103,7 @@ $.Viewport.prototype = {
* 1 means original image size, 0.5 half size...
* Viewport zoom: ratio of the displayed image's width to viewport's width.
* 1 means identical width, 2 means image's width is twice the viewport's width...
- * Note: not accurate with multi-image.
+ * Note: not accurate with multi-image; use [TiledImage.imageToViewportZoom] for the specific image of interest.
* @function
* @param {Number} imageZoom The image zoom
* target zoom.
@@ -21650,7 +24115,7 @@ $.Viewport.prototype = {
if (count > 1) {
if (!this.silenceMultiImageWarnings) {
$.console.error('[Viewport.imageToViewportZoom] is not accurate ' +
- 'with multi-image.');
+ 'with multi-image. Instead, use [TiledImage.imageToViewportZoom] for the specific image of interest');
}
} else if (count === 1) {
// It is better to use TiledImage.imageToViewportZoom
@@ -21716,7 +24181,41 @@ $.Viewport.prototype = {
*/
this.viewer.raiseEvent('flip', {flipped: state});
return this;
- }
+ },
+
+ /**
+ * Gets current max zoom pixel ratio
+ * @function
+ * @returns {Number} Max zoom pixel ratio
+ */
+ getMaxZoomPixelRatio: function() {
+ return this.maxZoomPixelRatio;
+ },
+
+ /**
+ * Sets max zoom pixel ratio
+ * @function
+ * @param {Number} ratio - Max zoom pixel ratio
+ * @param {Boolean} [applyConstraints=true] - Apply constraints after setting ratio;
+ * Takes effect only if current zoom is greater than set max zoom pixel ratio
+ * @param {Boolean} [immediately=false] - Whether to animate to new zoom
+ */
+ setMaxZoomPixelRatio: function(ratio, applyConstraints = true, immediately = false) {
+
+ $.console.assert(!isNaN(ratio), "[Viewport.setMaxZoomPixelRatio] ratio must be a number");
+
+ if (isNaN(ratio)) {
+ return;
+ }
+
+ this.maxZoomPixelRatio = ratio;
+
+ if (applyConstraints) {
+ if (this.getZoom() > this.getMaxZoom()) {
+ this.applyConstraints(immediately);
+ }
+ }
+ },
};
@@ -21726,7 +24225,7 @@ $.Viewport.prototype = {
* OpenSeadragon - TiledImage
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -21796,8 +24295,8 @@ $.Viewport.prototype = {
* @param {Boolean} [options.iOSDevice] - See {@link OpenSeadragon.Options}.
* @param {Number} [options.opacity=1] - Set to draw at proportional opacity. If zero, images will not draw.
* @param {Boolean} [options.preload=false] - Set true to load even when the image is hidden by zero opacity.
- * @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible
- values.
+ * @param {String} [options.compositeOperation] - How the image is composited onto other images;
+ * see compositeOperation in {@link OpenSeadragon.Options} for possible values.
* @param {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}.
* @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}.
* @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}.
@@ -21809,7 +24308,7 @@ $.Viewport.prototype = {
* 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
@@ -21883,10 +24382,15 @@ $.TiledImage = function( options ) {
loadingCoverage: {}, // A '3d' dictionary [level][x][y] --> Boolean; shows what areas are loaded or are being loaded/blended.
lastDrawn: [], // An unordered list of Tiles drawn last frame.
lastResetTime: 0, // Last time for which the tiledImage was reset.
- _midDraw: false, // Is the tiledImage currently updating the viewport?
- _needsDraw: true, // Does the tiledImage need to update the viewport again?
+ _needsDraw: true, // Does the tiledImage need to be drawn again?
+ _needsUpdate: 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, 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?
+ _isTainted: false, // Has a Tile been found with tainted data?
//configurable settings
springStiffness: $.DEFAULT_SETTINGS.springStiffness,
animationTime: $.DEFAULT_SETTINGS.animationTime,
@@ -21906,7 +24410,8 @@ $.TiledImage = function( options ) {
opacity: $.DEFAULT_SETTINGS.opacity,
preload: $.DEFAULT_SETTINGS.preload,
compositeOperation: $.DEFAULT_SETTINGS.compositeOperation,
- subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency
+ subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency,
+ maxTilesPerFrame: $.DEFAULT_SETTINGS.maxTilesPerFrame
}, options );
this._preload = this.preload;
@@ -21944,30 +24449,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 */{
@@ -21978,6 +24462,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
return this._needsDraw;
},
+ /**
+ * Mark the tiled image as needing to be (re)drawn
+ */
+ redraw: function() {
+ this._needsDraw = true;
+ },
+
/**
* @returns {Boolean} Whether all tiles necessary for this TiledImage to draw at the current view have been loaded.
*/
@@ -22020,17 +24511,29 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
},
/**
- * Updates the TiledImage's bounds, animating if needed.
- * @returns {Boolean} Whether the TiledImage animated.
+ * Updates the TiledImage's bounds, animating if needed. Based on the new
+ * bounds, updates the levels and tiles to be drawn into the viewport.
+ * @param viewportChanged Whether the viewport changed meaning tiles need to be updated.
+ * @returns {Boolean} Whether the TiledImage needs to be drawn.
*/
- update: function() {
- var xUpdated = this._xSpring.update();
- var yUpdated = this._ySpring.update();
- var scaleUpdated = this._scaleSpring.update();
- var degreesUpdated = this._degreesSpring.update();
+ update: function(viewportChanged) {
+ let xUpdated = this._xSpring.update();
+ let yUpdated = this._ySpring.update();
+ let scaleUpdated = this._scaleSpring.update();
+ let degreesUpdated = this._degreesSpring.update();
+
+ let updated = (xUpdated || yUpdated || scaleUpdated || degreesUpdated || this._needsUpdate);
+
+ if (updated || viewportChanged || !this._fullyLoaded){
+ let fullyLoadedFlag = this._updateLevelsForViewport();
+ this._setFullyLoaded(fullyLoadedFlag);
+ }
+
+ this._needsUpdate = false;
- if (xUpdated || yUpdated || scaleUpdated || degreesUpdated) {
+ if (updated) {
this._updateForScale();
+ this._raiseBoundsChange();
this._needsDraw = true;
return true;
}
@@ -22039,18 +24542,33 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
},
/**
- * Draws the TiledImage to its Drawer.
+ * Mark this TiledImage as having been drawn, so that it will only be drawn
+ * 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
*/
- draw: function() {
- if (this.opacity !== 0 || this._preload) {
- this._midDraw = true;
- this._updateViewport();
- this._midDraw = false;
- }
- // Images with opacity 0 should not need to be drawn in future. this._needsDraw = false is set in this._updateViewport() for other images.
- else {
- this._needsDraw = false;
- }
+ setDrawn: function(){
+ this._needsDraw = this._isBlending || this._wasBlending;
+ return this._needsDraw;
+ },
+
+ /**
+ * Set the internal _isTainted flag for this TiledImage. Lazy loaded - not
+ * checked each time a Tile is loaded, but can be set if a consumer of the
+ * tiles (e.g. a Drawer) discovers a Tile to have tainted data so that further
+ * checks are not needed and alternative rendering strategies can be used.
+ * @private
+ */
+ setTainted(isTainted){
+ this._isTainted = isTainted;
+ },
+
+ /**
+ * @private
+ * @returns {Boolean} whether the TiledImage has been marked as tainted
+ */
+ isTainted(){
+ return this._isTainted;
},
/**
@@ -22060,7 +24578,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this.reset();
if (this.source.destroy) {
- this.source.destroy();
+ this.source.destroy(this.viewer);
}
},
@@ -22137,7 +24655,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
var yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y;
var bounds = this.source.getTileBounds(level, xMod, yMod);
if (this.getFlip()) {
- bounds.x = 1 - bounds.x - bounds.width;
+ bounds.x = Math.max(0, 1 - bounds.x - bounds.width);
}
bounds.x += (x - xMod) / numTiles.x;
bounds.y += (this._worldHeightCurrent / this._worldWidthCurrent) * ((y - yMod) / numTiles.y);
@@ -22218,7 +24736,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;
@@ -22404,6 +24922,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._xSpring.resetTo(position.x);
this._ySpring.resetTo(position.y);
this._needsDraw = true;
+ this._needsUpdate = true;
} else {
if (sameTarget) {
return;
@@ -22412,6 +24931,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._xSpring.springTo(position.x);
this._ySpring.springTo(position.y);
this._needsDraw = true;
+ this._needsUpdate = true;
}
if (!sameTarget) {
@@ -22450,7 +24970,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* ]
*/
setCroppingPolygons: function( polygons ) {
-
var isXYObject = function(obj) {
return obj instanceof $.Point || (typeof obj.x === 'number' && typeof obj.y === 'number');
};
@@ -22476,10 +24995,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._croppingPolygons = polygons.map(function(polygon){
return objectToSimpleXYObject(polygon);
});
+ this._needsDraw = true;
} catch (e) {
$.console.error('[TiledImage.setCroppingPolygons] Cropping polygon format not supported');
$.console.error(e);
- this._croppingPolygons = null;
+ this.resetCroppingPolygons();
}
},
@@ -22489,6 +25009,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
*/
resetCroppingPolygons: function() {
this._croppingPolygons = null;
+ this._needsDraw = true;
},
/**
@@ -22597,7 +25118,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @returns {Boolean} Whether the TiledImage should be flipped before rendering.
*/
getFlip: function() {
- return !!this.flipped;
+ return this.flipped;
},
/**
@@ -22605,9 +25126,54 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @fires OpenSeadragon.TiledImage.event:bounds-change
*/
setFlip: function(flip) {
- this.flipped = !!flip;
+ this.flipped = flip;
+ },
+
+ get flipped(){
+ return this._flipped;
+ },
+ set flipped(flipped){
+ let changed = this._flipped !== !!flipped;
+ this._flipped = !!flipped;
+ 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(){
+ return this._debugMode;
+ },
+ set debugMode(debug){
+ this._debugMode = !!debug;
this._needsDraw = true;
- this._raiseBoundsChange();
},
/**
@@ -22622,11 +25188,19 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @fires OpenSeadragon.TiledImage.event:opacity-change
*/
setOpacity: function(opacity) {
+ this.opacity = opacity;
+ },
+
+ get opacity() {
+ return this._opacity;
+ },
+
+ set opacity(opacity) {
if (opacity === this.opacity) {
return;
}
- this.opacity = opacity;
+ this._opacity = opacity;
this._needsDraw = true;
/**
* Raised when the TiledImage's opacity is changed.
@@ -22687,9 +25261,58 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._degreesSpring.springTo(degrees);
}
this._needsDraw = true;
+ this._needsUpdate = true;
this._raiseBoundsChange();
},
+ /**
+ * Get the region of this tiled image that falls within the viewport.
+ * @returns {OpenSeadragon.Rect} the region of this tiled image that falls within the viewport.
+ * Returns false for images with opacity==0 unless preload==true
+ */
+ getDrawArea: function(){
+
+ if( this._opacity === 0 && !this._preload){
+ return false;
+ }
+
+ var drawArea = this._viewportToTiledImageRectangle(
+ this.viewport.getBoundsWithMargins(true));
+
+ if (!this.wrapHorizontal && !this.wrapVertical) {
+ var tiledImageBounds = this._viewportToTiledImageRectangle(
+ this.getClippedBounds(true));
+ drawArea = drawArea.intersection(tiledImageBounds);
+ }
+
+ return drawArea;
+ },
+
+ /**
+ *
+ * @returns {Array} Array of Tiles that make up the current view
+ */
+ getTilesToDraw: function(){
+ // start with all the tiles added to this._tilesToDraw during the most recent
+ // call to this.update. Then update them so the blending and coverage properties
+ // are updated based on the current time
+ let tileArray = this._tilesToDraw.flat();
+
+ // update all tiles, which can change the coverage provided
+ 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;
+ },
+
/**
* Get the point around which this tiled image is rotated
* @private
@@ -22700,23 +25323,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
return this.getBoundsNoRotate(current).getCenter();
},
- /**
- * @returns {String} The TiledImage's current compositeOperation.
- */
- getCompositeOperation: function() {
- return this.compositeOperation;
+ get compositeOperation(){
+ return this._compositeOperation;
},
- /**
- * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation.
- * @fires OpenSeadragon.TiledImage.event:composite-operation-change
- */
- setCompositeOperation: function(compositeOperation) {
- if (compositeOperation === this.compositeOperation) {
+ set compositeOperation(compositeOperation){
+
+ if (compositeOperation === this._compositeOperation) {
return;
}
-
- this.compositeOperation = compositeOperation;
+ this._compositeOperation = compositeOperation;
this._needsDraw = true;
/**
* Raised when the TiledImage's opacity is changed.
@@ -22729,8 +25345,24 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent('composite-operation-change', {
- compositeOperation: this.compositeOperation
+ compositeOperation: this._compositeOperation
});
+
+ },
+
+ /**
+ * @returns {String} The TiledImage's current compositeOperation.
+ */
+ getCompositeOperation: function() {
+ return this._compositeOperation;
+ },
+
+ /**
+ * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation.
+ * @fires OpenSeadragon.TiledImage.event:composite-operation-change
+ */
+ setCompositeOperation: function(compositeOperation) {
+ this.compositeOperation = compositeOperation; //invokes setter
},
/**
@@ -22828,6 +25460,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._scaleSpring.resetTo(scale);
this._updateForScale();
this._needsDraw = true;
+ this._needsUpdate = true;
} else {
if (sameTarget) {
return;
@@ -22836,6 +25469,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._scaleSpring.springTo(scale);
this._updateForScale();
this._needsDraw = true;
+ this._needsUpdate = true;
}
if (!sameTarget) {
@@ -22898,650 +25532,365 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
};
},
- /**
- * @private
- * @inner
- * Pretty much every other line in this needs to be documented so it's clear
- * how each piece of this routine contributes to the drawing process. That's
- * why there are so many TODO's inside this function.
- */
- _updateViewport: function() {
- this._needsDraw = false;
- this._tilesLoading = 0;
- this.loadingCoverage = {};
-
- // Reset tile's internal drawn state
- while (this.lastDrawn.length > 0) {
- var tile = this.lastDrawn.pop();
- tile.beingDrawn = false;
- }
-
- var viewport = this.viewport;
- var drawArea = this._viewportToTiledImageRectangle(
- viewport.getBoundsWithMargins(true));
-
- if (!this.wrapHorizontal && !this.wrapVertical) {
- var tiledImageBounds = this._viewportToTiledImageRectangle(
- this.getClippedBounds(true));
- drawArea = drawArea.intersection(tiledImageBounds);
- if (drawArea === null) {
- return;
- }
- }
-
+ // returns boolean flag of whether the image should be marked as fully loaded
+ _updateLevelsForViewport: function(){
var levelsInterval = this._getLevelsInterval();
- var lowestLevel = levelsInterval.lowestLevel;
- var highestLevel = levelsInterval.highestLevel;
- var bestTile = null;
- var haveDrawn = false;
+ var lowestLevel = levelsInterval.lowestLevel; // the lowest level we should draw at our current zoom
+ var highestLevel = levelsInterval.highestLevel; // the highest level we should draw at our current zoom
+ var bestTiles = [];
+ var drawArea = this.getDrawArea();
var currentTime = $.now();
- // Update any level that will be drawn
- for (var level = highestLevel; level >= lowestLevel; level--) {
- var drawLevel = false;
-
- //Avoid calculations for draw if we have already drawn this
- var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
- this.source.getPixelRatio(level),
- true
- ).x * this._scaleSpring.current.value;
-
- if (level === lowestLevel ||
- (!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio)) {
- drawLevel = true;
- haveDrawn = true;
- } else if (!haveDrawn) {
- continue;
- }
-
- //Perform calculations for draw if we haven't drawn this
- var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
- this.source.getPixelRatio(level),
- false
- ).x * this._scaleSpring.current.value;
-
- var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate(
- this.source.getPixelRatio(
- Math.max(
- this.source.getClosestLevel(),
- 0
- )
- ),
- false
- ).x * this._scaleSpring.current.value;
-
- var optimalRatio = this.immediateRender ? 1 : targetZeroRatio;
- var levelOpacity = Math.min(1, (currentRenderPixelRatio - 0.5) / 0.5);
- var levelVisibility = optimalRatio / Math.abs(
- optimalRatio - targetRenderPixelRatio
- );
-
- // Update the level and keep track of 'best' tile to load
- bestTile = this._updateLevel(
- haveDrawn,
- drawLevel,
- level,
- levelOpacity,
- levelVisibility,
- drawArea,
- currentTime,
- bestTile
- );
-
- // Stop the loop if lower-res tiles would all be covered by
- // already drawn tiles
- if (this._providesCoverage(this.coverage, level)) {
- break;
- }
- }
-
- // Perform the actual drawing
- this._drawTiles(this.lastDrawn);
-
- // Load the new 'best' tile
- if (bestTile && !bestTile.context2D) {
- this._loadTile(bestTile, currentTime);
- this._needsDraw = true;
- this._setFullyLoaded(false);
- } else {
- this._setFullyLoaded(this._tilesLoading === 0);
- }
- },
-
- // private
- _getCornerTiles: function(level, topLeftBound, bottomRightBound) {
- var leftX;
- var rightX;
- if (this.wrapHorizontal) {
- leftX = $.positiveModulo(topLeftBound.x, 1);
- rightX = $.positiveModulo(bottomRightBound.x, 1);
- } else {
- leftX = Math.max(0, topLeftBound.x);
- rightX = Math.min(1, bottomRightBound.x);
- }
- var topY;
- var bottomY;
- var aspectRatio = 1 / this.source.aspectRatio;
- if (this.wrapVertical) {
- topY = $.positiveModulo(topLeftBound.y, aspectRatio);
- bottomY = $.positiveModulo(bottomRightBound.y, aspectRatio);
- } else {
- topY = Math.max(0, topLeftBound.y);
- bottomY = Math.min(aspectRatio, bottomRightBound.y);
- }
-
- var topLeftTile = this.source.getTileAtPoint(level, new $.Point(leftX, topY));
- var bottomRightTile = this.source.getTileAtPoint(level, new $.Point(rightX, bottomY));
- var numTiles = this.source.getNumTiles(level);
-
- if (this.wrapHorizontal) {
- topLeftTile.x += numTiles.x * Math.floor(topLeftBound.x);
- bottomRightTile.x += numTiles.x * Math.floor(bottomRightBound.x);
- }
- if (this.wrapVertical) {
- topLeftTile.y += numTiles.y * Math.floor(topLeftBound.y / aspectRatio);
- bottomRightTile.y += numTiles.y * Math.floor(bottomRightBound.y / aspectRatio);
- }
-
- return {
- topLeft: topLeftTile,
- bottomRight: bottomRightTile,
- };
- },
-
- /**
- * Updates all tiles at a given resolution level.
- * @private
- * @param {Boolean} haveDrawn
- * @param {Boolean} drawLevel
- * @param {Number} level
- * @param {Number} levelOpacity
- * @param {Number} levelVisibility
- * @param {OpenSeadragon.Rect} drawArea
- * @param {Number} currentTime
- * @param {OpenSeadragon.Tile} best - The current "best" tile to draw.
- */
- _updateLevel: function(haveDrawn, drawLevel, level, levelOpacity,
- levelVisibility, drawArea, currentTime, best) {
-
- var topLeftBound = drawArea.getBoundingBox().getTopLeft();
- var bottomRightBound = drawArea.getBoundingBox().getBottomRight();
-
- if (this.viewer) {
- /**
- * - Needs documentation -
- *
- * @event update-level
- * @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 {Object} havedrawn
- * @property {Object} level
- * @property {Object} opacity
- * @property {Object} visibility
- * @property {OpenSeadragon.Rect} drawArea
- * @property {Object} topleft deprecated, use drawArea instead
- * @property {Object} bottomright deprecated, use drawArea instead
- * @property {Object} currenttime
- * @property {Object} best
- * @property {?Object} userData - Arbitrary subscriber-defined object.
- */
- this.viewer.raiseEvent('update-level', {
- tiledImage: this,
- havedrawn: haveDrawn,
- level: level,
- opacity: levelOpacity,
- visibility: levelVisibility,
- drawArea: drawArea,
- topleft: topLeftBound,
- bottomright: bottomRightBound,
- currenttime: currentTime,
- best: best
- });
- }
-
- this._resetCoverage(this.coverage, level);
- this._resetCoverage(this.loadingCoverage, level);
-
- //OK, a new drawing so do your calculations
- var cornerTiles = this._getCornerTiles(level, topLeftBound, bottomRightBound);
- var topLeftTile = cornerTiles.topLeft;
- var bottomRightTile = cornerTiles.bottomRight;
- var numberOfTiles = this.source.getNumTiles(level);
-
- var viewportCenter = this.viewport.pixelFromPoint(this.viewport.getCenter());
-
- if (this.getFlip()) {
- // The right-most tile can be narrower than the others. When flipped,
- // this tile is now on the left. Because it is narrower than the normal
- // left-most tile, the subsequent tiles may not be wide enough to completely
- // fill the viewport. Fix this by rendering an extra column of tiles. If we
- // are not wrapping, make sure we never render more than the number of tiles
- // in the image.
- bottomRightTile.x += 1;
- if (!this.wrapHorizontal) {
- bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1);
- }
- }
-
- for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) {
- for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) {
-
- var flippedX;
- if (this.getFlip()) {
- var xMod = ( numberOfTiles.x + ( x % numberOfTiles.x ) ) % numberOfTiles.x;
- flippedX = x + numberOfTiles.x - xMod - xMod - 1;
- } else {
- flippedX = x;
- }
-
- if (drawArea.intersection(this.getTileBounds(level, flippedX, y)) === null) {
- // This tile is outside of the viewport, no need to draw it
- continue;
- }
-
- best = this._updateTile(
- drawLevel,
- haveDrawn,
- flippedX, y,
- level,
- levelOpacity,
- levelVisibility,
- viewportCenter,
- numberOfTiles,
- currentTime,
- best
- );
- }
- }
-
- return best;
- },
-
- /**
- * @private
- * @inner
- * Update a single tile at a particular resolution level.
- * @param {Boolean} haveDrawn
- * @param {Boolean} drawLevel
- * @param {Number} x
- * @param {Number} y
- * @param {Number} level
- * @param {Number} levelOpacity
- * @param {Number} levelVisibility
- * @param {OpenSeadragon.Point} viewportCenter
- * @param {Number} numberOfTiles
- * @param {Number} currentTime
- * @param {OpenSeadragon.Tile} best - The current "best" tile to draw.
- */
- _updateTile: function( haveDrawn, drawLevel, x, y, level, levelOpacity,
- levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
-
- var tile = this._getTile(
- x, y,
- level,
- currentTime,
- numberOfTiles,
- this._worldWidthCurrent,
- this._worldHeightCurrent
- ),
- drawTile = drawLevel;
-
- if( this.viewer ){
- /**
- * - Needs documentation -
- *
- * @event update-tile
- * @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( 'update-tile', {
- tiledImage: this,
- tile: tile
- });
- }
-
- this._setCoverage( this.coverage, level, x, y, false );
+ // reset each tile's beingDrawn flag
+ this._lastDrawn.forEach(tileinfo => {
+ tileinfo.tile.beingDrawn = false;
+ });
+ // clear the list of tiles to draw
+ this._tilesToDraw = [];
+ this._tilesLoading = 0;
+ this.loadingCoverage = {};
- var loadingCoverage = tile.loaded || tile.loading || this._isCovered(this.loadingCoverage, level, x, y);
- this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage);
+ if(!drawArea){
+ this._needsDraw = false;
+ return this._fullyLoaded;
+ }
- if ( !tile.exists ) {
- return best;
+ // make a list of levels to use for the current zoom level
+ var levelList = new Array(highestLevel - lowestLevel + 1);
+ // go from highest to lowest resolution
+ for(let i = 0, level = highestLevel; level >= lowestLevel; level--, i++){
+ levelList[i] = level;
}
- if ( haveDrawn && !drawTile ) {
- if ( this._isCovered( this.coverage, level, x, y ) ) {
- this._setCoverage( this.coverage, level, x, y, true );
- } else {
- drawTile = true;
+ // if a single-tile level is loaded, add that to the end of the list
+ // as a fallback to use during zooming out, until a lower-res tile is
+ // loaded
+ for(let level = highestLevel + 1; level <= this.source.maxLevel; level++){
+ var tile = (
+ this.tilesMatrix[level] &&
+ this.tilesMatrix[level][0] &&
+ this.tilesMatrix[level][0][0]
+ );
+ if(tile && tile.isBottomMost && tile.isRightMost && tile.loaded){
+ levelList.push(level);
+ break;
}
}
- if ( !drawTile ) {
- return best;
- }
- this._positionTile(
- tile,
- this.source.tileOverlap,
- this.viewport,
- viewportCenter,
- levelVisibility
- );
+ // Update any level that will be drawn.
+ // We are iterating from highest resolution to lowest resolution
+ // Once a level fully covers the viewport the loop is halted and
+ // lower-resolution levels are skipped
+ let useLevel = false;
+ for (let i = 0; i < levelList.length; i++) {
+ let level = levelList[i];
- if (!tile.loaded) {
- if (tile.context2D) {
- this._setTileLoaded(tile);
- } else {
- var imageRecord = this._tileCache.getImageRecord(tile.cacheKey);
- if (imageRecord) {
- this._setTileLoaded(tile, imageRecord.getData());
- }
+ var currentRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate(
+ this.source.getPixelRatio(level),
+ true
+ ).x * this._scaleSpring.current.value;
+
+ // make sure we skip levels until currentRenderPixelRatio becomes >= minPixelRatio
+ // but always use the last level in the list so we draw something
+ if (i === levelList.length - 1 || currentRenderPixelRatio >= this.minPixelRatio ) {
+ useLevel = true;
+ } else if (!useLevel) {
+ continue;
}
- }
- if ( tile.loaded ) {
- var needsDraw = this._blendTile(
- tile,
- x, y,
+ var targetRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate(
+ this.source.getPixelRatio(level),
+ false
+ ).x * this._scaleSpring.current.value;
+
+ var targetZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate(
+ this.source.getPixelRatio(
+ Math.max(
+ this.source.getClosestLevel(),
+ 0
+ )
+ ),
+ false
+ ).x * this._scaleSpring.current.value;
+
+ var optimalRatio = this.immediateRender ? 1 : targetZeroRatio;
+ var levelOpacity = Math.min(1, (currentRenderPixelRatio - 0.5) / 0.5);
+ var levelVisibility = optimalRatio / Math.abs(
+ optimalRatio - targetRenderPixelRatio
+ );
+
+ // Update the level and keep track of 'best' tiles to load
+ var result = this._updateLevel(
level,
levelOpacity,
- currentTime
+ levelVisibility,
+ drawArea,
+ currentTime,
+ bestTiles
);
- if ( needsDraw ) {
- this._needsDraw = true;
+ bestTiles = result.bestTiles;
+ var tiles = result.updatedTiles.filter(tile => tile.loaded);
+ var makeTileInfoObject = (function(level, levelOpacity, currentTime){
+ return function(tile){
+ return {
+ tile: tile,
+ level: level,
+ levelOpacity: levelOpacity,
+ currentTime: currentTime
+ };
+ };
+ })(level, levelOpacity, currentTime);
+
+ this._tilesToDraw[level] = tiles.map(makeTileInfoObject);
+
+ // Stop the loop if lower-res tiles would all be covered by
+ // already drawn tiles
+ if (this._providesCoverage(this.coverage, level)) {
+ break;
}
- } else if ( tile.loading ) {
- // the tile is already in the download queue
- this._tilesLoading++;
- } else if (!loadingCoverage) {
- best = this._compareTiles( best, tile );
}
- return best;
+
+ // Load the new 'best' n tiles
+ if (bestTiles && bestTiles.length > 0) {
+ bestTiles.forEach(function (tile) {
+ if (tile && !tile.context2D) {
+ this._loadTile(tile, currentTime);
+ }
+ }, this);
+
+ this._needsDraw = true;
+ return false;
+ } else {
+ return this._tilesLoading === 0;
+ }
+
+ // Update
+
},
/**
+ * Update all tiles that contribute to the current view
* @private
- * @inner
- * Obtains a tile at the given location.
- * @param {Number} x
- * @param {Number} y
- * @param {Number} level
- * @param {Number} time
- * @param {Number} numTiles
- * @param {Number} worldWidth
- * @param {Number} worldHeight
- * @returns {OpenSeadragon.Tile}
+ *
*/
- _getTile: function(
- x, y,
- level,
- time,
- numTiles,
- worldWidth,
- worldHeight
- ) {
- var xMod,
- yMod,
- bounds,
- sourceBounds,
- exists,
- urlOrGetter,
- post,
- ajaxHeaders,
- context2D,
- tile,
- tilesMatrix = this.tilesMatrix,
- tileSource = this.source;
+ _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;
- if ( !tilesMatrix[ level ] ) {
- tilesMatrix[ level ] = {};
- }
- if ( !tilesMatrix[ level ][ x ] ) {
- tilesMatrix[ level ][ x ] = {};
+ let drawArea = this.getDrawArea();
+ if(!drawArea){
+ return;
}
- if ( !tilesMatrix[ level ][ x ][ y ] || !tilesMatrix[ level ][ x ][ y ].flipped !== !this.flipped ) {
- xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x;
- yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y;
- bounds = this.getTileBounds( level, x, y );
- sourceBounds = tileSource.getTileBounds( level, xMod, yMod, true );
- exists = tileSource.tileExists( level, xMod, yMod );
- urlOrGetter = tileSource.getTileUrl( level, xMod, yMod );
- post = tileSource.getTilePostData( level, xMod, yMod );
-
- // Headers are only applicable if loadTilesWithAjax is set
- if (this.loadTilesWithAjax) {
- ajaxHeaders = tileSource.getTileAjaxHeaders( level, xMod, yMod );
- // Combine tile AJAX headers with tiled image AJAX headers (if applicable)
- if ($.isPlainObject(this.ajaxHeaders)) {
- ajaxHeaders = $.extend({}, this.ajaxHeaders, ajaxHeaders);
- }
- } else {
- ajaxHeaders = null;
+ function updateTile(info){
+ let tile = info.tile;
+ if(tile && tile.loaded){
+ let tileIsBlending = _this._blendTile(
+ tile,
+ tile.x,
+ tile.y,
+ info.level,
+ info.levelOpacity,
+ currentTime,
+ lowestLevel
+ );
+ _this._isBlending = _this._isBlending || tileIsBlending;
+ _this._needsDraw = _this._needsDraw || tileIsBlending || _this._wasBlending;
}
+ }
- context2D = tileSource.getContext2D ?
- tileSource.getContext2D(level, xMod, yMod) : undefined;
-
- tile = new $.Tile(
- level,
- x,
- y,
- bounds,
- exists,
- urlOrGetter,
- context2D,
- this.loadTilesWithAjax,
- ajaxHeaders,
- sourceBounds,
- post,
- tileSource.getTileHashKey(level, xMod, yMod, urlOrGetter, ajaxHeaders, post)
- );
-
- if (this.getFlip()) {
- if (xMod === 0) {
- tile.isRightMost = true;
- }
- } else {
- if (xMod === numTiles.x - 1) {
- tile.isRightMost = true;
- }
+ // Update each tile in the list of tiles. 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 < tiles.length; i++){
+ let tile = tiles[i];
+ updateTile(tile);
+ if(this._providesCoverage(this.coverage, tile.level)){
+ level = Math.max(level, tile.level);
}
-
- if (yMod === numTiles.y - 1) {
- tile.isBottomMost = true;
+ }
+ if(level > 0){
+ for( let levelKey in this._tilesToDraw ){
+ if( levelKey < level ){
+ delete this._tilesToDraw[levelKey];
+ }
}
-
- tile.flipped = this.flipped;
-
- tilesMatrix[ level ][ x ][ y ] = tile;
}
- tile = tilesMatrix[ level ][ x ][ y ];
- tile.lastTouchTime = time;
-
- return tile;
},
/**
+ * Updates the opacity of a tile according to the time it has been on screen
+ * to perform a fade-in.
+ * Updates coverage once a tile is fully opaque.
+ * Returns whether the fade-in has completed.
* @private
- * @inner
- * Dispatch a job to the ImageLoader to load the Image for a Tile.
+ *
* @param {OpenSeadragon.Tile} tile
- * @param {Number} time
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} level
+ * @param {Number} levelOpacity
+ * @param {Number} currentTime
+ * @param {Boolean} lowestLevel
+ * @returns {Boolean} true if blending did not yet finish
*/
- _loadTile: function(tile, time ) {
- var _this = this;
- tile.loading = true;
- this._imageLoader.addJob({
- src: tile.getUrl(),
- tile: tile,
- source: this.source,
- postData: tile.postData,
- loadWithAjax: tile.loadWithAjax,
- ajaxHeaders: tile.ajaxHeaders,
- crossOriginPolicy: this.crossOriginPolicy,
- ajaxWithCredentials: this.ajaxWithCredentials,
- callback: function( data, errorMsg, tileRequest ){
- _this._onTileLoad( tile, time, data, errorMsg, tileRequest );
- },
- abort: function() {
- tile.loading = false;
- }
- });
+ _blendTile: function(tile, x, y, level, levelOpacity, currentTime, lowestLevel ){
+ let blendTimeMillis = 1000 * this.blendTime,
+ deltaTime,
+ opacity;
+
+ if ( !tile.blendStart ) {
+ tile.blendStart = currentTime;
+ }
+
+ 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;
+ }
+ // return true if the tile is still blending
+ return deltaTime < blendTimeMillis;
},
/**
+ * Updates all tiles at a given resolution level.
* @private
- * @inner
- * Callback fired when a Tile's Image finished downloading.
- * @param {OpenSeadragon.Tile} tile
- * @param {Number} time
- * @param {*} data image data
- * @param {String} errorMsg
- * @param {XMLHttpRequest} tileRequest
+ * @param {Number} level
+ * @param {Number} levelOpacity
+ * @param {Number} levelVisibility
+ * @param {OpenSeadragon.Rect} drawArea
+ * @param {Number} currentTime
+ * @param {OpenSeadragon.Tile[]} best Array of the current best tiles
+ * @returns {Object} Dictionary {bestTiles: OpenSeadragon.Tile - the current "best" tiles to draw, updatedTiles: OpenSeadragon.Tile) - the updated tiles}.
*/
- _onTileLoad: function( tile, time, data, errorMsg, tileRequest ) {
- if ( !data ) {
- $.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.getUrl(), errorMsg );
+ _updateLevel: function(level, levelOpacity,
+ levelVisibility, drawArea, currentTime, best) {
+
+ var topLeftBound = drawArea.getBoundingBox().getTopLeft();
+ var bottomRightBound = drawArea.getBoundingBox().getBottomRight();
+
+ if (this.viewer) {
/**
- * Triggered when a tile fails to load.
+ * - Needs documentation -
*
- * @event tile-load-failed
+ * @event update-level
* @memberof OpenSeadragon.Viewer
* @type {object}
- * @property {OpenSeadragon.Tile} tile - The tile that failed to load.
- * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to.
- * @property {number} time - The time in milliseconds when the tile load began.
- * @property {string} message - The error message.
- * @property {XMLHttpRequest} tileRequest - The XMLHttpRequest used to load the tile if available.
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn.
+ * @property {Object} havedrawn - deprecated, always true (kept for backwards compatibility)
+ * @property {Object} level
+ * @property {Object} opacity
+ * @property {Object} visibility
+ * @property {OpenSeadragon.Rect} drawArea
+ * @property {Object} topleft deprecated, use drawArea instead
+ * @property {Object} bottomright deprecated, use drawArea instead
+ * @property {Object} currenttime
+ * @property {Object[]} best
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
*/
- this.viewer.raiseEvent("tile-load-failed", {
- tile: tile,
+ this.viewer.raiseEvent('update-level', {
tiledImage: this,
- time: time,
- message: errorMsg,
- tileRequest: tileRequest
+ havedrawn: true, // deprecated, kept for backwards compatibility
+ level: level,
+ opacity: levelOpacity,
+ visibility: levelVisibility,
+ drawArea: drawArea,
+ topleft: topLeftBound,
+ bottomright: bottomRightBound,
+ currenttime: currentTime,
+ best: best
});
- tile.loading = false;
- tile.exists = false;
- return;
- } else {
- tile.exists = true;
- }
-
- if ( time < this.lastResetTime ) {
- $.console.warn( "Ignoring tile %s loaded before reset: %s", tile, tile.getUrl() );
- tile.loading = false;
- return;
}
- var _this = this,
- finish = function() {
- var ccc = _this.source;
- var cutoff = ccc.getClosestLevel();
- _this._setTileLoaded(tile, data, cutoff, tileRequest);
- };
+ this._resetCoverage(this.coverage, level);
+ this._resetCoverage(this.loadingCoverage, level);
- // Check if we're mid-update; this can happen on IE8 because image load events for
- // cached images happen immediately there
- if ( !this._midDraw ) {
- finish();
- } else {
- // Wait until after the update, in case caching unloads any tiles
- window.setTimeout( finish, 1);
- }
- },
+ //OK, a new drawing so do your calculations
+ var cornerTiles = this._getCornerTiles(level, topLeftBound, bottomRightBound);
+ var topLeftTile = cornerTiles.topLeft;
+ var bottomRightTile = cornerTiles.bottomRight;
+ var numberOfTiles = this.source.getNumTiles(level);
- /**
- * @private
- * @inner
- * @param {OpenSeadragon.Tile} tile
- * @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
- * @param {Number|undefined} cutoff
- * @param {XMLHttpRequest|undefined} tileRequest
- */
- _setTileLoaded: function(tile, data, cutoff, tileRequest) {
- var increment = 0,
- eventFinished = false,
- _this = this;
+ var viewportCenter = this.viewport.pixelFromPoint(this.viewport.getCenter());
- function getCompletionCallback() {
- if (eventFinished) {
- $.console.error("Event 'tile-loaded' argument getCompletionCallback must be called synchronously. " +
- "Its return value should be called asynchronously.");
+ if (this.getFlip()) {
+ // The right-most tile can be narrower than the others. When flipped,
+ // this tile is now on the left. Because it is narrower than the normal
+ // left-most tile, the subsequent tiles may not be wide enough to completely
+ // fill the viewport. Fix this by rendering an extra column of tiles. If we
+ // are not wrapping, make sure we never render more than the number of tiles
+ // in the image.
+ bottomRightTile.x += 1;
+ if (!this.wrapHorizontal) {
+ bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1);
}
- increment++;
- return completionCallback;
}
+ var numTiles = Math.max(0, (bottomRightTile.x - topLeftTile.x) * (bottomRightTile.y - topLeftTile.y));
+ var tiles = new Array(numTiles);
+ var tileIndex = 0;
+ for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) {
+ for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) {
- function completionCallback() {
- increment--;
- if (increment === 0) {
- tile.loading = false;
- tile.loaded = true;
- tile.hasTransparency = _this.source.hasTransparency(
- tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData
- );
- if (!tile.context2D) {
- _this._tileCache.cacheTile({
- data: data,
- tile: tile,
- cutoff: cutoff,
- tiledImage: _this
- });
+ var flippedX;
+ if (this.getFlip()) {
+ var xMod = ( numberOfTiles.x + ( x % numberOfTiles.x ) ) % numberOfTiles.x;
+ flippedX = x + numberOfTiles.x - xMod - xMod - 1;
+ } else {
+ flippedX = x;
}
- _this._needsDraw = true;
+
+ if (drawArea.intersection(this.getTileBounds(level, flippedX, y)) === null) {
+ // This tile is outside of the viewport, no need to draw it
+ continue;
+ }
+
+ var result = this._updateTile(
+ flippedX, y,
+ level,
+ levelVisibility,
+ viewportCenter,
+ numberOfTiles,
+ currentTime,
+ best
+ );
+ best = result.bestTiles;
+ tiles[tileIndex] = result.tile;
+ tileIndex += 1;
}
}
- /**
- * Triggered when a tile has just been loaded in memory. That means that the
- * image has been downloaded and can be modified before being drawn to the canvas.
- *
- * @event tile-loaded
- * @memberof OpenSeadragon.Viewer
- * @type {object}
- * @property {Image|*} image - The image (data) of the tile. Deprecated.
- * @property {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
- * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
- * @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
- * @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable).
- * @property {function} getCompletionCallback - A function giving a callback to call
- * when the asynchronous processing of the image is done. The image will be
- * marked as entirely loaded when the callback has been called once for each
- * call to getCompletionCallback.
- */
-
- var fallbackCompletion = getCompletionCallback();
- this.viewer.raiseEvent("tile-loaded", {
- tile: tile,
- tiledImage: this,
- tileRequest: tileRequest,
- get image() {
- $.console.error("[tile-loaded] event 'image' has been deprecated. Use 'data' property instead.");
- return data;
- },
- data: data,
- getCompletionCallback: getCompletionCallback
- });
- eventFinished = true;
- // In case the completion callback is never called, we at least force it once.
- fallbackCompletion();
+ return {
+ bestTiles: best,
+ updatedTiles: tiles
+ };
},
/**
* @private
- * @inner
* @param {OpenSeadragon.Tile} tile
* @param {Boolean} overlap
* @param {OpenSeadragon.Viewport} viewport
@@ -23561,6 +25910,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),
@@ -23568,16 +25922,18 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
tileCenter = positionT.plus( sizeT.divide( 2 ) ),
tileSquaredDistance = viewportCenter.squaredDistanceTo( tileCenter );
- if ( !overlap ) {
- sizeC = sizeC.plus( new $.Point( 1, 1 ) );
- }
+ if(this.viewer.drawer.minimumOverlapRequired(this)){
+ if ( !overlap ) {
+ sizeC = sizeC.plus( new $.Point(1, 1));
+ }
- if (tile.isRightMost && this.wrapHorizontal) {
- sizeC.x += 0.75; // Otherwise Firefox and Safari show seams
- }
+ if (tile.isRightMost && this.wrapHorizontal) {
+ sizeC.x += 0.75; // Otherwise Firefox and Safari show seams
+ }
- if (tile.isBottomMost && this.wrapVertical) {
- sizeC.y += 0.75; // Otherwise Firefox and Safari show seams
+ if (tile.isBottomMost && this.wrapVertical) {
+ sizeC.y += 0.75; // Otherwise Firefox and Safari show seams
+ }
}
tile.position = positionC;
@@ -23587,347 +25943,457 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
},
/**
+ * Update a single tile at a particular resolution level.
* @private
- * @inner
- * Updates the opacity of a tile according to the time it has been on screen
- * to perform a fade-in.
- * Updates coverage once a tile is fully opaque.
- * Returns whether the fade-in has completed.
- *
- * @param {OpenSeadragon.Tile} tile
* @param {Number} x
* @param {Number} y
* @param {Number} level
- * @param {Number} levelOpacity
+ * @param {Number} levelVisibility
+ * @param {OpenSeadragon.Point} viewportCenter
+ * @param {Number} numberOfTiles
* @param {Number} currentTime
- * @returns {Boolean}
+ * @param {OpenSeadragon.Tile} best - The current "best" tile to draw.
+ * @returns {Object} Dictionary {bestTiles: OpenSeadragon.Tile[] - the current best tiles, tile: OpenSeadragon.Tile the current tile}
*/
- _blendTile: function( tile, x, y, level, levelOpacity, currentTime ){
- var blendTimeMillis = 1000 * this.blendTime,
- deltaTime,
- opacity;
+ _updateTile: function( x, y, level,
+ levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
- if ( !tile.blendStart ) {
- tile.blendStart = currentTime;
+ var tile = this._getTile(
+ x, y,
+ level,
+ currentTime,
+ numberOfTiles
+ );
+
+ if( this.viewer ){
+ /**
+ * - Needs documentation -
+ *
+ * @event update-tile
+ * @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( 'update-tile', {
+ tiledImage: this,
+ tile: tile
+ });
}
- deltaTime = currentTime - tile.blendStart;
- opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1;
+ this._setCoverage( this.coverage, level, x, y, false );
- if ( this.alwaysBlend ) {
- opacity *= levelOpacity;
+ var loadingCoverage = tile.loaded || tile.loading || this._isCovered(this.loadingCoverage, level, x, y);
+ this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage);
+
+ if ( !tile.exists ) {
+ return {
+ bestTiles: best,
+ tile: tile
+ };
+ }
+ if (tile.loaded && tile.opacity === 1){
+ this._setCoverage( this.coverage, level, x, y, true );
+ }
+
+ this._positionTile(
+ tile,
+ this.source.tileOverlap,
+ this.viewport,
+ viewportCenter,
+ levelVisibility
+ );
+
+ if (!tile.loaded) {
+ if (tile.context2D) {
+ this._setTileLoaded(tile);
+ } else {
+ var imageRecord = this._tileCache.getImageRecord(tile.cacheKey);
+ if (imageRecord) {
+ this._setTileLoaded(tile, imageRecord.getData());
+ }
+ }
+ }
+
+ if ( tile.loading ) {
+ // the tile is already in the download queue
+ this._tilesLoading++;
+ } else if (!loadingCoverage) {
+ best = this._compareTiles( best, tile, this.maxTilesPerFrame );
+ }
+
+ return {
+ bestTiles: best,
+ tile: tile
+ };
+ },
+
+ // private
+ _getCornerTiles: function(level, topLeftBound, bottomRightBound) {
+ var leftX;
+ var rightX;
+ if (this.wrapHorizontal) {
+ leftX = $.positiveModulo(topLeftBound.x, 1);
+ rightX = $.positiveModulo(bottomRightBound.x, 1);
+ } else {
+ leftX = Math.max(0, topLeftBound.x);
+ rightX = Math.min(1, bottomRightBound.x);
+ }
+ var topY;
+ var bottomY;
+ var aspectRatio = 1 / this.source.aspectRatio;
+ if (this.wrapVertical) {
+ topY = $.positiveModulo(topLeftBound.y, aspectRatio);
+ bottomY = $.positiveModulo(bottomRightBound.y, aspectRatio);
+ } else {
+ topY = Math.max(0, topLeftBound.y);
+ bottomY = Math.min(aspectRatio, bottomRightBound.y);
}
- tile.opacity = opacity;
-
- this.lastDrawn.push( tile );
+ var topLeftTile = this.source.getTileAtPoint(level, new $.Point(leftX, topY));
+ var bottomRightTile = this.source.getTileAtPoint(level, new $.Point(rightX, bottomY));
+ var numTiles = this.source.getNumTiles(level);
- if ( opacity === 1 ) {
- this._setCoverage( this.coverage, level, x, y, true );
- this._hasOpaqueTile = true;
- } else if ( deltaTime < blendTimeMillis ) {
- return true;
+ if (this.wrapHorizontal) {
+ topLeftTile.x += numTiles.x * Math.floor(topLeftBound.x);
+ bottomRightTile.x += numTiles.x * Math.floor(bottomRightBound.x);
}
-
- return false;
- },
-
-
- /**
- * @private
- * @inner
- * Determines whether the 'last best' tile for the area is better than the
- * tile in question.
- *
- * @param {OpenSeadragon.Tile} previousBest
- * @param {OpenSeadragon.Tile} tile
- * @returns {OpenSeadragon.Tile} The new best tile.
- */
- _compareTiles: function( previousBest, tile ) {
- if ( !previousBest ) {
- return tile;
+ if (this.wrapVertical) {
+ topLeftTile.y += numTiles.y * Math.floor(topLeftBound.y / aspectRatio);
+ bottomRightTile.y += numTiles.y * Math.floor(bottomRightBound.y / aspectRatio);
}
- if ( tile.visibility > previousBest.visibility ) {
- return tile;
- } else if ( tile.visibility === previousBest.visibility ) {
- if ( tile.squaredDistance < previousBest.squaredDistance ) {
- return tile;
- }
- }
- return previousBest;
+ return {
+ topLeft: topLeftTile,
+ bottomRight: bottomRightTile,
+ };
},
/**
+ * Obtains a tile at the given location.
* @private
- * @inner
- * Draws a TiledImage.
- * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame.
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} level
+ * @param {Number} time
+ * @param {Number} numTiles
+ * @returns {OpenSeadragon.Tile}
*/
- _drawTiles: function( lastDrawn ) {
- if (this.opacity === 0 || (lastDrawn.length === 0 && !this.placeholderFillStyle)) {
- return;
- }
-
- var tile = lastDrawn[0];
- var useSketch;
+ _getTile: function(
+ x, y,
+ level,
+ time,
+ numTiles
+ ) {
+ var xMod,
+ yMod,
+ bounds,
+ sourceBounds,
+ exists,
+ urlOrGetter,
+ post,
+ ajaxHeaders,
+ context2D,
+ tile,
+ tilesMatrix = this.tilesMatrix,
+ tileSource = this.source;
- if (tile) {
- useSketch = this.opacity < 1 ||
- (this.compositeOperation && this.compositeOperation !== 'source-over') ||
- (!this._isBottomItem() &&
- this.source.hasTransparency(tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData));
+ if ( !tilesMatrix[ level ] ) {
+ tilesMatrix[ level ] = {};
}
-
- var sketchScale;
- var sketchTranslate;
-
- var zoom = this.viewport.getZoom(true);
- var imageZoom = this.viewportToImageZoom(zoom);
-
- if (lastDrawn.length > 1 &&
- imageZoom > this.smoothTileEdgesMinZoom &&
- !this.iOSDevice &&
- this.getRotation(true) % 360 === 0 && // TODO: support tile edge smoothing with tiled image rotation.
- $.supportsCanvas && this.viewer.useCanvas) {
- // When zoomed in a lot (>100%) the tile edges are visible.
- // So we have to composite them at ~100% and scale them up together.
- // Note: Disabled on iOS devices per default as it causes a native crash
- useSketch = true;
- sketchScale = tile.getScaleForEdgeSmoothing();
- sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale,
- this._drawer.getCanvasSize(false),
- this._drawer.getCanvasSize(true));
+ if ( !tilesMatrix[ level ][ x ] ) {
+ tilesMatrix[ level ][ x ] = {};
}
- var bounds;
- if (useSketch) {
- if (!sketchScale) {
- // Except when edge smoothing, we only clean the part of the
- // sketch canvas we are going to use for performance reasons.
- bounds = this.viewport.viewportToViewerElementRectangle(
- this.getClippedBounds(true))
- .getIntegerBoundingBox();
+ if ( !tilesMatrix[ level ][ x ][ y ] || !tilesMatrix[ level ][ x ][ y ].flipped !== !this.flipped ) {
+ xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x;
+ yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y;
+ bounds = this.getTileBounds( level, x, y );
+ sourceBounds = tileSource.getTileBounds( level, xMod, yMod, true );
+ exists = tileSource.tileExists( level, xMod, yMod );
+ urlOrGetter = tileSource.getTileUrl( level, xMod, yMod );
+ post = tileSource.getTilePostData( level, xMod, yMod );
- if(this._drawer.viewer.viewport.getFlip()) {
- if (this.viewport.getRotation(true) % 360 !== 0 ||
- this.getRotation(true) % 360 !== 0) {
- bounds.x = this._drawer.viewer.container.clientWidth - (bounds.x + bounds.width);
- }
+ // Headers are only applicable if loadTilesWithAjax is set
+ if (this.loadTilesWithAjax) {
+ ajaxHeaders = tileSource.getTileAjaxHeaders( level, xMod, yMod );
+ // Combine tile AJAX headers with tiled image AJAX headers (if applicable)
+ if ($.isPlainObject(this.ajaxHeaders)) {
+ ajaxHeaders = $.extend({}, this.ajaxHeaders, ajaxHeaders);
}
-
- bounds = bounds.times($.pixelDensityRatio);
+ } else {
+ ajaxHeaders = null;
}
- this._drawer._clear(true, bounds);
- }
- // When scaling, we must rotate only when blending the sketch canvas to
- // avoid interpolation
- if (!sketchScale) {
- if (this.viewport.getRotation(true) % 360 !== 0) {
- this._drawer._offsetForRotation({
- degrees: this.viewport.getRotation(true),
- useSketch: useSketch
- });
- }
- if (this.getRotation(true) % 360 !== 0) {
- this._drawer._offsetForRotation({
- degrees: this.getRotation(true),
- point: this.viewport.pixelFromPointNoRotate(
- this._getRotationPoint(true), true),
- useSketch: useSketch
- });
- }
+ context2D = tileSource.getContext2D ?
+ tileSource.getContext2D(level, xMod, yMod) : undefined;
+
+ tile = new $.Tile(
+ level,
+ x,
+ y,
+ bounds,
+ exists,
+ urlOrGetter,
+ context2D,
+ this.loadTilesWithAjax,
+ ajaxHeaders,
+ sourceBounds,
+ post,
+ tileSource.getTileHashKey(level, xMod, yMod, urlOrGetter, ajaxHeaders, post)
+ );
- if (this.viewport.getRotation(true) % 360 === 0 &&
- this.getRotation(true) % 360 === 0) {
- if(this._drawer.viewer.viewport.getFlip()) {
- this._drawer._flip();
+ if (this.getFlip()) {
+ if (xMod === 0) {
+ tile.isRightMost = true;
+ }
+ } else {
+ if (xMod === numTiles.x - 1) {
+ tile.isRightMost = true;
}
}
- }
-
- var usedClip = false;
- if ( this._clip ) {
- this._drawer.saveContext(useSketch);
- var box = this.imageToViewportRectangle(this._clip, true);
- box = box.rotate(-this.getRotation(true), this._getRotationPoint(true));
- var clipRect = this._drawer.viewportToDrawerRectangle(box);
- if (sketchScale) {
- clipRect = clipRect.times(sketchScale);
- }
- if (sketchTranslate) {
- clipRect = clipRect.translate(sketchTranslate);
+ if (yMod === numTiles.y - 1) {
+ tile.isBottomMost = true;
}
- this._drawer.setClip(clipRect, useSketch);
- usedClip = true;
- }
+ tile.flipped = this.flipped;
- if (this._croppingPolygons) {
- var self = this;
- this._drawer.saveContext(useSketch);
- try {
- var polygons = this._croppingPolygons.map(function (polygon) {
- return polygon.map(function (coord) {
- var point = self
- .imageToViewportCoordinates(coord.x, coord.y, true)
- .rotate(-self.getRotation(true), self._getRotationPoint(true));
- var clipPoint = self._drawer.viewportCoordToDrawerCoord(point);
- if (sketchScale) {
- clipPoint = clipPoint.times(sketchScale);
- }
- if (sketchTranslate) {
- clipPoint = clipPoint.plus(sketchTranslate);
- }
- return clipPoint;
- });
- });
- this._drawer.clipWithPolygons(polygons, useSketch);
- } catch (e) {
- $.console.error(e);
- }
- usedClip = true;
+ tilesMatrix[ level ][ x ][ y ] = tile;
}
- if ( this.placeholderFillStyle && this._hasOpaqueTile === false ) {
- var placeholderRect = this._drawer.viewportToDrawerRectangle(this.getBounds(true));
- if (sketchScale) {
- placeholderRect = placeholderRect.times(sketchScale);
- }
- if (sketchTranslate) {
- placeholderRect = placeholderRect.translate(sketchTranslate);
- }
+ tile = tilesMatrix[ level ][ x ][ y ];
+ tile.lastTouchTime = time;
- var fillStyle = null;
- if ( typeof this.placeholderFillStyle === "function" ) {
- fillStyle = this.placeholderFillStyle(this, this._drawer.context);
- }
- else {
- fillStyle = this.placeholderFillStyle;
+ return tile;
+ },
+
+ /**
+ * Dispatch a job to the ImageLoader to load the Image for a Tile.
+ * @private
+ * @param {OpenSeadragon.Tile} tile
+ * @param {Number} time
+ */
+ _loadTile: function(tile, time ) {
+ var _this = this;
+ tile.loading = true;
+ this._imageLoader.addJob({
+ src: tile.getUrl(),
+ tile: tile,
+ source: this.source,
+ postData: tile.postData,
+ loadWithAjax: tile.loadWithAjax,
+ ajaxHeaders: tile.ajaxHeaders,
+ crossOriginPolicy: this.crossOriginPolicy,
+ ajaxWithCredentials: this.ajaxWithCredentials,
+ callback: function( data, errorMsg, tileRequest ){
+ _this._onTileLoad( tile, time, data, errorMsg, tileRequest );
+ },
+ abort: function() {
+ tile.loading = false;
}
+ });
+ },
+
+ /**
+ * Callback fired when a Tile's Image finished downloading.
+ * @private
+ * @param {OpenSeadragon.Tile} tile
+ * @param {Number} time
+ * @param {*} data image data
+ * @param {String} errorMsg
+ * @param {XMLHttpRequest} tileRequest
+ */
+ _onTileLoad: function( tile, time, data, errorMsg, tileRequest ) {
+ if ( !data ) {
+ $.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.getUrl(), errorMsg );
+ /**
+ * Triggered when a tile fails to load.
+ *
+ * @event tile-load-failed
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Tile} tile - The tile that failed to load.
+ * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to.
+ * @property {number} time - The time in milliseconds when the tile load began.
+ * @property {string} message - The error message.
+ * @property {XMLHttpRequest} tileRequest - The XMLHttpRequest used to load the tile if available.
+ */
+ this.viewer.raiseEvent("tile-load-failed", {
+ tile: tile,
+ tiledImage: this,
+ time: time,
+ message: errorMsg,
+ tileRequest: tileRequest
+ });
+ tile.loading = false;
+ tile.exists = false;
+ return;
+ } else {
+ tile.exists = true;
+ }
- this._drawer.drawRectangle(placeholderRect, fillStyle, useSketch);
+ if ( time < this.lastResetTime ) {
+ $.console.warn( "Ignoring tile %s loaded before reset: %s", tile, tile.getUrl() );
+ tile.loading = false;
+ return;
}
- var subPixelRoundingRule = determineSubPixelRoundingRule(this.subPixelRoundingForTransparency);
+ var _this = this,
+ finish = function() {
+ var ccc = _this.source;
+ var cutoff = ccc.getClosestLevel();
+ _this._setTileLoaded(tile, data, cutoff, tileRequest);
+ };
- var shouldRoundPositionAndSize = false;
- if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) {
- shouldRoundPositionAndSize = true;
- } else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) {
- var isAnimating = this.viewer && this.viewer.isAnimating();
- shouldRoundPositionAndSize = !isAnimating;
- }
+ finish();
+ },
- for (var i = lastDrawn.length - 1; i >= 0; i--) {
- tile = lastDrawn[ i ];
- this._drawer.drawTile( tile, this._drawingHandler, useSketch, sketchScale,
- sketchTranslate, shouldRoundPositionAndSize, this.source );
- tile.beingDrawn = true;
+ /**
+ * @private
+ * @param {OpenSeadragon.Tile} tile
+ * @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
+ * @param {Number|undefined} cutoff
+ * @param {XMLHttpRequest|undefined} tileRequest
+ */
+ _setTileLoaded: function(tile, data, cutoff, tileRequest) {
+ var increment = 0,
+ eventFinished = false,
+ _this = this;
- if( this.viewer ){
+ function getCompletionCallback() {
+ if (eventFinished) {
+ $.console.error("Event 'tile-loaded' argument getCompletionCallback must be called synchronously. " +
+ "Its return value should be called asynchronously.");
+ }
+ increment++;
+ return completionCallback;
+ }
+
+ function completionCallback() {
+ increment--;
+ if (increment === 0) {
+ tile.loading = false;
+ tile.loaded = true;
+ tile.hasTransparency = _this.source.hasTransparency(
+ tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData
+ );
+ if (!tile.context2D) {
+ _this._tileCache.cacheTile({
+ data: data,
+ tile: tile,
+ cutoff: cutoff,
+ tiledImage: _this
+ });
+ }
/**
- * - Needs documentation -
+ * Triggered when a tile is loaded and pre-processing is compelete,
+ * and the tile is ready to draw.
*
- * @event tile-drawn
+ * @event tile-ready
* @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.
+ * @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
+ * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
+ * @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable).
+ * @private
*/
- this.viewer.raiseEvent( 'tile-drawn', {
- tiledImage: this,
- tile: tile
+ _this.viewer.raiseEvent("tile-ready", {
+ tile: tile,
+ tiledImage: _this,
+ tileRequest: tileRequest
});
+ _this._needsDraw = true;
}
}
- if ( usedClip ) {
- this._drawer.restoreContext( useSketch );
- }
+ /**
+ * Triggered when a tile has just been loaded in memory. That means that the
+ * image has been downloaded and can be modified before being drawn to the canvas.
+ *
+ * @event tile-loaded
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {Image|*} image - The image (data) of the tile. Deprecated.
+ * @property {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
+ * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
+ * @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
+ * @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable).
+ * @property {function} getCompletionCallback - A function giving a callback to call
+ * when the asynchronous processing of the image is done. The image will be
+ * marked as entirely loaded when the callback has been called once for each
+ * call to getCompletionCallback.
+ */
- if (!sketchScale) {
- if (this.getRotation(true) % 360 !== 0) {
- this._drawer._restoreRotationChanges(useSketch);
- }
- if (this.viewport.getRotation(true) % 360 !== 0) {
- this._drawer._restoreRotationChanges(useSketch);
- }
- }
+ var fallbackCompletion = getCompletionCallback();
+ this.viewer.raiseEvent("tile-loaded", {
+ tile: tile,
+ tiledImage: this,
+ tileRequest: tileRequest,
+ get image() {
+ $.console.error("[tile-loaded] event 'image' has been deprecated. Use 'data' property instead.");
+ return data;
+ },
+ data: data,
+ getCompletionCallback: getCompletionCallback
+ });
+ eventFinished = true;
+ // In case the completion callback is never called, we at least force it once.
+ fallbackCompletion();
+ },
- if (useSketch) {
- if (sketchScale) {
- if (this.viewport.getRotation(true) % 360 !== 0) {
- this._drawer._offsetForRotation({
- degrees: this.viewport.getRotation(true),
- useSketch: false
- });
- }
- if (this.getRotation(true) % 360 !== 0) {
- this._drawer._offsetForRotation({
- degrees: this.getRotation(true),
- point: this.viewport.pixelFromPointNoRotate(
- this._getRotationPoint(true), true),
- useSketch: false
- });
- }
- }
- this._drawer.blendSketch({
- opacity: this.opacity,
- scale: sketchScale,
- translate: sketchTranslate,
- compositeOperation: this.compositeOperation,
- bounds: bounds
- });
- if (sketchScale) {
- if (this.getRotation(true) % 360 !== 0) {
- this._drawer._restoreRotationChanges(false);
- }
- if (this.viewport.getRotation(true) % 360 !== 0) {
- this._drawer._restoreRotationChanges(false);
- }
- }
- }
- if (!sketchScale) {
- if (this.viewport.getRotation(true) % 360 === 0 &&
- this.getRotation(true) % 360 === 0) {
- if(this._drawer.viewer.viewport.getFlip()) {
- this._drawer._flip();
- }
- }
+ /**
+ * Determines the 'best tiles' from the given 'last best' tiles and the
+ * tile in question.
+ * @private
+ *
+ * @param {OpenSeadragon.Tile[]} previousBest The best tiles so far.
+ * @param {OpenSeadragon.Tile} tile The new tile to consider.
+ * @param {Number} maxNTiles The max number of best tiles.
+ * @returns {OpenSeadragon.Tile[]} The new best tiles.
+ */
+ _compareTiles: function( previousBest, tile, maxNTiles ) {
+ if ( !previousBest ) {
+ return [tile];
}
-
- this._drawDebugInfo( lastDrawn );
+ previousBest.push(tile);
+ this._sortTiles(previousBest);
+ if (previousBest.length > maxNTiles) {
+ previousBest.pop();
+ }
+ return previousBest;
},
/**
+ * Sorts tiles in an array according to distance and visibility.
* @private
- * @inner
- * Draws special debug information for a TiledImage if in debug mode.
- * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame.
+ *
+ * @param {OpenSeadragon.Tile[]} tiles The tiles.
*/
- _drawDebugInfo: function( lastDrawn ) {
- if( this.debugMode ) {
- for ( var i = lastDrawn.length - 1; i >= 0; i-- ) {
- var tile = lastDrawn[ i ];
- try {
- this._drawer.drawDebugInfo(tile, lastDrawn.length, i, this);
- } catch(e) {
- $.console.error(e);
- }
+ _sortTiles: function( tiles ) {
+ tiles.sort(function (a, b) {
+ if (a === null) {
+ return 1;
}
- }
+ if (b === null) {
+ return -1;
+ }
+ if (a.visibility === b.visibility) {
+ // sort by smallest squared distance
+ return (a.squaredDistance - b.squaredDistance);
+ } else {
+ // sort by largest visibility value
+ return (b.visibility - a.visibility);
+ }
+ });
},
+
/**
- * @private
- * @inner
* Returns true if the given tile provides coverage to lower-level tiles of
* lower resolution representing the same content. If neither x nor y is
* given, returns true if the entire visible level provides coverage.
@@ -23935,6 +26401,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* Note that out-of-bounds tiles provide coverage in this sense, since
* there's no content that they would need to cover. Tiles at non-existent
* levels that are within the image bounds, however, do not.
+ * @private
*
* @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean.
* @param {Number} level - The resolution level of the tile.
@@ -23975,11 +26442,10 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
},
/**
- * @private
- * @inner
* Returns true if the given tile is completely covered by higher-level
* tiles of higher resolution representing the same content. If neither x
* nor y is given, returns true if the entire visible level is covered.
+ * @private
*
* @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean.
* @param {Number} level - The resolution level of the tile.
@@ -24001,9 +26467,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
},
/**
- * @private
- * @inner
* Sets whether the given tile provides coverage or not.
+ * @private
*
* @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean.
* @param {Number} level - The resolution level of the tile.
@@ -24028,11 +26493,10 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
},
/**
- * @private
- * @inner
* Resets coverage information for the given level. This should be called
* after every draw routine. Note that at the beginning of the next draw
* routine, coverage for every visible tile should be explicitly set.
+ * @private
*
* @param {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean.
* @param {Number} level - The resolution level of tiles to completely reset.
@@ -24043,72 +26507,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
});
-/**
- * @private
- * @inner
- * Defines the value for subpixel rounding to fallback to in case of missing or
- * invalid value.
- */
-var DEFAULT_SUBPIXEL_ROUNDING_RULE = $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;
-
-/**
- * @private
- * @inner
- * Checks whether the input value is an invalid subpixel rounding enum value.
- *
- * @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to check.
- * @returns {Boolean} Returns true if the input value is none of the expected
- * {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS}, {@link SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST} or {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} value.
- */
-function isSubPixelRoundingRuleUnknown(value) {
- return value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS &&
- value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST &&
- value !== $.SUBPIXEL_ROUNDING_OCCURRENCES.NEVER;
-}
-
-/**
- * @private
- * @inner
- * Ensures the returned value is always a valid subpixel rounding enum value,
- * defaulting to {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} if input is missing or invalid.
- *
- * @param {SUBPIXEL_ROUNDING_OCCURRENCES} value - The subpixel rounding enum value to normalize.
- * @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns a valid subpixel rounding enum value.
- */
-function normalizeSubPixelRoundingRule(value) {
- if (isSubPixelRoundingRuleUnknown(value)) {
- return DEFAULT_SUBPIXEL_ROUNDING_RULE;
- }
- return value;
-}
-
-/**
- * @private
- * @inner
- * Ensures the returned value is always a valid subpixel rounding enum value,
- * defaulting to 'NEVER' if input is missing or invalid.
- *
- * @param {Object} subPixelRoundingRules - A subpixel rounding enum values dictionary [{@link BROWSERS}] --> {@link SUBPIXEL_ROUNDING_OCCURRENCES}.
- * @returns {SUBPIXEL_ROUNDING_OCCURRENCES} Returns the determined subpixel rounding enum value for the
- * current browser.
- */
-function determineSubPixelRoundingRule(subPixelRoundingRules) {
- if (typeof subPixelRoundingRules === 'number') {
- return normalizeSubPixelRoundingRule(subPixelRoundingRules);
- }
-
- if (!subPixelRoundingRules || !$.Browser) {
- return DEFAULT_SUBPIXEL_ROUNDING_RULE;
- }
-
- var subPixelRoundingRule = subPixelRoundingRules[$.Browser.vendor];
-
- if (isSubPixelRoundingRuleUnknown(subPixelRoundingRule)) {
- subPixelRoundingRule = subPixelRoundingRules['*'];
- }
-
- return normalizeSubPixelRoundingRule(subPixelRoundingRule);
-}
}( OpenSeadragon ));
@@ -24116,7 +26514,7 @@ function determineSubPixelRoundingRule(subPixelRoundingRules) {
* OpenSeadragon - TileCache
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -24350,19 +26748,52 @@ $.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
+ * @private
+ */
+ tiledImage.viewer.raiseEvent("image-unloaded", {
+ context2D: context2D,
+ tile: tile
+ });
+ }
+
}
/**
- * 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
@@ -24374,6 +26805,7 @@ $.TileCache.prototype = {
tile: tile,
tiledImage: tiledImage
});
+
}
};
@@ -24383,7 +26815,7 @@ $.TileCache.prototype = {
* OpenSeadragon - World
*
* Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2022 OpenSeadragon contributors
+ * Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -24623,11 +27055,14 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
/**
* Updates (i.e. animates bounds of) all items.
+ * @function
+ * @param viewportChanged Whether the viewport changed, which indicates that
+ * all TiledImages need to be updated.
*/
- update: function() {
+ update: function(viewportChanged) {
var animated = false;
for ( var i = 0; i < this._items.length; i++ ) {
- animated = this._items[i].update() || animated;
+ animated = this._items[i].update(viewportChanged) || animated;
}
return animated;
@@ -24637,11 +27072,11 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
* Draws all items.
*/
draw: function() {
- for ( var i = 0; i < this._items.length; i++ ) {
- this._items[i].draw();
- }
-
+ this.viewer.drawer.draw(this._items);
this._needsDraw = false;
+ this._items.forEach((item) => {
+ this._needsDraw = item.setDrawn() || this._needsDraw;
+ });
},
/**