diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 00000000..bc1e4a8b
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,39 @@
+cff-version: 1.2.0
+title: OpenSeadragon
+message: "If you use this software, please cite it using the metadata from this file."
+type: software
+authors:
+ - given-names: Ian
+ family-names: Gilman
+ email: ian@iangilman.com
+ - given-names: Aseem
+ family-names: Kishore
+ - given-names: Chris
+ family-names: Thatcher
+ - given-names: Mark
+ family-names: Salsbery
+ - given-names: Antoine
+ family-names: Vandecreme
+ - given-names: Thomas
+ family-names: Pearce
+identifiers:
+ - type: url
+ value: https://openseadragon.github.io/
+ description: Homepage
+ - type: url
+ value: https://github.com/openseadragon/openseadragon
+ description: Repository
+repository-code: https://github.com/openseadragon/openseadragon
+url: https://openseadragon.github.io/
+abstract: "An open-source, web-based viewer for high-resolution zoomable images, implemented in pure JavaScript, for desktop and mobile."
+keywords:
+ - javascript
+ - image
+ - zooming
+ - viewer
+ - image-viewer
+ - high-resolution
+ - iiif
+license: BSD-3-Clause
+version: 4.1.1
+date-released: 2024-04-01
diff --git a/Gruntfile.js b/Gruntfile.js
index 3111bd65..d526fabe 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -28,6 +28,7 @@ module.exports = function(grunt) {
coverageDir = 'coverage/' + dateFormat(new Date(), 'yyyymmdd-HHMMss'),
sources = [
"src/openseadragon.js",
+ "src/matrix3.js",
"src/fullscreen.js",
"src/eventsource.js",
"src/mousetracker.js",
@@ -57,11 +58,14 @@ module.exports = function(grunt) {
"src/imageloader.js",
"src/tile.js",
"src/overlay.js",
- "src/drawer.js",
+ "src/drawerbase.js",
+ "src/htmldrawer.js",
+ "src/canvasdrawer.js",
+ "src/webgldrawer.js",
"src/viewport.js",
"src/tiledimage.js",
"src/tilecache.js",
- "src/world.js"
+ "src/world.js",
];
var banner = "//! <%= pkg.name %> <%= pkg.version %>\n" +
diff --git a/LICENSE.txt b/LICENSE.txt
index 7c6df831..247d11af 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,5 +1,5 @@
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 met:
diff --git a/changelog.txt b/changelog.txt
index a8c64b4c..a479cd2d 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -5,10 +5,25 @@ OPENSEADRAGON CHANGELOG
* BREAKING CHANGE: Dropped support for IE11 (#2300, #2361 @AndrewADev)
* DEPRECATION: The OpenSeadragon.createCallback function is no longer recommended (#2367 @akansjain)
+* The viewer now uses WebGL when available (#2310, #2462, #2466, #2468, #2469, #2472, #2478, #2488, #2492 @pearcetm, @Aiosa, @thec0keman)
+* Added webp to supported image formats (#2455 @BeebBenjamin)
* Introduced maxTilesPerFrame option to allow loading more tiles simultaneously (#2387 @jetic83)
+* Now when creating a viewer or navigator, we leave its position style alone if possible (#2393 @VIRAT9358)
+* Added getter & setter for Viewport.maxZoomPixelRatio (#2506 @eug-L)
* Test improvements (#2382 @AndrewADev)
* MouseTracker options documentation fixes (#2389 @msalsbery)
+* Improved documentation and error message for Viewport.imageToViewportZoom (#2505 @eug-L)
+* Fixed documentation typos (#2507 @frameflare)
* Fixed: Sometimes if the viewport was flipped and the user zoomed in far enough, it would flip back (#2364 @SebDelile)
+* Fixed: Two-finger tap on a Mac trackpad would zoom you out (#2431 @cavenel)
+* Fixed: dragToPan gesture could not be disabled when flickEnabled was activated (#2464 @jonasengelmann)
+* Fixed: placeholderFillStyle didn't work properly when the image was rotated (#2469 @pearcetm)
+* Fixed: Sometimes exponential springs wouldn't ever settle (#2469 @pearcetm)
+* Fixed: The navigator wouldn't update its tracking rectangle when the navigator was resized (#2491 @pearcetm)
+
+4.1.1:
+
+* Fixed: Strange behavior if IIIF sizes were not in ascending order (#2416 @lutzhelm)
4.1.0:
diff --git a/package.json b/package.json
index b1228eba..7654a199 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openseadragon",
- "version": "4.1.0",
+ "version": "4.1.1",
"description": "Provides a smooth, zoomable user interface for HTML/Javascript.",
"keywords": [
"image",
@@ -48,4 +48,4 @@
"test": "grunt test",
"prepare": "grunt build"
}
-}
+}
\ No newline at end of file
diff --git a/src/button.js b/src/button.js
index e6c525d7..34d1f27a 100644
--- a/src/button.js
+++ b/src/button.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Button
*
* 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
diff --git a/src/buttongroup.js b/src/buttongroup.js
index 6cab6395..8a1b4c9e 100644
--- a/src/buttongroup.js
+++ b/src/buttongroup.js
@@ -2,7 +2,7 @@
* OpenSeadragon - ButtonGroup
*
* 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
diff --git a/src/canvasdrawer.js b/src/canvasdrawer.js
new file mode 100644
index 00000000..e755c97b
--- /dev/null
+++ b/src/canvasdrawer.js
@@ -0,0 +1,1065 @@
+/*
+ * 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;
+
+ this._viewportFlipped = false;
+
+ // 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
+ */
+ 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();
+ this._viewportFlipped = !this._viewportFlipped;
+ }
+ 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);
+ }
+
+ /**
+ * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams.
+ */
+ minimumOverlapRequired() {
+ 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();
+ }
+
+ /**
+ * 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 ));
diff --git a/src/control.js b/src/control.js
index 3cba9943..51fe020a 100644
--- a/src/control.js
+++ b/src/control.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Control
*
* 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
diff --git a/src/controldock.js b/src/controldock.js
index 84b178f5..31f44b52 100644
--- a/src/controldock.js
+++ b/src/controldock.js
@@ -2,7 +2,7 @@
* OpenSeadragon - ControlDock
*
* 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
@@ -59,7 +59,9 @@
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%';
}
diff --git a/src/displayrectangle.js b/src/displayrectangle.js
index 58610058..3c3284d5 100644
--- a/src/displayrectangle.js
+++ b/src/displayrectangle.js
@@ -2,7 +2,7 @@
* OpenSeadragon - DisplayRect
*
* 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
diff --git a/src/drawer.js b/src/drawer.js
deleted file mode 100644
index 2aa2c3ad..00000000
--- a/src/drawer.js
+++ /dev/null
@@ -1,767 +0,0 @@
-/*
- * OpenSeadragon - Drawer
- *
- * Copyright (C) 2009 CodePlex Foundation
- * Copyright (C) 2010-2023 OpenSeadragon contributors
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * - Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * - Neither the name of CodePlex Foundation nor the names of its
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
- * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-(function( $ ){
-
-/**
- * @class Drawer
- * @memberof OpenSeadragon
- * @classdesc Handles rendering of tiles 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.
- */
-$.Drawer = function( options ) {
-
- $.console.assert( options.viewer, "[Drawer] options.viewer is required" );
-
- //backward compatibility for positional args while preferring more
- //idiomatic javascript options object as the only argument
- var args = arguments;
-
- if( !$.isPlainObject( options ) ){
- options = {
- source: args[ 0 ], // Reference to Viewer tile source.
- viewport: args[ 1 ], // Reference to Viewer viewport.
- element: args[ 2 ] // Parent element.
- };
- }
-
- $.console.assert( options.viewport, "[Drawer] options.viewport is required" );
- $.console.assert( options.element, "[Drawer] options.element is required" );
-
- if ( options.source ) {
- $.console.error( "[Drawer] options.source is no longer accepted; use TiledImage instead" );
- }
-
- this.viewer = options.viewer;
- this.viewport = options.viewport;
- this.debugGridColor = typeof options.debugGridColor === 'string' ? [options.debugGridColor] : options.debugGridColor || $.DEFAULT_SETTINGS.debugGridColor;
- if (options.opacity) {
- $.console.error( "[Drawer] options.opacity is no longer accepted; set the opacity on the TiledImage instead" );
- }
-
- this.useCanvas = $.supportsCanvas && ( this.viewer ? this.viewer.useCanvas : true );
- /**
- * The parent element of this Drawer instance, passed in when the Drawer was created.
- * The parent of {@link OpenSeadragon.Drawer#canvas}.
- * @member {Element} container
- * @memberof OpenSeadragon.Drawer#
- */
- this.container = $.getElement( options.element );
- /**
- * A <canvas> element if the browser supports them, otherwise a <div> element.
- * Child element of {@link OpenSeadragon.Drawer#container}.
- * @member {Element} canvas
- * @memberof OpenSeadragon.Drawer#
- */
- this.canvas = $.makeNeutralElement( this.useCanvas ? "canvas" : "div" );
- /**
- * 2d drawing context for {@link OpenSeadragon.Drawer#canvas} if it's a <canvas> element, otherwise null.
- * @member {Object} context
- * @memberof OpenSeadragon.Drawer#
- */
- this.context = this.useCanvas ? this.canvas.getContext( "2d" ) : null;
-
- /**
- * 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;
-
- /**
- * @member {Element} element
- * @memberof OpenSeadragon.Drawer#
- * @deprecated Alias for {@link OpenSeadragon.Drawer#container}.
- */
- this.element = this.container;
-
- // We force our container to ltr because our drawing math doesn't work in rtl.
- // This issue only affects our canvas renderer, but we do it always for consistency.
- // Note that this means overlays you want to be rtl need to be explicitly set to rtl.
- this.container.dir = 'ltr';
-
- // check canvas available width and height, set canvas width and height such that the canvas backing store is set to the proper pixel density
- if (this.useCanvas) {
- var viewportSize = this._calculateCanvasSize();
- this.canvas.width = viewportSize.x;
- this.canvas.height = viewportSize.y;
- }
-
- this.canvas.style.width = "100%";
- this.canvas.style.height = "100%";
- this.canvas.style.position = "absolute";
- $.setElementOpacity( this.canvas, this.opacity, true );
- // Allow pointer events to pass through the canvas element so implicit
- // pointer capture works on touch devices
- $.setElementPointerEventsNone( this.canvas );
- $.setElementTouchActionNone( this.canvas );
-
- // explicit left-align
- this.container.style.textAlign = "left";
- this.container.appendChild( this.canvas );
-
- // Image smoothing for canvas rendering (only if canvas is used).
- // Canvas default is "true", so this will only be changed if user specified "false".
- this._imageSmoothingEnabled = true;
-};
-
-/** @lends OpenSeadragon.Drawer.prototype */
-$.Drawer.prototype = {
- // deprecated
- addOverlay: function( element, location, placement, onDraw ) {
- $.console.error("drawer.addOverlay is deprecated. Use viewer.addOverlay instead.");
- this.viewer.addOverlay( element, location, placement, onDraw );
- return this;
- },
-
- // deprecated
- updateOverlay: function( element, location, placement ) {
- $.console.error("drawer.updateOverlay is deprecated. Use viewer.updateOverlay instead.");
- this.viewer.updateOverlay( element, location, placement );
- return this;
- },
-
- // deprecated
- removeOverlay: function( element ) {
- $.console.error("drawer.removeOverlay is deprecated. Use viewer.removeOverlay instead.");
- this.viewer.removeOverlay( element );
- return this;
- },
-
- // deprecated
- clearOverlays: function() {
- $.console.error("drawer.clearOverlays is deprecated. Use viewer.clearOverlays instead.");
- this.viewer.clearOverlays();
- return this;
- },
-
- /**
- * This function converts the given point from to the drawer coordinate by
- * multiplying it with the pixel density.
- * This function does not take rotation into account, thus assuming provided
- * point is at 0 degree.
- * @param {OpenSeadragon.Point} point - the pixel point to convert
- * @returns {OpenSeadragon.Point} Point in drawer coordinate system.
- */
- viewportCoordToDrawerCoord: function(point) {
- var vpPoint = this.viewport.pixelFromPointNoRotate(point, true);
- return new $.Point(
- vpPoint.x * $.pixelDensityRatio,
- vpPoint.y * $.pixelDensityRatio
- );
- },
-
- /**
- * This function will create multiple polygon paths on the drawing context by provided polygons,
- * then clip the context to the paths.
- * @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: function (polygons, useSketch) {
- if (!this.useCanvas) {
- return;
- }
- var context = this._getContext(useSketch);
- context.beginPath();
- polygons.forEach(function (polygon) {
- polygon.forEach(function (coord, i) {
- context[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y);
- });
- });
- context.clip();
- },
-
- /**
- * Set the opacity of the drawer.
- * @param {Number} opacity
- * @returns {OpenSeadragon.Drawer} Chainable.
- */
- setOpacity: function( opacity ) {
- $.console.error("drawer.setOpacity is deprecated. Use tiledImage.setOpacity instead.");
- var world = this.viewer.world;
- for (var i = 0; i < world.getItemCount(); i++) {
- world.getItemAt( i ).setOpacity( opacity );
- }
- return this;
- },
-
- /**
- * Get the opacity of the drawer.
- * @returns {Number}
- */
- getOpacity: function() {
- $.console.error("drawer.getOpacity is deprecated. Use tiledImage.getOpacity instead.");
- var world = this.viewer.world;
- var maxOpacity = 0;
- for (var i = 0; i < world.getItemCount(); i++) {
- var opacity = world.getItemAt( i ).getOpacity();
- if ( opacity > maxOpacity ) {
- maxOpacity = opacity;
- }
- }
- return maxOpacity;
- },
-
- // deprecated
- needsUpdate: function() {
- $.console.error( "[Drawer.needsUpdate] this function is deprecated. Use World.needsDraw instead." );
- return this.viewer.world.needsDraw();
- },
-
- // deprecated
- numTilesLoaded: function() {
- $.console.error( "[Drawer.numTilesLoaded] this function is deprecated. Use TileCache.numTilesLoaded instead." );
- return this.viewer.tileCache.numTilesLoaded();
- },
-
- // deprecated
- reset: function() {
- $.console.error( "[Drawer.reset] this function is deprecated. Use World.resetItems instead." );
- this.viewer.world.resetItems();
- return this;
- },
-
- // deprecated
- update: function() {
- $.console.error( "[Drawer.update] this function is deprecated. Use Drawer.clear and World.draw instead." );
- this.clear();
- this.viewer.world.draw();
- return this;
- },
-
- /**
- * @returns {Boolean} True if rotation is supported.
- */
- canRotate: function() {
- return this.useCanvas;
- },
-
- /**
- * Destroy the drawer (unload current loaded tiles)
- */
- destroy: function() {
- //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;
- },
-
- /**
- * Clears the Drawer so it's ready to draw another frame.
- */
- clear: function() {
- this.canvas.innerHTML = "";
- if ( this.useCanvas ) {
- 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();
- }
- },
-
- _clear: function (useSketch, bounds) {
- if (!this.useCanvas) {
- return;
- }
- 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);
- }
- },
-
- /**
- * Scale from OpenSeadragon viewer rectangle to drawer rectangle
- * (ignoring rotation)
- * @param {OpenSeadragon.Rect} rectangle - The rectangle in viewport coordinate system.
- * @returns {OpenSeadragon.Rect} Rectangle in drawer coordinate system.
- */
- viewportToDrawerRectangle: function(rectangle) {
- var topLeft = this.viewport.pixelFromPointNoRotate(rectangle.getTopLeft(), true);
- var size = this.viewport.deltaPixelsFromPointsNoRotate(rectangle.getSize(), true);
-
- return new $.Rect(
- topLeft.x * $.pixelDensityRatio,
- topLeft.y * $.pixelDensityRatio,
- size.x * $.pixelDensityRatio,
- size.y * $.pixelDensityRatio
- );
- },
-
- /**
- * Draws the given tile.
- * @param {OpenSeadragon.Tile} tile - The tile to draw.
- * @param {Function} drawingHandler - Method for firing the drawing event if using canvas.
- * drawingHandler({context, tile, rendered})
- * @param {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: function( tile, drawingHandler, useSketch, scale, translate, shouldRoundPositionAndSize, source) {
- $.console.assert(tile, '[Drawer.drawTile] tile is required');
- $.console.assert(drawingHandler, '[Drawer.drawTile] drawingHandler 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 );
- }
- },
-
- _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 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;
- },
-
- // private
- saveContext: function( useSketch ) {
- if (!this.useCanvas) {
- return;
- }
-
- this._getContext( useSketch ).save();
- },
-
- // private
- restoreContext: function( useSketch ) {
- if (!this.useCanvas) {
- return;
- }
-
- this._getContext( useSketch ).restore();
- },
-
- // 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();
- },
-
- // 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();
- },
-
- /**
- * 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: function(opacity, scale, translate, compositeOperation) {
- var options = opacity;
- if (!$.isPlainObject(options)) {
- options = {
- opacity: opacity,
- scale: scale,
- translate: translate,
- compositeOperation: compositeOperation
- };
- }
- if (!this.useCanvas || !this.sketchCanvas) {
- return;
- }
- 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
- drawDebugInfo: function(tile, count, i, tiledImage) {
- if ( !this.useCanvas ) {
- return;
- }
-
- 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];
-
- if (this.viewport.getRotation(true) % 360 !== 0 ) {
- this._offsetForRotation({degrees: this.viewport.getRotation(true)});
- }
- if (tiledImage.getRotation(true) % 360 !== 0) {
- this._offsetForRotation({
- degrees: tiledImage.getRotation(true),
- point: tiledImage.viewport.pixelFromPointNoRotate(
- tiledImage._getRotationPoint(true), true)
- });
- }
- if (tiledImage.viewport.getRotation(true) % 360 === 0 &&
- tiledImage.getRotation(true) % 360 === 0) {
- if(tiledImage._drawer.viewer.viewport.getFlip()) {
- tiledImage._drawer._flip();
- }
- }
-
- context.strokeRect(
- tile.position.x * $.pixelDensityRatio,
- tile.position.y * $.pixelDensityRatio,
- tile.size.x * $.pixelDensityRatio,
- tile.size.y * $.pixelDensityRatio
- );
-
- var tileCenterX = (tile.position.x + (tile.size.x / 2)) * $.pixelDensityRatio;
- var tileCenterY = (tile.position.y + (tile.size.y / 2)) * $.pixelDensityRatio;
-
- // Rotate the text the right way around.
- context.translate( tileCenterX, tileCenterY );
- context.rotate( Math.PI / 180 * -this.viewport.getRotation(true) );
- context.translate( -tileCenterX, -tileCenterY );
-
- if( tile.x === 0 && tile.y === 0 ){
- context.fillText(
- "Zoom: " + this.viewport.getZoom(),
- tile.position.x * $.pixelDensityRatio,
- (tile.position.y - 30) * $.pixelDensityRatio
- );
- context.fillText(
- "Pan: " + this.viewport.getBounds().toString(),
- tile.position.x * $.pixelDensityRatio,
- (tile.position.y - 20) * $.pixelDensityRatio
- );
- }
- context.fillText(
- "Level: " + tile.level,
- (tile.position.x + 10) * $.pixelDensityRatio,
- (tile.position.y + 20) * $.pixelDensityRatio
- );
- context.fillText(
- "Column: " + tile.x,
- (tile.position.x + 10) * $.pixelDensityRatio,
- (tile.position.y + 30) * $.pixelDensityRatio
- );
- context.fillText(
- "Row: " + tile.y,
- (tile.position.x + 10) * $.pixelDensityRatio,
- (tile.position.y + 40) * $.pixelDensityRatio
- );
- context.fillText(
- "Order: " + i + " of " + count,
- (tile.position.x + 10) * $.pixelDensityRatio,
- (tile.position.y + 50) * $.pixelDensityRatio
- );
- context.fillText(
- "Size: " + tile.size.toString(),
- (tile.position.x + 10) * $.pixelDensityRatio,
- (tile.position.y + 60) * $.pixelDensityRatio
- );
- context.fillText(
- "Position: " + tile.position.toString(),
- (tile.position.x + 10) * $.pixelDensityRatio,
- (tile.position.y + 70) * $.pixelDensityRatio
- );
-
- if (this.viewport.getRotation(true) % 360 !== 0 ) {
- this._restoreRotationChanges();
- }
- if (tiledImage.getRotation(true) % 360 !== 0) {
- this._restoreRotationChanges();
- }
-
- if (tiledImage.viewport.getRotation(true) % 360 === 0 &&
- tiledImage.getRotation(true) % 360 === 0) {
- if(tiledImage._drawer.viewer.viewport.getFlip()) {
- tiledImage._drawer._flip();
- }
- }
-
- context.restore();
- },
-
- // private
- 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
- );
-
- 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();
- }
- },
-
- // private
- _updateImageSmoothingEnabled: function(context){
- context.msImageSmoothingEnabled = this._imageSmoothingEnabled;
- context.imageSmoothingEnabled = this._imageSmoothingEnabled;
- },
-
- /**
- * 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);
- },
-
- getCanvasCenter: function() {
- return new $.Point(this.canvas.width / 2, this.canvas.height / 2);
- },
-
- // private
- _offsetForRotation: function(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);
- 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);
- },
-
- // private
- _flip: function(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: function(useSketch) {
- var context = this._getContext(useSketch);
- context.restore();
- },
-
- // 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)
- };
- },
-
- // private
- _calculateSketchCanvasSize: function() {
- 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
- };
- }
-};
-
-}( OpenSeadragon ));
diff --git a/src/drawerbase.js b/src/drawerbase.js
new file mode 100644
index 00000000..0a0d04ce
--- /dev/null
+++ b/src/drawerbase.js
@@ -0,0 +1,316 @@
+/*
+ * OpenSeadragon - DrawerBase
+ *
+ * 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.DrawerBase
+ * @classdesc Base class for Drawers that handle rendering of tiles 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 {HTMLElement} options.element - Parent element.
+ * @abstract
+ */
+
+OpenSeadragon.DrawerBase = class DrawerBase{
+ constructor(options){
+ $.console.assert( options.viewer, "[Drawer] options.viewer is required" );
+ $.console.assert( options.viewport, "[Drawer] options.viewport is required" );
+ $.console.assert( options.element, "[Drawer] options.element is required" );
+
+ this.viewer = options.viewer;
+ this.viewport = options.viewport;
+ this.debugGridColor = typeof options.debugGridColor === 'string' ? [options.debugGridColor] : options.debugGridColor || $.DEFAULT_SETTINGS.debugGridColor;
+ this.options = options.options || {};
+
+ this.container = $.getElement( options.element );
+
+ this._renderingTarget = this._createDrawingElement();
+
+
+ this.canvas.style.width = "100%";
+ this.canvas.style.height = "100%";
+ this.canvas.style.position = "absolute";
+ // set canvas.style.left = 0 so the canvas is positioned properly in ltr and rtl html
+ this.canvas.style.left = "0";
+ $.setElementOpacity( this.canvas, this.viewer.opacity, true );
+
+ // Allow pointer events to pass through the canvas element so implicit
+ // pointer capture works on touch devices
+ $.setElementPointerEventsNone( this.canvas );
+ $.setElementTouchActionNone( this.canvas );
+
+ // explicit left-align
+ this.container.style.textAlign = "left";
+ this.container.appendChild( this.canvas );
+
+ this._checkForAPIOverrides();
+ }
+
+ // protect the canvas member with a getter
+ get canvas(){
+ return this._renderingTarget;
+ }
+ get element(){
+ $.console.error('Drawer.element is deprecated. Use Drawer.container instead.');
+ return this.container;
+ }
+
+ /**
+ * @abstract
+ * @returns {String | undefined} What type of drawer this is. Must be overridden by extending classes.
+ */
+ getType(){
+ $.console.error('Drawer.getType must be implemented by child class');
+ return undefined;
+ }
+
+ /**
+ * @abstract
+ * @returns {Boolean} Whether the drawer implementation is supported by the browser. Must be overridden by extending classes.
+ */
+ static isSupported() {
+ $.console.error('Drawer.isSupported must be implemented by child class');
+ }
+
+ /**
+ * @abstract
+ * @returns {Element} the element to draw into
+ * @private
+ */
+ _createDrawingElement() {
+ $.console.error('Drawer._createDrawingElement must be implemented by child class');
+ return null;
+ }
+
+ /**
+ * @abstract
+ * @param {Array} tiledImages - An array of TiledImages that are ready to be drawn.
+ * @private
+ */
+ draw(tiledImages) {
+ $.console.error('Drawer.draw must be implemented by child class');
+ }
+
+ /**
+ * @abstract
+ * @returns {Boolean} True if rotation is supported.
+ */
+ canRotate() {
+ $.console.error('Drawer.canRotate must be implemented by child class');
+ }
+
+ /**
+ * @abstract
+ */
+ destroy() {
+ $.console.error('Drawer.destroy must be implemented by child class');
+ }
+
+ /**
+ * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams.
+ * @private
+ */
+ minimumOverlapRequired() {
+ return false;
+ }
+
+
+ /**
+ * @abstract
+ * @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){
+ $.console.error('Drawer.setImageSmoothingEnabled must be implemented by child class');
+ }
+
+ /**
+ * Optional public API to draw a rectangle (e.g. for debugging purposes)
+ * Child classes can override this method if they wish to support this
+ * @param {OpenSeadragon.Rect} rect
+ */
+ drawDebuggingRect(rect) {
+ $.console.warn('[drawer].drawDebuggingRect is not implemented by this drawer');
+ }
+
+ // Deprecated functions
+ clear(){
+ $.console.warn('[drawer].clear() is deprecated. The drawer is responsible for clearing itself as needed before drawing tiles.');
+ }
+
+ // Private functions
+
+ /**
+ * Ensures that child classes have provided implementations for public API methods
+ * draw, canRotate, destroy, and setImageSmoothinEnabled. Throws an exception if the original
+ * placeholder methods are still in place.
+ * @private
+ *
+ */
+ _checkForAPIOverrides(){
+ if(this._createDrawingElement === $.DrawerBase.prototype._createDrawingElement){
+ throw(new Error("[drawer]._createDrawingElement must be implemented by child class"));
+ }
+ if(this.draw === $.DrawerBase.prototype.draw){
+ throw(new Error("[drawer].draw must be implemented by child class"));
+ }
+ if(this.canRotate === $.DrawerBase.prototype.canRotate){
+ throw(new Error("[drawer].canRotate must be implemented by child class"));
+ }
+ if(this.destroy === $.DrawerBase.prototype.destroy){
+ throw(new Error("[drawer].destroy must be implemented by child class"));
+ }
+ if(this.setImageSmoothingEnabled === $.DrawerBase.prototype.setImageSmoothingEnabled){
+ throw(new Error("[drawer].setImageSmoothingEnabled must be implemented by child class"));
+ }
+ }
+
+
+ // Utility functions
+
+ /**
+ * Scale from OpenSeadragon viewer rectangle to drawer rectangle
+ * (ignoring rotation)
+ * @param {OpenSeadragon.Rect} rectangle - The rectangle in viewport coordinate system.
+ * @returns {OpenSeadragon.Rect} Rectangle in drawer coordinate system.
+ */
+ viewportToDrawerRectangle(rectangle) {
+ var topLeft = this.viewport.pixelFromPointNoRotate(rectangle.getTopLeft(), true);
+ var size = this.viewport.deltaPixelsFromPointsNoRotate(rectangle.getSize(), true);
+
+ return new $.Rect(
+ topLeft.x * $.pixelDensityRatio,
+ topLeft.y * $.pixelDensityRatio,
+ size.x * $.pixelDensityRatio,
+ size.y * $.pixelDensityRatio
+ );
+ }
+
+ /**
+ * This function converts the given point from to the drawer coordinate by
+ * multiplying it with the pixel density.
+ * This function does not take rotation into account, thus assuming provided
+ * point is at 0 degree.
+ * @param {OpenSeadragon.Point} point - the pixel point to convert
+ * @returns {OpenSeadragon.Point} Point in drawer coordinate system.
+ */
+ viewportCoordToDrawerCoord(point) {
+ var vpPoint = this.viewport.pixelFromPointNoRotate(point, true);
+ return new $.Point(
+ vpPoint.x * $.pixelDensityRatio,
+ vpPoint.y * $.pixelDensityRatio
+ );
+ }
+
+
+ // Internal utility functions
+
+ /**
+ * Calculate width and height of the canvas based on viewport dimensions
+ * and pixelDensityRatio
+ * @private
+ * @returns {OpenSeadragon.Point} {x, y} size of the canvas
+ */
+ _calculateCanvasSize() {
+ var pixelDensityRatio = $.pixelDensityRatio;
+ var viewportSize = this.viewport.getContainerSize();
+ return new OpenSeadragon.Point( Math.round(viewportSize.x * pixelDensityRatio), Math.round(viewportSize.y * pixelDensityRatio));
+ }
+
+ /**
+ * Called by implementations to fire the tiled-image-drawn event (used by tests)
+ * @private
+ */
+ _raiseTiledImageDrawnEvent(tiledImage, tiles){
+ if(!this.viewer) {
+ return;
+ }
+
+ /**
+ * Raised when a tiled image is drawn to the canvas. Used internally for testing.
+ * The update-viewport event is preferred if you want to know when a frame has been drawn.
+ *
+ * @event tiled-image-drawn
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn.
+ * @property {Array} tiles - An array of Tile objects that were drawn.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ * @private
+ */
+ this.viewer.raiseEvent( 'tiled-image-drawn', {
+ tiledImage: tiledImage,
+ tiles: tiles,
+ });
+ }
+
+ /**
+ * Called by implementations to fire the drawer-error event
+ * @private
+ */
+ _raiseDrawerErrorEvent(tiledImage, errorMessage){
+ if(!this.viewer) {
+ return;
+ }
+
+ /**
+ * Raised when a tiled image is drawn to the canvas. Used internally for testing.
+ * The update-viewport event is preferred if you want to know when a frame has been drawn.
+ *
+ * @event drawer-error
+ * @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.DrawerBase} drawer - The drawer that raised the error.
+ * @property {String} error - A message describing the error.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ * @private
+ */
+ this.viewer.raiseEvent( 'drawer-error', {
+ tiledImage: tiledImage,
+ drawer: this,
+ error: errorMessage,
+ });
+ }
+
+
+};
+
+}( OpenSeadragon ));
diff --git a/src/dzitilesource.js b/src/dzitilesource.js
index 57b9f5a3..5ee28bfc 100644
--- a/src/dzitilesource.js
+++ b/src/dzitilesource.js
@@ -2,7 +2,7 @@
* OpenSeadragon - DziTileSource
*
* 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
diff --git a/src/eventsource.js b/src/eventsource.js
index 94949f0f..0edea517 100644
--- a/src/eventsource.js
+++ b/src/eventsource.js
@@ -2,7 +2,7 @@
* OpenSeadragon - EventSource
*
* 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
@@ -51,6 +51,7 @@
*/
$.EventSource = function() {
this.events = {};
+ this._rejectedEventList = {};
};
/** @lends OpenSeadragon.EventSource.prototype */
@@ -68,6 +69,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;
@@ -80,7 +82,7 @@ $.EventSource.prototype = {
}
return handler(event);
};
- this.addHandler(eventName, onceHandler, userData, priority);
+ return this.addHandler(eventName, onceHandler, userData, priority);
},
/**
@@ -90,8 +92,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 = [];
@@ -106,6 +115,7 @@ $.EventSource.prototype = {
index--;
}
}
+ return true;
},
/**
@@ -191,17 +201,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 ));
diff --git a/src/fullscreen.js b/src/fullscreen.js
index 1b80464c..4af8630d 100644
--- a/src/fullscreen.js
+++ b/src/fullscreen.js
@@ -2,7 +2,7 @@
* OpenSeadragon - full-screen support functions
*
* 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
diff --git a/src/htmldrawer.js b/src/htmldrawer.js
new file mode 100644
index 00000000..80f851e4
--- /dev/null
+++ b/src/htmldrawer.js
@@ -0,0 +1,259 @@
+/*
+ * OpenSeadragon - HTMLDrawer
+ *
+ * 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 back for JSDoc
+
+/**
+ * @class OpenSeadragon.HTMLDrawer
+ * @extends OpenSeadragon.DrawerBase
+ * @classdesc HTML-based implementation of DrawerBase 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 HTMLDrawer extends OpenSeadragon.DrawerBase{
+ constructor(options){
+ super(options);
+
+ /**
+ * The HTML element (div) that this drawer uses for drawing
+ * @member {Element} canvas
+ * @memberof OpenSeadragon.HTMLDrawer#
+ */
+
+ /**
+ * 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.HTMLDrawer#
+ */
+
+ // Reject listening for the tile-drawing event, which this drawer does not fire
+ this.viewer.rejectEventHandler("tile-drawing", "The HTMLDrawer does not raise the tile-drawing event");
+ // Since the tile-drawn event is fired by this drawer, make sure handlers can be added for it
+ this.viewer.allowEventHandler("tile-drawn");
+ }
+
+ /**
+ * @returns {Boolean} always true
+ */
+ static isSupported(){
+ return true;
+ }
+
+ /**
+ *
+ * @returns 'html'
+ */
+ getType(){
+ return 'html';
+ }
+
+ /**
+ * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams.
+ */
+ minimumOverlapRequired() {
+ return true;
+ }
+
+ /**
+ * create the HTML element (e.g. canvas, div) that the image will be drawn into
+ * @returns {Element} the div to draw into
+ */
+ _createDrawingElement(){
+ let canvas = $.makeNeutralElement("div");
+ return canvas;
+ }
+
+ /**
+ * Draws the TiledImages
+ */
+ draw(tiledImages) {
+ var _this = this;
+ this._prepareNewFrame(); // prepare to draw a new frame
+ tiledImages.forEach(function(tiledImage){
+ if (tiledImage.opacity !== 0) {
+ _this._drawTiles(tiledImage);
+ }
+ });
+
+ }
+
+ /**
+ * @returns {Boolean} False - rotation is not supported.
+ */
+ canRotate() {
+ return false;
+ }
+
+ /**
+ * Destroy the drawer (unload current loaded tiles)
+ */
+ destroy() {
+ this.container.removeChild(this.canvas);
+ }
+
+ /**
+ * This function is ignored by the HTML Drawer. Implementing it is required by DrawerBase.
+ * @param {Boolean} [imageSmoothingEnabled] - Whether or not the image is
+ * drawn smoothly on the canvas; see imageSmoothingEnabled in
+ * {@link OpenSeadragon.Options} for more explanation.
+ */
+ setImageSmoothingEnabled(){
+ // noop - HTML Drawer does not deal with this property
+ }
+
+ /**
+ * Clears the Drawer so it's ready to draw another frame.
+ * @private
+ *
+ */
+ _prepareNewFrame() {
+ this.canvas.innerHTML = "";
+ }
+
+ /**
+ * Draws a TiledImage.
+ * @private
+ *
+ */
+ _drawTiles( tiledImage ) {
+ var lastDrawn = tiledImage.getTilesToDraw().map(info => info.tile);
+ if (tiledImage.opacity === 0 || (lastDrawn.length === 0 && !tiledImage.placeholderFillStyle)) {
+ return;
+ }
+
+ // Iterate over the tiles to draw, and draw them
+ for (var i = lastDrawn.length - 1; i >= 0; i--) {
+ var tile = lastDrawn[ i ];
+ this._drawTile( tile );
+
+ 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
+ });
+ }
+ }
+
+ }
+
+ /**
+ * Draws the given tile.
+ * @private
+ * @param {OpenSeadragon.Tile} tile - The tile to draw.
+ * @param {Function} drawingHandler - Method for firing the drawing event if using canvas.
+ * drawingHandler({context, tile, rendered})
+ */
+ _drawTile( tile ) {
+ $.console.assert(tile, '[Drawer._drawTile] tile is required');
+
+ let container = this.canvas;
+
+ if (!tile.cacheImageRecord) {
+ $.console.warn(
+ '[Drawer._drawTileToHTML] attempting to draw tile %s when it\'s not cached',
+ tile.toString());
+ return;
+ }
+
+ if ( !tile.loaded ) {
+ $.console.warn(
+ "Attempting to draw tile %s when it's not yet loaded.",
+ tile.toString()
+ );
+ return;
+ }
+
+ //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;
+ }
+
+ 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";
+ }
+
+ if ( tile.element.parentNode !== container ) {
+ container.appendChild( tile.element );
+ }
+ if ( tile.imgElement.parentNode !== tile.element ) {
+ tile.element.appendChild( tile.imgElement );
+ }
+
+ 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";
+
+ if (tile.flipped) {
+ tile.style.transform = "scaleX(-1)";
+ }
+
+ $.setElementOpacity( tile.element, tile.opacity );
+ }
+
+}
+
+$.HTMLDrawer = HTMLDrawer;
+
+
+}( OpenSeadragon ));
diff --git a/src/iiiftilesource.js b/src/iiiftilesource.js
index 6db4e321..1433a968 100644
--- a/src/iiiftilesource.js
+++ b/src/iiiftilesource.js
@@ -2,7 +2,7 @@
* 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
@@ -145,7 +145,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} );
diff --git a/src/imageloader.js b/src/imageloader.js
index e66bd820..00e413c1 100644
--- a/src/imageloader.js
+++ b/src/imageloader.js
@@ -2,7 +2,7 @@
* OpenSeadragon - ImageLoader
*
* 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
@@ -37,6 +37,8 @@
/**
* @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.
@@ -87,6 +89,7 @@ $.ImageJob.prototype = {
/**
* Starts the image job.
* @method
+ * @memberof OpenSeadragon.ImageJob#
*/
start: function() {
this.tries++;
@@ -113,6 +116,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;
diff --git a/src/imagetilesource.js b/src/imagetilesource.js
index e3b7a43b..4ebc7591 100644
--- a/src/imagetilesource.js
+++ b/src/imagetilesource.js
@@ -2,7 +2,7 @@
* OpenSeadragon - ImageTileSource
*
* 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
@@ -42,8 +42,8 @@
* 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
@@ -58,16 +58,13 @@
* 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]);
@@ -198,9 +195,11 @@
/**
* 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
@@ -214,7 +213,7 @@
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;
@@ -270,11 +269,26 @@
* 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
+ */
+ viewer.raiseEvent("image-unloaded", {
+ context2D: this.levels[i].context2D
+ });
+ }
+
}
}
},
diff --git a/src/legacytilesource.js b/src/legacytilesource.js
index e80b931f..f500d728 100644
--- a/src/legacytilesource.js
+++ b/src/legacytilesource.js
@@ -2,7 +2,7 @@
* OpenSeadragon - LegacyTileSource
*
* 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
diff --git a/src/matrix3.js b/src/matrix3.js
new file mode 100644
index 00000000..b4ba0f9b
--- /dev/null
+++ b/src/matrix3.js
@@ -0,0 +1,209 @@
+/*
+ * 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 ));
diff --git a/src/mousetracker.js b/src/mousetracker.js
index 20200b31..2a0b6a3c 100644
--- a/src/mousetracker.js
+++ b/src/mousetracker.js
@@ -2,7 +2,7 @@
* OpenSeadragon - MouseTracker
*
* 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
@@ -357,7 +357,7 @@
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++ ) {
@@ -367,7 +367,9 @@
}
list = new $.MouseTracker.GesturePointList( type );
- delegate.activePointersLists.push( list );
+ if(delegate){
+ delegate.activePointersLists.push( list );
+ }
return list;
},
@@ -2070,7 +2072,7 @@
// 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,
diff --git a/src/navigator.js b/src/navigator.js
index 0665b58d..fb95e4b1 100644
--- a/src/navigator.js
+++ b/src/navigator.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Navigator
*
* 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
@@ -170,9 +170,6 @@ $.Navigator = function( options ){
style.border = borderWidth + 'px solid ' + options.displayRegionColor;
style.margin = '0px';
style.padding = '0px';
- //TODO: IE doesn't like this property being set
- //try{ style.outline = '2px auto #909'; }catch(e){/*ignore*/}
-
style.background = 'transparent';
// We use square bracket notation on the statement below, because float is a keyword.
@@ -310,8 +307,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);
}
}
},
@@ -358,7 +356,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 ) {
@@ -369,6 +367,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;
diff --git a/src/openseadragon.js b/src/openseadragon.js
index 42ad0927..c7a44fe1 100644
--- a/src/openseadragon.js
+++ b/src/openseadragon.js
@@ -2,7 +2,7 @@
* OpenSeadragon
*
* 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
@@ -190,6 +190,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.
@@ -204,9 +214,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.
@@ -508,7 +518,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
@@ -793,6 +803,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.
*
@@ -1388,12 +1408,32 @@ function OpenSeadragon( options ){
flipped: false,
// 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,
@@ -1427,7 +1467,6 @@ function OpenSeadragon( options ){
imageLoaderLimit: 0,
maxImageCacheCount: 200,
timeout: 30000,
- useCanvas: true, // Use canvas element for drawing if available
tileRetryMax: 0,
tileRetryDelay: 2500,
@@ -1497,16 +1536,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
@@ -2667,13 +2696,14 @@ function OpenSeadragon( options ){
* 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.
*/
@@ -2733,7 +2763,8 @@ function OpenSeadragon( options ){
jpg: true,
png: true,
tif: false,
- wdp: false
+ wdp: false,
+ webp: true
},
URLPARAMS = {};
diff --git a/src/osmtilesource.js b/src/osmtilesource.js
index 43e525ab..4f9d1f35 100644
--- a/src/osmtilesource.js
+++ b/src/osmtilesource.js
@@ -2,7 +2,7 @@
* OpenSeadragon - OsmTileSource
*
* 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
diff --git a/src/overlay.js b/src/overlay.js
index bc25a051..7dec6a6b 100644
--- a/src/overlay.js
+++ b/src/overlay.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Overlay
*
* 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
diff --git a/src/placement.js b/src/placement.js
index 561d5daf..2db3e2b1 100644
--- a/src/placement.js
+++ b/src/placement.js
@@ -1,7 +1,7 @@
/*
* 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
diff --git a/src/point.js b/src/point.js
index 8df11d63..cc831b26 100644
--- a/src/point.js
+++ b/src/point.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Point
*
* 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
diff --git a/src/profiler.js b/src/profiler.js
index d0ffc2b2..01566bc0 100644
--- a/src/profiler.js
+++ b/src/profiler.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Profiler
*
* 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
diff --git a/src/rectangle.js b/src/rectangle.js
index fdeab18f..d154691d 100644
--- a/src/rectangle.js
+++ b/src/rectangle.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Rect
*
* 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
diff --git a/src/referencestrip.js b/src/referencestrip.js
index 1646f0c0..4cb66fe3 100644
--- a/src/referencestrip.js
+++ b/src/referencestrip.js
@@ -2,7 +2,7 @@
* OpenSeadragon - ReferenceStrip
*
* 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
@@ -46,7 +46,7 @@ const 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.
*
@@ -589,6 +589,7 @@ function loadPanels( strip, viewerSize, scroll ) {
animationTime: 0,
loadTilesWithAjax: strip.viewer.loadTilesWithAjax,
ajaxHeaders: strip.viewer.ajaxHeaders,
+ 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
diff --git a/src/spring.js b/src/spring.js
index 12592889..77162188 100644
--- a/src/spring.js
+++ b/src/spring.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Spring
*
* 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
@@ -206,12 +206,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;
@@ -220,24 +221,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;
},
/**
diff --git a/src/strings.js b/src/strings.js
index e423bef9..9d2b30d7 100644
--- a/src/strings.js
+++ b/src/strings.js
@@ -2,7 +2,7 @@
* OpenSeadragon - getString/setString
*
* 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
diff --git a/src/tile.js b/src/tile.js
index 201034fe..0b6d5fb2 100644
--- a/src/tile.js
+++ b/src/tile.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Tile
*
* 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
@@ -81,6 +81,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.
@@ -274,64 +280,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
@@ -382,114 +330,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());
},
/**
diff --git a/src/tilecache.js b/src/tilecache.js
index d890b8a8..f85cb643 100644
--- a/src/tilecache.js
+++ b/src/tilecache.js
@@ -2,7 +2,7 @@
* OpenSeadragon - TileCache
*
* 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
@@ -236,19 +236,51 @@ $.TileCache.prototype = {
var tile = tileRecord.tile;
var tiledImage = tileRecord.tiledImage;
+ // tile.getCanvasContext should always exist in normal usage (with $.Tile)
+ // but the tile cache test passes in a dummy object
+ let context2D = tile.getCanvasContext && tile.getCanvasContext();
+
tile.unload();
tile.cacheImageRecord = null;
var imageRecord = this._imagesLoaded[tile.cacheKey];
+ if(!imageRecord){
+ return;
+ }
imageRecord.removeTile(tile);
if (!imageRecord.getTileCount()) {
+
imageRecord.destroy();
delete this._imagesLoaded[tile.cacheKey];
this._imagesLoadedCount--;
+
+ if(context2D){
+ /**
+ * Free up canvas memory
+ * (iOS 12 or higher on 2GB RAM device has only 224MB canvas memory,
+ * and Safari keeps canvas until its height and width will be set to 0).
+ */
+ context2D.canvas.width = 0;
+ context2D.canvas.height = 0;
+
+ /**
+ * Triggered when an image has just been unloaded
+ *
+ * @event image-unloaded
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {CanvasRenderingContext2D} context2D - The context that is being unloaded
+ */
+ tiledImage.viewer.raiseEvent("image-unloaded", {
+ context2D: context2D,
+ 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
@@ -260,6 +292,7 @@ $.TileCache.prototype = {
tile: tile,
tiledImage: tiledImage
});
+
}
};
diff --git a/src/tiledimage.js b/src/tiledimage.js
index 0bedb701..20fa2ebf 100644
--- a/src/tiledimage.js
+++ b/src/tiledimage.js
@@ -2,7 +2,7 @@
* OpenSeadragon - TiledImage
*
* 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
@@ -85,7 +85,7 @@
* A set of headers to include when making tile AJAX requests.
*/
$.TiledImage = function( options ) {
- var _this = this;
+ this._initialized = false;
/**
* The {@link OpenSeadragon.TileSource} that defines this TiledImage.
* @member {OpenSeadragon.TileSource} source
@@ -159,10 +159,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,
@@ -221,30 +226,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 */{
@@ -255,6 +239,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.
*/
@@ -297,17 +288,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();
- if (xUpdated || yUpdated || scaleUpdated || degreesUpdated) {
+ let updated = (xUpdated || yUpdated || scaleUpdated || degreesUpdated || this._needsUpdate);
+
+ if (updated || viewportChanged || !this._fullyLoaded){
+ let fullyLoadedFlag = this._updateLevelsForViewport();
+ this._setFullyLoaded(fullyLoadedFlag);
+ }
+
+ this._needsUpdate = false;
+
+ if (updated) {
this._updateForScale();
+ this._raiseBoundsChange();
this._needsDraw = true;
return true;
}
@@ -316,18 +319,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;
},
/**
@@ -337,7 +355,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this.reset();
if (this.source.destroy) {
- this.source.destroy();
+ this.source.destroy(this.viewer);
}
},
@@ -414,7 +432,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);
@@ -495,7 +513,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;
@@ -681,6 +699,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;
@@ -689,6 +708,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) {
@@ -727,7 +747,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');
};
@@ -753,10 +772,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();
}
},
@@ -766,6 +786,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
*/
resetCroppingPolygons: function() {
this._croppingPolygons = null;
+ this._needsDraw = true;
},
/**
@@ -874,7 +895,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;
},
/**
@@ -882,9 +903,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();
},
/**
@@ -899,11 +965,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.
@@ -964,9 +1038,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
@@ -977,23 +1100,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.
@@ -1006,8 +1122,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
},
/**
@@ -1105,6 +1237,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._scaleSpring.resetTo(scale);
this._updateForScale();
this._needsDraw = true;
+ this._needsUpdate = true;
} else {
if (sameTarget) {
return;
@@ -1113,6 +1246,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._scaleSpring.springTo(scale);
this._updateForScale();
this._needsDraw = true;
+ this._needsUpdate = true;
}
if (!sameTarget) {
@@ -1175,56 +1309,66 @@ $.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 bestTiles = [];
var haveDrawn = false;
+ var drawArea = this.getDrawArea();
var currentTime = $.now();
+ // 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 = {};
+
+ if(!drawArea){
+ this._needsDraw = false;
+ return this._fullyLoaded;
+ }
+
+ // 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 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);
+ levelList.hasHigherResolutionFallback = true;
+ break;
+ }
+ }
+
+
// Update any level that will be drawn
- for (var level = highestLevel; level >= lowestLevel; level--) {
+ for (let i = 0; i < levelList.length; i++) {
+ let level = levelList[i];
var drawLevel = false;
//Avoid calculations for draw if we have already drawn this
- var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
+ var currentRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate(
this.source.getPixelRatio(level),
true
).x * this._scaleSpring.current.value;
- if (level === lowestLevel ||
- (!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio)) {
+ if (i === levelList.length - 1 ||
+ (!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio) ) {
drawLevel = true;
haveDrawn = true;
} else if (!haveDrawn) {
@@ -1232,12 +1376,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
}
//Perform calculations for draw if we haven't drawn this
- var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
+ var targetRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate(
this.source.getPixelRatio(level),
false
).x * this._scaleSpring.current.value;
- var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate(
+ var targetZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate(
this.source.getPixelRatio(
Math.max(
this.source.getClosestLevel(),
@@ -1253,8 +1397,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
optimalRatio - targetRenderPixelRatio
);
- // Update the level and keep track of 'best' tile to load
- bestTiles = this._updateLevel(
+ // Update the level and keep track of 'best' tiles to load
+ // the bestTiles
+ var result = this._updateLevel(
haveDrawn,
drawLevel,
level,
@@ -1265,6 +1410,21 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
bestTiles
);
+ 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)) {
@@ -1272,8 +1432,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
}
}
- // Perform the actual drawing
- this._drawTiles(this.lastDrawn);
// Load the new 'best' n tiles
if (bestTiles && bestTiles.length > 0) {
@@ -1282,52 +1440,119 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._loadTile(tile, currentTime);
}
}, this);
+
this._needsDraw = true;
- this._setFullyLoaded(false);
+ return false;
} else {
- this._setFullyLoaded(this._tilesLoading === 0);
+ return this._tilesLoading === 0;
}
+
+ // Update
+
},
- // 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);
+ /**
+ * Update all tiles that contribute to the current view
+ * @private
+ *
+ */
+ _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;
+
+ let drawArea = this.getDrawArea();
+ if(!drawArea){
+ return;
}
- 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);
+ 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;
+ }
}
- return {
- topLeft: topLeftTile,
- bottomRight: bottomRightTile,
- };
+ // 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(level > 0){
+ for( let levelKey in this._tilesToDraw ){
+ if( levelKey < level ){
+ delete this._tilesToDraw[levelKey];
+ }
+ }
+ }
+
+ },
+
+ /**
+ * 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
+ *
+ * @param {OpenSeadragon.Tile} tile
+ * @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
+ */
+ _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;
},
/**
@@ -1340,7 +1565,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @param {Number} levelVisibility
* @param {OpenSeadragon.Rect} drawArea
* @param {Number} currentTime
- * @param {OpenSeadragon.Tile[]} best - The current "best" n tiles to draw.
+ * @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}.
*/
_updateLevel: function(haveDrawn, drawLevel, level, levelOpacity,
levelVisibility, drawArea, currentTime, best) {
@@ -1405,7 +1631,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1);
}
}
-
+ 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++) {
@@ -1422,50 +1650,105 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
continue;
}
- best = this._updateTile(
+ var result = this._updateTile(
drawLevel,
haveDrawn,
flippedX, y,
level,
- levelOpacity,
levelVisibility,
viewportCenter,
numberOfTiles,
currentTime,
best
);
+ best = result.bestTiles;
+ tiles[tileIndex] = result.tile;
+ tileIndex += 1;
}
}
- return best;
+ return {
+ bestTiles: best,
+ updatedTiles: tiles
+ };
},
/**
* @private
- * @inner
+ * @param {OpenSeadragon.Tile} tile
+ * @param {Boolean} overlap
+ * @param {OpenSeadragon.Viewport} viewport
+ * @param {OpenSeadragon.Point} viewportCenter
+ * @param {Number} levelVisibility
+ */
+ _positionTile: function( tile, overlap, viewport, viewportCenter, levelVisibility ){
+ var boundsTL = tile.bounds.getTopLeft();
+
+ boundsTL.x *= this._scaleSpring.current.value;
+ boundsTL.y *= this._scaleSpring.current.value;
+ boundsTL.x += this._xSpring.current.value;
+ boundsTL.y += this._ySpring.current.value;
+
+ var boundsSize = tile.bounds.getSize();
+
+ 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),
+ sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false),
+ tileCenter = positionT.plus( sizeT.divide( 2 ) ),
+ tileSquaredDistance = viewportCenter.squaredDistanceTo( tileCenter );
+
+ if(this.viewer.drawer.minimumOverlapRequired()){
+ 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.isBottomMost && this.wrapVertical) {
+ sizeC.y += 0.75; // Otherwise Firefox and Safari show seams
+ }
+ }
+
+ tile.position = positionC;
+ tile.size = sizeC;
+ tile.squaredDistance = tileSquaredDistance;
+ tile.visibility = levelVisibility;
+ },
+
+ /**
* Update a single tile at a particular resolution level.
+ * @private
* @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" tiles to draw.
+ * @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}
*/
- _updateTile: function( haveDrawn, drawLevel, x, y, level, levelOpacity,
+ _updateTile: function( haveDrawn, drawLevel, x, y, level,
levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
var tile = this._getTile(
x, y,
level,
currentTime,
- numberOfTiles,
- this._worldWidthCurrent,
- this._worldHeightCurrent
+ numberOfTiles
),
drawTile = drawLevel;
@@ -1493,9 +1776,14 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage);
if ( !tile.exists ) {
- return best;
+ return {
+ bestTiles: best,
+ tile: tile
+ };
+ }
+ if (tile.loaded && tile.opacity === 1){
+ this._setCoverage( this.coverage, level, x, y, true );
}
-
if ( haveDrawn && !drawTile ) {
if ( this._isCovered( this.coverage, level, x, y ) ) {
this._setCoverage( this.coverage, level, x, y, true );
@@ -1505,7 +1793,10 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
}
if ( !drawTile ) {
- return best;
+ return {
+ bestTiles: best,
+ tile: tile
+ };
}
this._positionTile(
@@ -1527,48 +1818,75 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
}
}
- if ( tile.loaded ) {
- var needsDraw = this._blendTile(
- tile,
- x, y,
- level,
- levelOpacity,
- currentTime
- );
-
- if ( needsDraw ) {
- this._needsDraw = true;
- }
- } else if ( tile.loading ) {
+ if ( tile.loading ) {
// the tile is already in the download queue
this._tilesLoading++;
} else if (!loadingCoverage) {
best = this._compareTiles( best, tile, this.maxTilesPerFrame );
}
- return best;
+ 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);
+ }
+
+ 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,
+ };
},
/**
- * @private
- * @inner
* Obtains a tile at the given location.
+ * @private
* @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
+ numTiles
) {
var xMod,
yMod,
@@ -1654,9 +1972,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
},
/**
- * @private
- * @inner
* Dispatch a job to the ImageLoader to load the Image for a Tile.
+ * @private
* @param {OpenSeadragon.Tile} tile
* @param {Number} time
*/
@@ -1682,9 +1999,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
},
/**
- * @private
- * @inner
* Callback fired when a Tile's Image finished downloading.
+ * @private
* @param {OpenSeadragon.Tile} tile
* @param {Number} time
* @param {*} data image data
@@ -1733,19 +2049,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
_this._setTileLoaded(tile, data, cutoff, tileRequest);
};
- // 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);
- }
+
+ finish();
},
/**
* @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
@@ -1781,6 +2090,22 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
tiledImage: _this
});
}
+ /**
+ * Triggered when a tile is loaded and pre-processing is compelete,
+ * and the tile is ready to draw.
+ *
+ * @event tile-ready
+ * @memberof OpenSeadragon.Viewer
+ * @type {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).
+ */
+ _this.viewer.raiseEvent("tile-ready", {
+ tile: tile,
+ tiledImage: _this,
+ tileRequest: tileRequest
+ });
_this._needsDraw = true;
}
}
@@ -1820,105 +2145,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
fallbackCompletion();
},
- /**
- * @private
- * @inner
- * @param {OpenSeadragon.Tile} tile
- * @param {Boolean} overlap
- * @param {OpenSeadragon.Viewport} viewport
- * @param {OpenSeadragon.Point} viewportCenter
- * @param {Number} levelVisibility
- */
- _positionTile: function( tile, overlap, viewport, viewportCenter, levelVisibility ){
- var boundsTL = tile.bounds.getTopLeft();
-
- boundsTL.x *= this._scaleSpring.current.value;
- boundsTL.y *= this._scaleSpring.current.value;
- boundsTL.x += this._xSpring.current.value;
- boundsTL.y += this._ySpring.current.value;
-
- var boundsSize = tile.bounds.getSize();
-
- boundsSize.x *= this._scaleSpring.current.value;
- boundsSize.y *= this._scaleSpring.current.value;
-
- var positionC = viewport.pixelFromPointNoRotate(boundsTL, true),
- positionT = viewport.pixelFromPointNoRotate(boundsTL, false),
- sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true),
- sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false),
- tileCenter = positionT.plus( sizeT.divide( 2 ) ),
- tileSquaredDistance = viewportCenter.squaredDistanceTo( tileCenter );
-
- 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.isBottomMost && this.wrapVertical) {
- sizeC.y += 0.75; // Otherwise Firefox and Safari show seams
- }
-
- tile.position = positionC;
- tile.size = sizeC;
- tile.squaredDistance = tileSquaredDistance;
- tile.visibility = levelVisibility;
- },
/**
- * @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} currentTime
- * @returns {Boolean}
- */
- _blendTile: function( tile, x, y, level, levelOpacity, currentTime ){
- var 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.alwaysBlend ) {
- opacity *= levelOpacity;
- }
-
- tile.opacity = opacity;
-
- this.lastDrawn.push( tile );
-
- if ( opacity === 1 ) {
- this._setCoverage( this.coverage, level, x, y, true );
- this._hasOpaqueTile = true;
- } else if ( deltaTime < blendTimeMillis ) {
- return true;
- }
-
- return false;
- },
-
-
- /**
- * @private
- * @inner
* 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.
@@ -1938,9 +2169,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
},
/**
- * @private
- * @inner
* Sorts tiles in an array according to distance and visibility.
+ * @private
*
* @param {OpenSeadragon.Tile[]} tiles The tiles.
*/
@@ -1960,277 +2190,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
});
},
- /**
- * @private
- * @inner
- * Draws a TiledImage.
- * @param {OpenSeadragon.Tile[]} lastDrawn - An unordered list of Tiles drawn last frame.
- */
- _drawTiles: function( lastDrawn ) {
- if (this.opacity === 0 || (lastDrawn.length === 0 && !this.placeholderFillStyle)) {
- return;
- }
-
- var tile = lastDrawn[0];
- var useSketch;
-
- 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));
- }
-
- 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 (viewport rotation is not a problem).
- this._drawer.viewer.viewport.getFlip() === false && // TODO: support tile edge smoothing with viewport flip (tiled image flip is not a problem).
- $.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));
- }
-
- 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(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);
- }
- }
-
- bounds = bounds.times($.pixelDensityRatio);
- }
- 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
- });
- }
-
- if (this.viewport.getRotation(true) % 360 === 0 &&
- this.getRotation(true) % 360 === 0) {
- if(this._drawer.viewer.viewport.getFlip()) {
- this._drawer._flip();
- }
- }
- }
-
- 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);
- }
- this._drawer.setClip(clipRect, useSketch);
-
- usedClip = true;
- }
-
- 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;
- }
-
- 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);
- }
-
- var fillStyle = null;
- if ( typeof this.placeholderFillStyle === "function" ) {
- fillStyle = this.placeholderFillStyle(this, this._drawer.context);
- }
- else {
- fillStyle = this.placeholderFillStyle;
- }
-
- this._drawer.drawRectangle(placeholderRect, fillStyle, useSketch);
- }
-
- var subPixelRoundingRule = determineSubPixelRoundingRule(this.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;
- }
-
- 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;
-
- if( this.viewer ){
- /**
- * - Needs documentation -
- *
- * @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: this,
- tile: tile
- });
- }
- }
-
- if ( usedClip ) {
- this._drawer.restoreContext( useSketch );
- }
-
- if (!sketchScale) {
- if (this.getRotation(true) % 360 !== 0) {
- this._drawer._restoreRotationChanges(useSketch);
- }
- if (this.viewport.getRotation(true) % 360 !== 0) {
- this._drawer._restoreRotationChanges(useSketch);
- }
- }
-
- 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();
- }
- }
- }
-
- this._drawDebugInfo( lastDrawn );
- },
/**
- * @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.
- */
- _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);
- }
- }
- }
- },
-
- /**
- * @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.
@@ -2238,6 +2199,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.
@@ -2278,11 +2240,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.
@@ -2304,9 +2265,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.
@@ -2331,11 +2291,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.
@@ -2346,71 +2305,5 @@ $.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 ));
diff --git a/src/tilesource.js b/src/tilesource.js
index fefa0098..2a5225c0 100644
--- a/src/tilesource.js
+++ b/src/tilesource.js
@@ -2,7 +2,7 @@
* OpenSeadragon - TileSource
*
* 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
@@ -370,6 +370,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;
@@ -568,13 +569,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
@@ -775,7 +776,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,
diff --git a/src/tilesourcecollection.js b/src/tilesourcecollection.js
index e1483184..29d7ec94 100644
--- a/src/tilesourcecollection.js
+++ b/src/tilesourcecollection.js
@@ -2,7 +2,7 @@
* OpenSeadragon - TileSourceCollection
*
* 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
diff --git a/src/tmstilesource.js b/src/tmstilesource.js
index b6deeb03..5f8c999f 100644
--- a/src/tmstilesource.js
+++ b/src/tmstilesource.js
@@ -2,7 +2,7 @@
* OpenSeadragon - TmsTileSource
*
* 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
diff --git a/src/viewer.js b/src/viewer.js
index efab2c60..6a9d35a2 100644
--- a/src/viewer.js
+++ b/src/viewer.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Viewer
*
* 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
@@ -89,6 +89,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, {
@@ -198,6 +213,7 @@ $.Viewer = function( options ) {
$.console.warn("Hash " + this.hash + " has already been used.");
}
+
//Private state properties
THIS[ this.hash ] = {
fsBoundsDelta: new $.Point( 1, 1 ),
@@ -418,13 +434,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" );
@@ -470,6 +514,7 @@ $.Viewer = function( options ) {
displayRegionColor: this.navigatorDisplayRegionColor,
crossOriginPolicy: this.crossOriginPolicy,
animationTime: this.animationTime,
+ drawer: this.drawer.getType(),
});
}
@@ -496,11 +541,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);
};
@@ -888,6 +928,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}
@@ -1040,7 +1147,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;
},
@@ -1087,7 +1194,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
return this;
}
- if ( fullPage ) {
+ if ( fullPage && this.element ) {
this.elementSize = $.getElementSize( this.element );
this.pageScroll = $.getPageScroll();
@@ -2108,7 +2215,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
@@ -2579,7 +2686,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 );
}
@@ -2597,9 +2703,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
@@ -3202,10 +3305,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;
@@ -3732,7 +3836,7 @@ function updateOnce( viewer ) {
var viewportChange = viewer.viewport.update();
- var animated = viewer.world.update() || viewportChange;
+ var animated = viewer.world.update(viewportChange) || viewportChange;
if (viewportChange) {
/**
@@ -3823,7 +3927,6 @@ function updateOnce( viewer ) {
function drawWorld( viewer ) {
viewer.imageLoader.clear();
- viewer.drawer.clear();
viewer.world.draw();
/**
@@ -3977,4 +4080,22 @@ 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 ));
diff --git a/src/viewport.js b/src/viewport.js
index 02e173f0..4d209dc5 100644
--- a/src/viewport.js
+++ b/src/viewport.js
@@ -2,7 +2,7 @@
* OpenSeadragon - Viewport
*
* 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
@@ -1133,7 +1133,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;
@@ -1165,7 +1165,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
@@ -1705,7 +1711,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.
@@ -1717,7 +1723,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
@@ -1783,7 +1789,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);
+ }
+ }
+ },
};
diff --git a/src/webgldrawer.js b/src/webgldrawer.js
new file mode 100644
index 00000000..25bbda66
--- /dev/null
+++ b/src/webgldrawer.js
@@ -0,0 +1,1318 @@
+
+/*
+ * 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.getExtension('WEBGL_lose_context');
+ if(ext){
+ ext.loseContext();
+ }
+ return !!( webglContext );
+ }
+
+ /**
+ *
+ * @returns 'webgl'
+ */
+ getType(){
+ return 'webgl';
+ }
+
+ /**
+ * 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);
+ }
+
+ // 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);
+
+ }
+
+ // 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();
+ });
+ }
+
+ // 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;
+ }
+
+ 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;
+ }
+
+ 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);
+
+ }
+
+ }
+
+ // 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);
+ }
+ }
+
+ // private
+ _imageUnloadedHandler(event){
+ let canvas = event.context2D.canvas;
+ this._cleanupImageData(canvas);
+ }
+
+ // 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);
+ }
+
+ }
+
+ // private
+ _setClip(){
+ // no-op: called by _renderToClippingCanvas when tiledImage._clip is truthy
+ // so that tests will pass.
+ }
+
+ // 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.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();
+ }
+
+ /**
+ * 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);
+ }
+
+ // 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);
+ }
+ }
+ }
+
+ // private
+ _drawDebugInfoOnTile(tile, count, i, tiledImage, flipped) {
+
+ 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(flipped){
+ 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();
+ }
+
+ _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;
+ }
+
+ 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();
+
+ }
+
+ /**
+ * 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();
+ }
+
+ // 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) {
+
+ function loadShader(gl, type, source) {
+ const shader = gl.createShader(type);
+
+ // Send the source to the shader object
+
+ gl.shaderSource(shader, source);
+
+ // Compile the shader program
+
+ gl.compileShader(shader);
+
+ // See if it compiled successfully
+
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ $.console.error(
+ `An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}`
+ );
+ gl.deleteShader(shader);
+ return null;
+ }
+
+ return shader;
+ }
+
+ const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
+ const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
+
+ // Create the shader program
+
+ const shaderProgram = gl.createProgram();
+ gl.attachShader(shaderProgram, vertexShader);
+ gl.attachShader(shaderProgram, fragmentShader);
+ gl.linkProgram(shaderProgram);
+
+ // If creating the shader program failed, alert
+
+ if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
+ $.console.error(
+ `Unable to initialize the shader program: ${gl.getProgramInfoLog(
+ shaderProgram
+ )}`
+ );
+ return null;
+ }
+
+ return shaderProgram;
+ }
+
+ };
+
+
+
+}( OpenSeadragon ));
diff --git a/src/world.js b/src/world.js
index a525997b..6049bb06 100644
--- a/src/world.js
+++ b/src/world.js
@@ -2,7 +2,7 @@
* OpenSeadragon - World
*
* 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
@@ -242,11 +242,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;
@@ -256,11 +259,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;
+ });
},
/**
diff --git a/test/demo/basic.html b/test/demo/basic.html
index 6a677420..ae4b96af 100644
--- a/test/demo/basic.html
+++ b/test/demo/basic.html
@@ -25,7 +25,7 @@
id: "contentDiv",
prefixUrl: "../../build/openseadragon/images/",
tileSources: "../data/testpattern.dzi",
- showNavigator: true
+ showNavigator: true,
});
+
+
+
Compare behavior of Context2d and WebGL drawers
+
+
+
+
+
Image options (drag and drop to re-order images)
+
+
+
+
+
HTMLDrawer: legacy pre-HTML5 drawer that uses <img> elements for tiles
+
+
+
+ HTML-based rendering can be selected in two different ways:
+
+
+ // via the 'html' drawer option:
+ let viewer = OpenSeadragon({
+ ...
+ drawer: 'html',
+ ...
+ });
+
+ // or by passing the HTMLDrawer constructor
+ let viewer = OpenSeadragon({
+ ...
+ drawer:OpenSeadragon.HTMLDrawer,
+ ...
+ });
+
+
+
+
+
+
+
+
+
diff --git a/test/demo/collections/main.js b/test/demo/collections/main.js
index 83563085..ef5d8b60 100644
--- a/test/demo/collections/main.js
+++ b/test/demo/collections/main.js
@@ -16,7 +16,6 @@
// debugMode: true,
zoomPerScroll: 1.02,
showNavigator: testNavigator,
- useCanvas: true,
// defaultZoomLevel: 2,
// homeFillsViewer: true,
// sequenceMode: true,
@@ -131,8 +130,9 @@
var box = new OpenSeadragon.Rect(margins.left, margins.top,
$('#contentDiv').width() - (margins.left + margins.right),
$('#contentDiv').height() - (margins.top + margins.bottom));
-
- self.viewer.drawer.debugRect(box);
+ // If drawDebuggingRect is implemented, use it to show the box.
+ // This is not implemented by all drawers however.
+ self.viewer.drawer.drawDebuggingRect(box);
});
}
diff --git a/test/demo/drawercomparison.html b/test/demo/drawercomparison.html
new file mode 100644
index 00000000..e8ca6e48
--- /dev/null
+++ b/test/demo/drawercomparison.html
@@ -0,0 +1,114 @@
+
+
+