/* * OpenSeadragon - Drawer * * Copyright (C) 2009 CodePlex Foundation * Copyright (C) 2010-2022 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 ));