From 2f3bef086546a210ee303c491cd8ee1486ecb3e3 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 12 Mar 2023 11:42:03 -0400 Subject: [PATCH] split canvas from html rendering --- Gruntfile.js | 3 +- src/canvasdrawer.js | 1218 +++++++++++++++++++++++++++++++++++++++++++ src/drawer.js | 6 +- src/htmldrawer.js | 347 ++++++++++++ src/viewer.js | 9 +- test/demo/webgl.js | 23 +- 6 files changed, 1590 insertions(+), 16 deletions(-) create mode 100644 src/canvasdrawer.js create mode 100644 src/htmldrawer.js diff --git a/Gruntfile.js b/Gruntfile.js index 4dfd5f2d..1e87ab4b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -58,7 +58,8 @@ module.exports = function(grunt) { "src/tile.js", "src/overlay.js", "src/drawerbase.js", - "src/drawer.js", + "src/htmldrawer.js", + "src/canvasdrawer.js", "src/viewport.js", "src/tiledimage.js", "src/tilecache.js", diff --git a/src/canvasdrawer.js b/src/canvasdrawer.js new file mode 100644 index 00000000..6f727f2c --- /dev/null +++ b/src/canvasdrawer.js @@ -0,0 +1,1218 @@ +/* + * 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 CanvasDrawer + * @memberof OpenSeadragon + * @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. + */ +$.CanvasDrawer = function(options) { + + $.DrawerBase.call(this, options); + + /** + * 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.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; + + // 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'; + + // 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; +}; + +$.extend( $.CanvasDrawer.prototype, $.DrawerBase.prototype, /** @lends OpenSeadragon.Drawer.prototype */ { + + /** + * Draws the TiledImages + */ + draw: function(tiledImages) { + var _this = this; + this._prepareNewFrame(); // prepare to draw a new frame + tiledImages.forEach(function(tiledImage){ + if (tiledImage.opacity !== 0 || tiledImage._preload) { + tiledImage._midDraw = true; + _this._updateViewportWithTiledImage(tiledImage); + tiledImage._midDraw = false; + } + else { + tiledImage._needsDraw = false; + } + }); + + }, + + /** + * @returns {Boolean} True - rotation is supported. + */ + canRotate: function() { + return true; + }, + + /** + * 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; + }, + + /** + * 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){ + this._imageSmoothingEnabled = imageSmoothingEnabled; + this._updateImageSmoothingEnabled(this.context); + this.viewer.forceRedraw(); + }, + + /** + * Draw a rectangle onto the canvas + * @param {OpenSeadragon.Rect} rect + */ + drawDebuggingRect: function(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(); + + }, + + /** + * @private + * @inner + * Clears the Drawer so it's ready to draw another frame. + * + */ + _prepareNewFrame: function() { + 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 + * @inner + * @param {Boolean} useSketch Whether to clear sketch canvas or main canvas + * @param {OpenSeadragon.Rect} [bounds] The rectangle to clear + */ + _clear: function(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); + } + }, + + /* Methods from TiledImage */ + + /** + * @private + * @inner + * Handles drawing a single TiledImage to the canvas + * + */ + _updateViewportWithTiledImage: function(tiledImage) { + var _this = this; + tiledImage._needsDraw = false; + tiledImage._tilesLoading = 0; + tiledImage.loadingCoverage = {}; + + // Reset tile's internal drawn state + while (tiledImage.lastDrawn.length > 0) { + var tile = tiledImage.lastDrawn.pop(); + tile.beingDrawn = false; + } + + + var drawArea = tiledImage.getDrawArea(); + if(!drawArea){ + return; + } + + function updateTile(info){ + var tile = info.tile; + if(tile && tile.loaded){ + var needsDraw = _this._blendTile( + tiledImage, + tile, + tile.x, + tile.y, + info.level, + info.levelOpacity, + info.currentTime + ); + if(needsDraw){ + tiledImage._needsDraw = true; + } + } + } + + var infoArray = tiledImage.getTileInfoForDrawing(); + infoArray.forEach(updateTile); + + this._drawTiles(tiledImage); + + }, + + + + /** + * @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( tiledImage, tile, x, y, level, levelOpacity, currentTime ){ + var blendTimeMillis = 1000 * tiledImage.blendTime, + deltaTime, + opacity; + + if ( !tile.blendStart ) { + tile.blendStart = currentTime; + } + + deltaTime = currentTime - tile.blendStart; + opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; + + if ( tiledImage.alwaysBlend ) { + opacity *= levelOpacity; + } + + tile.opacity = opacity; + + tiledImage.lastDrawn.push( tile ); + + if ( opacity === 1 ) { + tiledImage._setCoverage( tiledImage.coverage, level, x, y, true ); + tiledImage._hasOpaqueTile = true; + } else if ( deltaTime < blendTimeMillis ) { + return true; + } + + return false; + }, + + /** + * @private + * @inner + * Draws a TiledImage. + * + */ + _drawTiles: function( tiledImage ) { + var lastDrawn = tiledImage.lastDrawn; + 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 ){ // TO DO: 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(); + + if(this.viewer.viewport.getFlip()) { + if (this.viewport.getRotation(true) % 360 !== 0 || + tiledImage.getRotation(true) % 360 !== 0) { + bounds.x = this.viewer.container.clientWidth - (bounds.x + bounds.width); + } + } + + bounds = bounds.times($.pixelDensityRatio); + } + this._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._offsetForRotation({ + degrees: this.viewport.getRotation(true), + useSketch: useSketch + }); + } + if (tiledImage.getRotation(true) % 360 !== 0) { + this._offsetForRotation({ + degrees: tiledImage.getRotation(true), + point: this.viewport.pixelFromPointNoRotate( + tiledImage._getRotationPoint(true), true), + useSketch: useSketch + }); + } + + if (this.viewport.getRotation(true) % 360 === 0 && + tiledImage.getRotation(true) % 360 === 0) { + if(this.viewer.viewport.getFlip()) { + this._flip(); + } + } + } + + 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; + 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; + } + + if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) { + var placeholderRect = this._viewportToDrawerRectangle(tiledImage.getBounds(true)); + if (sketchScale) { + placeholderRect = placeholderRect.times(sketchScale); + } + if (sketchTranslate) { + placeholderRect = placeholderRect.translate(sketchTranslate); + } + + var 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 = lastDrawn.length - 1; i >= 0; i--) { + tile = lastDrawn[ i ]; + this._drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, + sketchTranslate, shouldRoundPositionAndSize, tiledImage.source ); + tile.beingDrawn = true; + + if( this.viewer ){ + /** + * Raised when a tile is drawn to the canvas + * + * @event tile-drawn + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'tile-drawn', { + tiledImage: tiledImage, + tile: tile + }); + } + } + + 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) { + if (this.viewport.getRotation(true) % 360 !== 0) { + this._offsetForRotation({ + degrees: this.viewport.getRotation(true), + useSketch: false + }); + } + if (tiledImage.getRotation(true) % 360 !== 0) { + this._offsetForRotation({ + degrees: tiledImage.getRotation(true), + point: this.viewport.pixelFromPointNoRotate( + tiledImage._getRotationPoint(true), true), + useSketch: false + }); + } + } + 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); + } + } + } + + if (!sketchScale) { + if (this.viewport.getRotation(true) % 360 === 0 && + tiledImage.getRotation(true) % 360 === 0) { + if(this.viewer.viewport.getFlip()) { + this._flip(); + } + } + } + + this._drawDebugInfo( tiledImage, 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( tiledImage, lastDrawn ) { + if( tiledImage.debugMode ) { + for ( var i = lastDrawn.length - 1; i >= 0; i-- ) { + var tile = lastDrawn[ i ]; + try { + this.drawDebugInfo(tile, lastDrawn.length, i, tiledImage); + } catch(e) { + $.console.error(e); + } + } + } + }, + + /* Methods from Tile */ + + /** + * @private + * @inner + * 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) { + 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(); + }, + + /** + * @private + * @inner + * Draws the given tile. + * @param {OpenSeadragon.Tile} tile - The tile to draw. + * @param {Function} drawingHandler - Method for firing the drawing event if using canvas. + * drawingHandler({context, tile, rendered}) + * @param {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'); + + var context = this._getContext(useSketch); + scale = scale || 1; + this._drawTileToCanvas(tile, context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source); + + }, + + /** + * @private + * @inner + * Renders the tile in a canvas-based context. + * @function + * @param {OpenSeadragon.Tile} tile - the tile to draw to the canvas + * @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. + */ + _drawTileToCanvas: function( tile, context, drawingHandler, 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(); + 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 && 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 gives the application a chance to make image manipulation + // changes as we are rendering the image + drawingHandler({context: context, tile: tile, rendered: 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(); + }, + + /** + * @private + * @inner + * Renders the tile in an html container. + * @function + * @param {OpenSeadragon.Tile} tile + * @param {Element} container + */ + _drawTileToHTML: function( tile, container ) { + 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 ); + }, + + /** + * @private + * @inner + * Get the context of the main or sketch canvas + * @param {Boolean} useSketch + * @returns + */ + _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 + * @inner + * Save the context of the main or sketch canvas + * @param {Boolean} useSketch + * @returns + */ + _saveContext: function( useSketch ) { + this._getContext( useSketch ).save(); + }, + + /** + * @private + * @inner + * Restore the context of the main or sketch canvas + * @param {Boolean} useSketch + * @returns + */ + _restoreContext: function( useSketch ) { + this._getContext( useSketch ).restore(); + }, + + // private + _setClip: function(rect, useSketch) { + 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) { + 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 + }; + } + + 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) { + + 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 + _updateImageSmoothingEnabled: function(context){ + context.msImageSmoothingEnabled = this._imageSmoothingEnabled; + context.imageSmoothingEnabled = this._imageSmoothingEnabled; + }, + + /** + * @private + * @inner + * 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); + }, + + /** + * @private + * @inner + * Get the canvas center + * @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: 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 + }; + }, + + +}); + + + +/** + * @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/drawer.js b/src/drawer.js index a8d4d09d..46b0afeb 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -444,7 +444,7 @@ $.extend( $.Drawer.prototype, $.DrawerBase.prototype, /** @lends OpenSeadragon.D fillStyle = tiledImage.placeholderFillStyle; } - this.drawRectangle(placeholderRect, fillStyle, useSketch); + this._drawRectangle(placeholderRect, fillStyle, useSketch); } var subPixelRoundingRule = determineSubPixelRoundingRule(tiledImage.subPixelRoundingForTransparency); @@ -611,7 +611,7 @@ $.extend( $.Drawer.prototype, $.DrawerBase.prototype, /** @lends OpenSeadragon.D scale = scale || 1; this._drawTileToCanvas(tile, context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source); } else { - tile._drawTileToHTML( tile, this.canvas ); + this._drawTileToHTML( tile, this.canvas ); } }, @@ -868,7 +868,7 @@ $.extend( $.Drawer.prototype, $.DrawerBase.prototype, /** @lends OpenSeadragon.D }, // private - drawRectangle: function(rect, fillStyle, useSketch) { + _drawRectangle: function(rect, fillStyle, useSketch) { if (!this.useCanvas) { return; } diff --git a/src/htmldrawer.js b/src/htmldrawer.js new file mode 100644 index 00000000..00db0697 --- /dev/null +++ b/src/htmldrawer.js @@ -0,0 +1,347 @@ +/* + * 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 HTMLDrawer + * @memberof OpenSeadragon + * @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. + */ +$.HTMLDrawer = function(options) { + + $.DrawerBase.call(this, options); + + /** + * 2d drawing context for {@link OpenSeadragon.Drawer#canvas} if it's a <canvas> element, otherwise null. + * @member {Object} context + * @memberof OpenSeadragon.Drawer# + */ + this.context = null; + + + // 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'; + +}; + +$.extend( $.HTMLDrawer.prototype, $.DrawerBase.prototype, /** @lends OpenSeadragon.Drawer.prototype */ { + + /** + * Draws the TiledImages + */ + draw: function(tiledImages) { + var _this = this; + this._prepareNewFrame(); // prepare to draw a new frame + tiledImages.forEach(function(tiledImage){ + if (tiledImage.opacity !== 0 || tiledImage._preload) { + tiledImage._midDraw = true; + _this._updateViewportWithTiledImage(tiledImage); + tiledImage._midDraw = false; + } + else { + tiledImage._needsDraw = false; + } + }); + + }, + + /** + * @returns {Boolean} False - rotation is not supported. + */ + canRotate: function() { + return false; + }, + + /** + * 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; + }, + + /** + * Turns image smoothing on or off for this viewer. Note: Ignored by HTML Drawer + * + * @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(){ + // noop - HTML Drawer does not deal with this property + $.console.warn('HTMLDrawer.setImageSmoothingEnabled does not have an effect.'); + }, + + /** + * @private + * @inner + * Clears the Drawer so it's ready to draw another frame. + * + */ + _prepareNewFrame: function() { + this.canvas.innerHTML = ""; + }, + + /* Methods from TiledImage */ + + /** + * @private + * @inner + * Handles drawing a single TiledImage to the canvas + * + */ + _updateViewportWithTiledImage: function(tiledImage) { + var _this = this; + tiledImage._needsDraw = false; + tiledImage._tilesLoading = 0; + tiledImage.loadingCoverage = {}; + + // Reset tile's internal drawn state + while (tiledImage.lastDrawn.length > 0) { + var tile = tiledImage.lastDrawn.pop(); + tile.beingDrawn = false; + } + + + var drawArea = tiledImage.getDrawArea(); + if(!drawArea){ + return; + } + + function updateTile(info){ + var tile = info.tile; + if(tile && tile.loaded){ + var needsDraw = _this._blendTile( + tiledImage, + tile, + tile.x, + tile.y, + info.level, + info.levelOpacity, + info.currentTime + ); + if(needsDraw){ + tiledImage._needsDraw = true; + } + } + } + + var infoArray = tiledImage.getTileInfoForDrawing(); + infoArray.forEach(updateTile); + + this._drawTiles(tiledImage); + + }, + + + + /** + * @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( tiledImage, tile, x, y, level, levelOpacity, currentTime ){ + var blendTimeMillis = 1000 * tiledImage.blendTime, + deltaTime, + opacity; + + if ( !tile.blendStart ) { + tile.blendStart = currentTime; + } + + deltaTime = currentTime - tile.blendStart; + opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; + + if ( tiledImage.alwaysBlend ) { + opacity *= levelOpacity; + } + + tile.opacity = opacity; + + tiledImage.lastDrawn.push( tile ); + + if ( opacity === 1 ) { + tiledImage._setCoverage( tiledImage.coverage, level, x, y, true ); + tiledImage._hasOpaqueTile = true; + } else if ( deltaTime < blendTimeMillis ) { + return true; + } + + return false; + }, + + /** + * @private + * @inner + * Draws a TiledImage. + * + */ + _drawTiles: function( tiledImage ) { + var lastDrawn = tiledImage.lastDrawn; + 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 ); + tile.beingDrawn = true; + + if( this.viewer ){ + /** + * Raised when a tile is drawn to the canvas + * + * @event tile-drawn + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent( 'tile-drawn', { + tiledImage: tiledImage, + tile: tile + }); + } + } + + }, + + /* Methods from Tile */ + + /** + * @private + * @inner + * Draws the given tile. + * @param {OpenSeadragon.Tile} tile - The tile to draw. + * @param {Function} drawingHandler - Method for firing the drawing event if using canvas. + * drawingHandler({context, tile, rendered}) + */ + _drawTile: function( tile ) { + $.console.assert(tile, '[Drawer._drawTile] tile is required'); + + this._drawTileToHTML( tile, this.canvas ); + }, + + + /** + * @private + * @inner + * Renders the tile in an html container. + * @function + * @param {OpenSeadragon.Tile} tile + * @param {Element} container + */ + _drawTileToHTML: function( tile, container ) { + 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 ); + }, + +}); + + + + +}( OpenSeadragon )); diff --git a/src/viewer.js b/src/viewer.js index cbcd80db..136a228b 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -433,13 +433,20 @@ $.Viewer = function( options ) { throw('Viewer option customDrawer must derive from OpenSeadragon.DrawerBase'); } - } else { + } else if(this.useCanvas && $.supportsCanvas) { this.drawer = new $.Drawer({ viewer: this, viewport: this.viewport, element: this.canvas, debugGridColor: this.debugGridColor }); + } else { + this.drawer = new $.HTMLDrawer({ + viewer: this, + viewport: this.viewport, + element: this.canvas, + debugGridColor: this.debugGridColor + }); } diff --git a/test/demo/webgl.js b/test/demo/webgl.js index a44fbc65..80b678df 100644 --- a/test/demo/webgl.js +++ b/test/demo/webgl.js @@ -27,10 +27,11 @@ var viewer = window.viewer = OpenSeadragon({ maxZoomPixelRatio:100, smoothTileEdgesMinZoom:1.1, crossOriginPolicy: 'Anonymous', - ajaxWithCredentials: false + ajaxWithCredentials: false, + useCanvas:false, }); -let threeRenderer = window.threeRenderer = new ThreeJSDrawer({viewer, viewport: viewer.viewport, element:viewer.element}); +// let threeRenderer = window.threeRenderer = new ThreeJSDrawer({viewer, viewport: viewer.viewport, element:viewer.element}); var viewer2 = window.viewer2 = OpenSeadragon({ id: "three-viewer", @@ -45,15 +46,15 @@ var viewer2 = window.viewer2 = OpenSeadragon({ }); //make the test canvas mirror all changes to the viewer canvas -let viewerCanvas = viewer.drawer.canvas; -let canvas = threeRenderer.canvas; -let canvasContainer = $('#three-canvas-container').append(canvas); -viewer.addHandler("resize", function(){ - canvasContainer[0].style.width = viewerCanvas.clientWidth+'px'; - canvasContainer[0].style.height = viewerCanvas.clientHeight+'px'; - // canvas.width = viewerCanvas.width; - // canvas.height = viewerCanvas.height; -}) +// let viewerCanvas = viewer.drawer.canvas; +// let canvas = threeRenderer.canvas; +// let canvasContainer = $('#three-canvas-container').append(canvas); +// viewer.addHandler("resize", function(){ +// canvasContainer[0].style.width = viewerCanvas.clientWidth+'px'; +// canvasContainer[0].style.height = viewerCanvas.clientHeight+'px'; +// // canvas.width = viewerCanvas.width; +// // canvas.height = viewerCanvas.height; +// }) // viewer.addHandler("open", () => viewer.world.getItemAt(0).source.hasTransparency = function(){ return true; });