diff --git a/Gruntfile.js b/Gruntfile.js index c963e170..4dfd5f2d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -57,6 +57,7 @@ module.exports = function(grunt) { "src/imageloader.js", "src/tile.js", "src/overlay.js", + "src/drawerbase.js", "src/drawer.js", "src/viewport.js", "src/tiledimage.js", diff --git a/src/drawer.js b/src/drawer.js index 3ac2c1fa..346f60d5 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -44,7 +44,9 @@ * @param {Element} options.element - Parent element. * @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details. */ -$.Drawer = function( options ) { +$.Drawer = function(options) { + + $.DrawerBase.call(this, options); $.console.assert( options.viewer, "[Drawer] options.viewer is required" ); @@ -138,51 +140,27 @@ $.Drawer = function( options ) { // 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 = { +$.extend( $.Drawer.prototype, $.DrawerBase.prototype, /** @lends OpenSeadragon.Drawer.prototype */ { /** - * 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. + * Draws the TiledImage to its Drawer. */ - 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; + draw: function(tiledImage) { + if (tiledImage.opacity !== 0 || tiledImage._preload) { + tiledImage._midDraw = true; + this._updateViewport(tiledImage); + tiledImage._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 { + tiledImage._needsDraw = false; } - 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(); }, - - - - /** * @returns {Boolean} True if rotation is supported. */ @@ -237,6 +215,419 @@ $.Drawer.prototype = { } }, + /* Methods from TiledImage */ + + + + /** + * @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(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, tiledImage.lastDrawn); + + }, + + + + /** + * @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 && // TODO: support tile edge smoothing with tiled image rotation. + $.supportsCanvas && this.viewer.useCanvas) { + // When zoomed in a lot (>100%) the tile edges are visible. + // So we have to composite them at ~100% and scale them up together. + // Note: Disabled on iOS devices per default as it causes a native crash + useSketch = true; + sketchScale = tile.getScaleForEdgeSmoothing(); + sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale, + this.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); + } + 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; + } + + 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 ){ + /** + * - 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: 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 */ + + + + /** + * 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(); + }, + + + + + + + /** * Scale from OpenSeadragon viewer rectangle to drawer rectangle * (ignoring rotation) @@ -276,12 +667,179 @@ $.Drawer.prototype = { if (this.useCanvas) { var context = this._getContext(useSketch); scale = scale || 1; - tile.drawCanvas(context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source); + this.drawTileToCanvas(tile, context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source); } else { - tile.drawHTML( this.canvas ); + tile.drawTileToHTML( tile, this.canvas ); } }, + /** + * 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(); + }, + + /** + * 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 ); + }, + _getContext: function( useSketch ) { var context = this.context; if ( useSketch ) { @@ -766,6 +1324,75 @@ $.Drawer.prototype = { } return maxOpacity; }, -}; +}); + + + +/** + * @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/drawerbase.js b/src/drawerbase.js new file mode 100644 index 00000000..e556ee1c --- /dev/null +++ b/src/drawerbase.js @@ -0,0 +1,344 @@ +/* + * OpenSeadragon - DrawerBase + * + * 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 DrawerBase + * @memberof OpenSeadragon + * @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 {Element} options.element - Parent element. + */ +$.DrawerBase = 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.DrawerBase#canvas}. + * @member {Element} container + * @memberof OpenSeadragon.DrawerBase# + */ + this.container = $.getElement( options.element ); + /** + * A <canvas> element if the browser supports them, otherwise a <div> element. + * Child element of {@link OpenSeadragon.DrawerBase#container}. + * @member {Element} canvas + * @memberof OpenSeadragon.DrawerBase# + */ + this.canvas = $.makeNeutralElement( this.useCanvas ? "canvas" : "div" ); + + + /** + * @member {Element} element + * @memberof OpenSeadragon.DrawerBase# + * @deprecated Alias for {@link OpenSeadragon.DrawerBase#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'; + + 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; + + this._checkForAPIOverrides(); +}; + +/** @lends OpenSeadragon.DrawerBaseBase.prototype */ +$.DrawerBase.prototype = { + + // Drawer implementaions must define the next four methods. These are called + // by core OSD, and forcing overrides (even for nullop methods) makes the + // behavior of the implementations explicitly clear in the code. + // Whether these have been overridden by child classes is checked in the + // constructor (via _checkForAPIOverrides). It could make sense to consolidate + // these a bit (e.g. by making `draw` take an array of `TiledImage`s and + // clearing the view as needed, rather than the existing pattern of + // `drawer.clear(); world.draw()` in the calling code), but they have been + // left as-is to maintain backwards compatibility. + + /** + * @param tiledImage the TiledImage that is ready to be drawn + */ + draw: function(tiledImage) { + $.console.error('Drawer.draw must be implemented by child class'); + }, + + /** + * @returns {Boolean} True if rotation is supported. + */ + canRotate: function() { + $.console.error('Drawer.canRotate must be implemented by child class'); + }, + + /** + * Destroy the drawer (unload current loaded tiles) + */ + destroy: function() { + $.console.error('Drawer.destroy must be implemented by child class'); + }, + + /** + * Clears the Drawer so it's ready to draw another frame. + */ + clear: function() { + $.console.error('Drawer.clear must be implemented by child class'); + }, + + /** + * 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){ + $.console.error('Drawer.setImageSmoothingEnabled must be implemented by child class'); + }, + + /** + * Ensures that child classes have provided implementations for API methods + * draw, canRotate, destroy, and clear. Throws an exception if the original + * placeholder methods are still in place. + */ + _checkForAPIOverrides: function(){ + if(this.draw === $.DrawerBase.prototype.draw){ + throw("[drawer].draw must be implemented by child class"); + } + if(this.canRotate === $.DrawerBase.prototype.canRotate){ + throw("[drawer].canRotate must be implemented by child class"); + } + if(this.destroy === $.DrawerBase.prototype.destroy){ + throw("[drawer].destroy must be implemented by child class"); + } + if(this.clear === $.DrawerBase.prototype.clear){ + throw("[drawer].clear must be implemented by child class"); + } + if(this.setImageSmoothingEnabled === $.DrawerBase.prototype.setImageSmoothingEnabled){ + throw("[drawer].setImageSmoothingEnabled must be implemented by child class"); + } + }, + + /** + * 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 + ); + }, + + /** + * 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 + ); + }, + + + // 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) + }; + }, + + + /* Deprecated Functions */ + + // 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; + }, + // 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; + }, + + // deprecated + 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; + }, + + // deprecated + 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; + }, +}; + +Object.defineProperty($.DrawerBase.prototype, "isOpenSeadragonDrawer", { + get: function get() { + return true; + } +}); + + +}( OpenSeadragon )); diff --git a/src/tile.js b/src/tile.js index c6ac95eb..7cc2d2fa 100644 --- a/src/tile.js +++ b/src/tile.js @@ -274,64 +274,6 @@ 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 @@ -385,113 +327,6 @@ 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(); - }, - /** * Get the ratio between current and original size. * @function diff --git a/src/tiledimage.js b/src/tiledimage.js index ad33bd02..c9eb5290 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -160,6 +160,7 @@ $.TiledImage = function( options ) { _needsDraw: true, // Does the tiledImage need to update the viewport again? _hasOpaqueTile: false, // Do we have even one fully opaque tile? _tilesLoading: 0, // The number of pending tile requests. + _tilesToDraw: [], // info about the tiles currently in the viewport //configurable settings springStiffness: $.DEFAULT_SETTINGS.springStiffness, animationTime: $.DEFAULT_SETTINGS.animationTime, @@ -299,6 +300,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag var scaleUpdated = this._scaleSpring.update(); var degreesUpdated = this._degreesSpring.update(); + this._updateTilesForViewport(); + if (xUpdated || yUpdated || scaleUpdated || degreesUpdated) { this._updateForScale(); this._raiseBoundsChange(); @@ -309,21 +312,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag return false; }, - /** - * Draws the TiledImage to its Drawer. - */ - 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; - } - }, - /** * Destroy the TiledImage (unload current loaded tiles). */ @@ -761,7 +749,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'); }; @@ -787,10 +774,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(); } }, @@ -800,6 +788,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag */ resetCroppingPolygons: function() { this._croppingPolygons = null; + this._needsDraw = true; }, /** @@ -1001,6 +990,24 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._raiseBoundsChange(); }, + /** + * Get the region of this tiled image that falls within the viewport. + * @returns OpenSeadragon.Rect + */ + getDrawArea: function(){ + + 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; + }, + /** * Get the point around which this tiled image is rotated * @private @@ -1023,6 +1030,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @fires OpenSeadragon.TiledImage.event:composite-operation-change */ setCompositeOperation: function(compositeOperation) { + var _this = this; if (compositeOperation === this.compositeOperation) { return; } @@ -1042,6 +1050,21 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this.raiseEvent('composite-operation-change', { compositeOperation: this.compositeOperation }); + + /** + * Raised when a TiledImage's opacity is changed. + * @event composite-operation-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {String} compositeOperation - The new compositeOperation value. + * @property {OpenSeadragon.Viewer} eventSource - A reference to the + * Viewer which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.viewer.raiseEvent('composite-operation-change', { + compositeOperation: _this.compositeOperation, + tiledImage: _this + }); }, // private @@ -1125,50 +1148,34 @@ $.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; - } - } + getTileInfoForDrawing: function(){ + return this._tilesToDraw; + }, + _updateTilesForViewport: function(){ var levelsInterval = this._getLevelsInterval(); var lowestLevel = levelsInterval.lowestLevel; var highestLevel = levelsInterval.highestLevel; var bestTile = null; var haveDrawn = false; + var drawArea = this.getDrawArea(); var currentTime = $.now(); + this._tilesToDraw = []; + this._tilesLoading = 0; + this.loadingCoverage = {}; + + if(!drawArea){ + this._needsDraw = false; + return; + } + // Update any level that will be drawn for (var level = highestLevel; level >= lowestLevel; level--) { var drawLevel = false; //Avoid calculations for draw if we have already drawn this - var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate( + var currentRenderPixelRatio = this.viewport.deltaPixelsFromPointsNoRotate( this.source.getPixelRatio(level), true ).x * this._scaleSpring.current.value; @@ -1182,12 +1189,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(), @@ -1204,7 +1211,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag ); // Update the level and keep track of 'best' tile to load - bestTile = this._updateLevel( + var result = this._updateLevel( haveDrawn, drawLevel, level, @@ -1215,6 +1222,21 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag bestTile ); + bestTile = result.best; + var tiles = result.tiles; + var makeTileInfoObject = (function(level, levelOpacity, currentTime){ + return function(tile){ + return { + tile: tile, + level: level, + levelOpacity: levelOpacity, + currentTime: currentTime + }; + }; + })(level, levelOpacity, currentTime); + + this._tilesToDraw = this._tilesToDraw.concat(tiles.map(makeTileInfoObject)); + // Stop the loop if lower-res tiles would all be covered by // already drawn tiles if (this._providesCoverage(this.coverage, level)) { @@ -1222,11 +1244,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag } } - // Perform the actual drawing - - this._drawTiles(this.lastDrawn); - - // Load the new 'best' tile if (bestTile && !bestTile.context2D) { this._loadTile(bestTile, currentTime); @@ -1235,47 +1252,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag } else { this._setFullyLoaded(this._tilesLoading === 0); } - }, - // private - _getCornerTiles: function(level, topLeftBound, bottomRightBound) { - var leftX; - var rightX; - if (this.wrapHorizontal) { - leftX = $.positiveModulo(topLeftBound.x, 1); - rightX = $.positiveModulo(bottomRightBound.x, 1); - } else { - leftX = Math.max(0, topLeftBound.x); - rightX = Math.min(1, bottomRightBound.x); - } - var topY; - var bottomY; - var aspectRatio = 1 / this.source.aspectRatio; - if (this.wrapVertical) { - topY = $.positiveModulo(topLeftBound.y, aspectRatio); - bottomY = $.positiveModulo(bottomRightBound.y, aspectRatio); - } else { - topY = Math.max(0, topLeftBound.y); - bottomY = Math.min(aspectRatio, bottomRightBound.y); - } - var topLeftTile = this.source.getTileAtPoint(level, new $.Point(leftX, topY)); - var bottomRightTile = this.source.getTileAtPoint(level, new $.Point(rightX, bottomY)); - var numTiles = this.source.getNumTiles(level); - - if (this.wrapHorizontal) { - topLeftTile.x += numTiles.x * Math.floor(topLeftBound.x); - bottomRightTile.x += numTiles.x * Math.floor(bottomRightBound.x); - } - if (this.wrapVertical) { - topLeftTile.y += numTiles.y * Math.floor(topLeftBound.y / aspectRatio); - bottomRightTile.y += numTiles.y * Math.floor(bottomRightBound.y / aspectRatio); - } - - return { - topLeft: topLeftTile, - bottomRight: bottomRightTile, - }; + // return bestTile; }, /** @@ -1288,7 +1267,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @param {Number} levelVisibility * @param {OpenSeadragon.Rect} drawArea * @param {Number} currentTime - * @param {OpenSeadragon.Tile} best - The current "best" tile to draw. + * @param {Object} result Dictionary {best: OpenSeadragon.Tile - the current "best" tile to draw, tiles: Array(OpenSeadragon.Tile) - the updated tiles}. */ _updateLevel: function(haveDrawn, drawLevel, level, levelOpacity, levelVisibility, drawArea, currentTime, best) { @@ -1353,7 +1332,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++) { @@ -1370,22 +1351,74 @@ $.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.best; + tiles[tileIndex] = result.tile; + tileIndex += 1; } } - return best; + return { + best: best, + tiles: 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; + + 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; }, /** @@ -1397,14 +1430,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @param {Number} x * @param {Number} y * @param {Number} level - * @param {Number} levelOpacity * @param {Number} levelVisibility * @param {OpenSeadragon.Point} viewportCenter * @param {Number} numberOfTiles * @param {Number} currentTime * @param {OpenSeadragon.Tile} best - The current "best" tile to draw. */ - _updateTile: function( haveDrawn, drawLevel, x, y, level, levelOpacity, + _updateTile: function( haveDrawn, drawLevel, x, y, level, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ var tile = this._getTile( @@ -1441,7 +1473,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._setCoverage(this.loadingCoverage, level, x, y, loadingCoverage); if ( !tile.exists ) { - return best; + return { + best: best + }; } if ( haveDrawn && !drawTile ) { @@ -1453,7 +1487,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag } if ( !drawTile ) { - return best; + return { + best: best + }; } this._positionTile( @@ -1475,26 +1511,58 @@ $.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 ); } - return best; + return { + best: 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, + }; }, /** @@ -1784,99 +1852,6 @@ $.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 @@ -1903,270 +1878,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag return previousBest; }, - /** - * @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. - $.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); - } - 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 @@ -2285,71 +1996,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 e1cbd13b..78ec8062 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -368,7 +368,10 @@ $.TileSource.prototype = { getTileAtPoint: function(level, point) { var validPoint = point.x >= 0 && point.x <= 1 && point.y >= 0 && point.y <= 1 / this.aspectRatio; - $.console.assert(validPoint, "[TileSource.getTileAtPoint] must be called with a valid point."); + // $.console.assert(validPoint, "[TileSource.getTileAtPoint] must be called with a valid point."); + if(!validPoint){ + $.console.warn("[TileSource.getTileAtPoint] called with an invalid point."); + } var widthScaled = this.dimensions.x * this.getLevelScale(level); var pixelX = point.x * widthScaled; diff --git a/src/viewer.js b/src/viewer.js index 9107a329..84950d26 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -417,12 +417,29 @@ $.Viewer = function( options ) { }); // Create the drawer - this.drawer = new $.Drawer({ - viewer: this, - viewport: this.viewport, - element: this.canvas, - debugGridColor: this.debugGridColor - }); + if(this.customDrawer){ + if(this.customDrawer.prototype.isOpenSeadragonDrawer){ + var Drawer = this.customDrawer; + this.drawer = new Drawer({ + viewer: this, + viewport: this.viewport, + element: this.canvas, + debugGridColor: this.debugGridColor + }); + } else { + // $.console.error('Viewer option customDrawer must derive from OpenSeadragon.DrawerBase'); + throw('Viewer option customDrawer must derive from OpenSeadragon.DrawerBase'); + } + + } else { + this.drawer = new $.Drawer({ + viewer: this, + viewport: this.viewport, + element: this.canvas, + debugGridColor: this.debugGridColor + }); + } + // Overlay container this.overlaysContainer = $.makeNeutralElement( "div" ); diff --git a/src/world.js b/src/world.js index c0fea25c..12af10d3 100644 --- a/src/world.js +++ b/src/world.js @@ -257,7 +257,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W */ draw: function() { for ( var i = 0; i < this._items.length; i++ ) { - this._items[i].draw(); + this.viewer.drawer.draw(this._items[i]); } this._needsDraw = false; diff --git a/test/demo/threejsdrawer.js b/test/demo/threejsdrawer.js new file mode 100644 index 00000000..feaf0a91 --- /dev/null +++ b/test/demo/threejsdrawer.js @@ -0,0 +1,796 @@ +// import 'https://cdnjs.cloudflare.com/ajax/libs/three.js/0.149.0/three.min.js'; +import '../lib/three.js'; +const THREE = window.THREE; + +export class ThreeJSDrawer extends OpenSeadragon.DrawerBase{ + constructor(options){ + super(options); + let _this = this; + + // this.viewer set by parent constructor + // this.canvas set by parent constructor, created and appended to the viewer container element + this._camera = null; + this._currentImages = []; + this._renderer = null; + + this._tileMap = {}; + this._tiledImageMap = {}; + this._uuid = generateUUID(); // to use for reference mapping + + this._renderingContinously = false; + this._animationFrame = null; + + 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; + + //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._renderer.setViewport(0, 0, _this._outputCanvas.width, _this._outputCanvas.height); + } + + _this._renderingCanvas.style.width = _this._outputCanvas.clientWidth+'px'; + _this._renderingCanvas.style.height = _this._outputCanvas.clientHeight+'px'; + _this._renderingCanvas.width = _this._clippingCanvas.width = _this._outputCanvas.width; + _this._renderingCanvas.height = _this._clippingCanvas.height = _this._outputCanvas.height; + + _this.render(); + }) + this._setupRenderer(); + } + renderFrame(){ + if(this._animationFrame) { + cancelAnimationFrame(this._animationFrame); + } + this._animationFrame = requestAnimationFrame(()=>this.render()); + } + render(){ + let numItems = this.viewer.world.getItemCount(); + this._outputContext.clearRect(0, 0, this._outputCanvas.width, this._outputCanvas.height); + //iterate over items to draw + for(let i = 0; i < numItems; i++){ + let item = this.viewer.world.getItemAt(i); + let scene = this._tiledImageMap[item[this._uuid]]; + + if(item.wrapHorizontal || item.wrapVertical){ + createWrappingGrid(scene, item); + } else { + scene.userData.wrappedCopies.clear(); + } + this._renderer.render(scene, this._camera); //renders to this._renderingCanvas + + this._outputContext.save(); + // set composite operation; ignore for first image drawn + this._outputContext.globalCompositeOperation = i===0 ? null : item.compositeOperation || this.viewer.compositeOperation; + if(item._croppingPolygons || item._clip){ + this._renderToClippingCanvas(item); + this._outputContext.drawImage(this._clippingCanvas, 0, 0); + + } else { + this._outputContext.drawImage(this._renderingCanvas, 0, 0); + } + this._outputContext.restore(); + if(item.debugMode){ + this._drawDebugInfo(item) + } + } + this._animationFrame = null; + if(this._renderingContinuously){ + this.renderFrame(); + } + // console.log(this._renderer.info.memory, this._renderer.info.render.triangles); + } + renderContinuously(continuously){ + if(continuously){ + this._renderingContinuously = true; + + } else { + this._renderingContinuously = false; + } + } + + // Public API required by all Drawer implementations + + destroy(){ + // clear all resources used by the renderer, geometries, textures etc + + // to do: remove handlers from viewer + + // dispose of any remaining textures/materials + Object.values(this._tileMap).forEach(material=>material.dispose()); + //to do: check whether tiled images are all removed (for clean up) before viewer destroy event + + // clean up renderer and camera objects + cleanupObject(this._renderer); + cleanupObject(this._camera); + this._renderer = null; + this._camera = null; + + } + clear(){ + //not needed by this implementation + } + canRotate(){ + return true; + } + draw(tiledImage){ + // actual drawing is handled by event listeneners + // just mark this tiledImage as having been drawn (possibly unnecessary) + tiledImage._needsDraw = false; + } + setImageSmoothingEnabled(enabled){ + this._clippingContext.imageSmoothingEnabled = enabled; + this._outputContext.imageSmoothingEnabled = enabled; + } + + // Private methods + + _setupRenderer(){ + //to do: test support for pages of sequence mode + + let viewerBounds = this.viewer.viewport.getBoundsNoRotate(); + this._camera = new THREE.OrthographicCamera( + viewerBounds.width / -2, + viewerBounds.width / 2, + viewerBounds.height / 2, + viewerBounds.height / -2, + 0, + 10000 + ); + this._camera.position.x = viewerBounds.x + viewerBounds.width/2; + this._camera.position.y = -(viewerBounds.y + viewerBounds.height/2); + this._camera.position.z = 100; + this._camera.lookAt(this._camera.position.x, this._camera.position.y, 0); + this._camera.updateProjectionMatrix(); + + + this._renderer = new THREE.WebGLRenderer({canvas: this._renderingCanvas, alpha:true}); + + // Add listeners for events that require modifying the scene or camera + this.viewer.addHandler("destroy", ()=>this.destroy()); + this.viewer.world.addHandler("add-item", ev => this._addTiledImage(ev)); + this.viewer.world.addHandler("remove-item", ev => this._removeTiledImage(ev)); + this.viewer.addHandler("tile-ready", ev => this._tileReadyHandler(ev)); + this.viewer.addHandler("tile-unloaded", ev => this._tileUnloadedHandler(ev)); + this.viewer.addHandler("viewport-change", () => this._viewportChangeHandler()); + this.viewer.addHandler("home", () => this._viewportChangeHandler()); + + // this.viewer.world.addHandler("item-index-change", () => this.renderFrame()); + // this.viewer.addHandler("crop-change", () => this.renderFrame()); + // this.viewer.addHandler("clip-change", () => this.renderFrame()); + // this.viewer.addHandler("opacity-change", () => this.renderFrame()); + // this.viewer.addHandler("composite-operation-change", () => this.renderFrame()); + + + this.viewer.addHandler("update-viewport", () => this.renderFrame()); + + this._viewportChangeHandler(); + } + + _addTiledImage(event){ + let tiledImage = event.item; + + // create a Group for the tiles of this tiled image. + if(!tiledImage[this._uuid]){ + let tileContainer = new THREE.Group(); + let rotationAxis = new THREE.Group(); + let positioningGroup = new THREE.Group(); + + rotationAxis.add(tileContainer); + positioningGroup.add(rotationAxis); + + let wrappedCopies = new THREE.Group(); + rotationAxis.add(wrappedCopies); + + // save mutual references between OpenSceneGraph and ThreeJSRenderer versions of tiledImages + // add unique ID to the tiledImage to look it up in our map in event handlers + let scene = new THREE.Scene() + let light = new THREE.AmbientLight(); + scene.add(light); + scene.add(positioningGroup); + scene.userData.tileContainer = tileContainer; + scene.userData.wrappedCopies = wrappedCopies; + + let tiledImageID = generateUUID(); + tiledImage[this._uuid] = tiledImageID; + this._tiledImageMap[tiledImageID] = scene; + + + // keep a direct reference to the TiledImage on the Group + positioningGroup._tiledImage = tiledImage; + + //offset the tileContainer so the center of the image is at the origin of the parent group + tileContainer.position.x = -0.5; + tileContainer.position.y = -0.5 / tiledImage.source.aspectRatio; + + //undo the offset of the tileContainer, moving this back into original viewport coordinate space + rotationAxis.position.x = tileContainer.position.x * -1; + rotationAxis.position.y = tileContainer.position.y * -1; + + + this._updateTiledImageParameters(tiledImage, positioningGroup, rotationAxis); + tiledImage.addHandler('bounds-change',()=>this._updateTiledImageParameters(tiledImage, positioningGroup, rotationAxis, true)); + tiledImage.addHandler('opacity-change',()=>this._updateTiledImageParameters(tiledImage, positioningGroup, rotationAxis, true)); + + } + this._updateMeshIfNeeded(tiledImage); + this.renderFrame(); + } + + _removeTiledImage(event){ + let tiledImage = event.item; + + cleanupObject(this._tiledImageMap[tiledImage[this._uuid]]); + delete this._tiledImageMap[tiledImage[this._uuid]]; + this.renderFrame(); + } + + _tileReadyHandler(event){ + let tile = event.tile; + let tiledImage = event.tiledImage; + + if(this._tileMap[tile.cacheKey]){ + // this tile has already been handled; ignore repeat ready request (happens when image is wrapped) + return; + } + + //create a THREE.Material with the image data for this tile + let texture = new THREE.CanvasTexture(event.tile.getCanvasContext().canvas); + texture.flipY = false; // To match OSD reference frame + let material = new THREE.MeshLambertMaterial({ + map: texture, + transparent: !!tile.hasTransparency || tiledImage.opacity < 1, + opacity: tiledImage.opacity + }); + + // cache the material using the tile's cacheKey so that when a tile is located by OpenSeadragon methods, the associate material can be retrieved + this._tileMap[tile.cacheKey] = material; + + let numTiles = tiledImage.source.getNumTiles(tile.level); + let tx = OpenSeadragon.positiveModulo(tile.x, numTiles.x); + let ty = OpenSeadragon.positiveModulo(tile.y, numTiles.y); + + //cache the bounds for this material so it doesn't have to be recomputed every time it is used to update the scene + material.userData._tileBounds = tiledImage.source.getTileBounds(tile.level, tx, ty); + material.userData.hasTransparency = !!tile.hasTransparency; + material.userData.tile = tile; + material.userData.tiledImage = tiledImage; + + //since a new tile is available, update the image (if needed) + this._updateTiledImageRendering(tiledImage, tile); + } + + _tileUnloadedHandler(event){ + let tile = event.tile; + if(!this._tileMap[tile.cacheKey]){ + //already cleaned up + return; + } + this._updateTiledImageRendering(event.tiledImage, tile); + cleanupObject(this._tileMap[tile.cacheKey]); + delete this._tileMap[tile.cacheKey]; + } + + _updateTiledImageParameters(tiledImage, positioningGroup, rotationAxis, requestRender){ + let bounds = tiledImage.getBoundsNoRotate(true); + let rotation = tiledImage.getRotation(true); + + //set size and location + positioningGroup.scale.x = bounds.width; //scale the normalized image coordinates to match the size within the world + positioningGroup.scale.y = -bounds.width; //flip Y + positioningGroup.position.x = bounds.x; + positioningGroup.position.y = bounds.y * -1;//flip Y + + // rotate about the rotation axis + rotationAxis.rotation.z = rotation * Math.PI / 180; + rotationAxis.scale.x = tiledImage.getFlip() ? -1 : 1; + + updateOpacity(this._tiledImageMap[tiledImage[this._uuid]].userData.tileContainer, tiledImage.opacity); + + if(requestRender){ + this.renderFrame(); + } + + } + + _updateTiledImageRendering(tiledImage, tile){ + + let scene = this._tiledImageMap[tiledImage[this._uuid]]; + + let bounds = this._tileMap[tile.cacheKey].userData._tileBounds + + if(bounds.x < 0 || bounds.y < 0 || bounds.x >= 1 || bounds.y >= 1){ + return; + } + + this._updateMeshIfNeeded(tiledImage); + + let level = scene.userData.currentLevel; + + //whether the tile was just loaded or unloaded, update any tiles that it overlaps in the current tileGrid (as needed) + + let topLeft = tiledImage.source.getTileAtPoint(level, {x: bounds.x, y: bounds.y}); + let bottomRight = tiledImage.source.getTileAtPoint(level, {x: bounds.x + bounds.width, y: bounds.y + bounds.height}); + + //iterate over the tiles overlapped by this one + let x, y; + for(x = topLeft.x; x<= bottomRight.x; x++){ + for(y = topLeft.y; y <= bottomRight.y; y++){ + let mesh = scene.userData._tileMatrix[x][y]; + this._loadBestImage(mesh); + } + } + + this.renderFrame(); + } + + _updateMeshIfNeeded(tiledImage){ + let tileContainer = this._tiledImageMap[tiledImage[this._uuid]].userData.tileContainer; + let scene = this._tiledImageMap[tiledImage[this._uuid]] + let levelsInterval = tiledImage._getLevelsInterval(); + let level = levelsInterval.highestLevel; + + if(scene.userData.currentLevel === level){ + //we are already drawing the highest-resolution tiles, just return + return; + } + // console.log('new level', level); + scene.userData.currentLevel = level; + //we need to update the grid. + //clear the old matrix + scene.userData._tileMatrix = []; + scene.userData.tiledImage = tiledImage; + //remove existing tiles + + tileContainer.children.forEach(cleanupObject); + tileContainer.clear(); + + //create new set of tiles and add to the tileContainer + let gridInfo = tiledImage.getGridDefinition(level); + let col, row; + for(col = 0; col < gridInfo.numColumns; col += 1){ + scene.userData._tileMatrix[col] = []; + for(row = 0; row < gridInfo.numRows; row += 1){ + let colInfo = gridInfo.columnInfo[col]; + let rowInfo = gridInfo.rowInfo[row]; + + let left = colInfo.x; + let top = rowInfo.y; + let x = left + colInfo.width / 2; + let y = top + rowInfo.height / 2; + let z = 0; + + let tileGeometry = new THREE.PlaneGeometry(colInfo.width, rowInfo.height); + let mesh = new THREE.Mesh(tileGeometry); + + + mesh.position.set(x, y, z); + + mesh.userData._tileInfo = { + row: row, + col: col, + level: level, + ...rowInfo, + ...colInfo, + tiledImageID: tiledImage[this._uuid], + center: new OpenSeadragon.Point(x, y), + uvMapOriginal: [], + // uvMap + } + + let i; + let uvAttribute = tileGeometry.attributes.uv; + + for(i =0 ; i= 0){ + let tileIndex = tileSource.getTileAtPoint(queryLevel, tileInfo.center); + let tile = getTile(tilesMatrix, queryLevel, tileIndex.x, tileIndex.y); + let material = tile && this._tileMap[tile.cacheKey]; + if(material){ + addMaterialToMesh(mesh, material, tiledImage.opacity); + break; + } + queryLevel--; + } + } + } + + _viewportChangeHandler(){ + let viewer = this.viewer; + + let viewerBounds = viewer.viewport.getBoundsNoRotate(true); + this._camera.left = viewerBounds.width / -2; + this._camera.right = viewerBounds.width / 2; + this._camera.top = viewerBounds.height / 2; + this._camera.bottom = viewerBounds.height / -2; + + let center = viewer.viewport.getCenter(true); + this._camera.position.x = center.x; + this._camera.position.y = -center.y; + this._camera.rotation.z = viewer.viewport.getRotation(true) * Math.PI / 180; + + this._camera.updateProjectionMatrix(); + + let numItems = viewer.world.getItemCount(); + let i; + for(i = 0; i < numItems; i++){ + let tiledImage = viewer.world.getItemAt(i); + this._updateMeshIfNeeded(tiledImage); + } + + this.renderFrame(); + + } + + _renderToClippingCanvas(item){ + let _this = this; + + this._clippingContext.clearRect(0, 0, this._clippingCanvas.width, this._clippingCanvas.height); + this._clippingContext.save(); + + if(item._clip){ + var box = item.imageToViewportRectangle(item._clip, true); + var rect = this.viewportToDrawerRectangle(box); + this._clippingContext.beginPath(); + this._clippingContext.rect(rect.x, rect.y, rect.width, rect.height); + this._clippingContext.clip(); + } + if(item._croppingPolygons){ + let polygons = item._croppingPolygons.map(function (polygon) { + return polygon.map(function (coord) { + let point = item.imageToViewportCoordinates(coord.x, coord.y, true); + let clipPoint = _this.viewportCoordToDrawerCoord(point); + return clipPoint; + }); + }); + this._clippingContext.beginPath(); + polygons.forEach(function (polygon) { + polygon.forEach(function (coord, i) { + _this._clippingContext[i === 0 ? 'moveTo' : 'lineTo'](coord.x, coord.y); + }); + }); + this._clippingContext.clip(); + } + + this._clippingContext.drawImage(this._renderingCanvas, 0, 0); + + this._clippingContext.restore(); + } + + // private + _offsetForRotation(options) { + var point = options.point ? + options.point.times(OpenSeadragon.pixelDensityRatio) : + new OpenSeadragon.Point(this._outputCanvas.width / 2, this._outputCanvas.height / 2); + + var context = this._outputContext; + context.save(); + + context.translate(point.x, point.y); + if(this.viewport.flipped){ + context.rotate(Math.PI / 180 * -options.degrees); + context.scale(-1, 1); + } else{ + context.rotate(Math.PI / 180 * options.degrees); + } + context.translate(-point.x, -point.y); + } + // private + _drawDebugInfoOnTile(tile, count, i, tiledImage) { + + var colorIndex = this.viewer.world.getIndexOfItem(tiledImage) % this.debugGridColor.length; + var context = this._outputContext; + context.save(); + context.lineWidth = 2 * OpenSeadragon.pixelDensityRatio; + context.font = 'small-caps bold ' + (13 * OpenSeadragon.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 * OpenSeadragon.pixelDensityRatio, + tile.position.y * OpenSeadragon.pixelDensityRatio, + tile.size.x * OpenSeadragon.pixelDensityRatio, + tile.size.y * OpenSeadragon.pixelDensityRatio + ); + + var tileCenterX = (tile.position.x + (tile.size.x / 2)) * OpenSeadragon.pixelDensityRatio; + var tileCenterY = (tile.position.y + (tile.size.y / 2)) * OpenSeadragon.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 * OpenSeadragon.pixelDensityRatio, + (tile.position.y - 30) * OpenSeadragon.pixelDensityRatio + ); + context.fillText( + "Pan: " + this.viewport.getBounds().toString(), + tile.position.x * OpenSeadragon.pixelDensityRatio, + (tile.position.y - 20) * OpenSeadragon.pixelDensityRatio + ); + } + context.fillText( + "Level: " + tile.level, + (tile.position.x + 10) * OpenSeadragon.pixelDensityRatio, + (tile.position.y + 20) * OpenSeadragon.pixelDensityRatio + ); + context.fillText( + "Column: " + tile.x, + (tile.position.x + 10) * OpenSeadragon.pixelDensityRatio, + (tile.position.y + 30) * OpenSeadragon.pixelDensityRatio + ); + context.fillText( + "Row: " + tile.y, + (tile.position.x + 10) * OpenSeadragon.pixelDensityRatio, + (tile.position.y + 40) * OpenSeadragon.pixelDensityRatio + ); + context.fillText( + "Order: " + i + " of " + count, + (tile.position.x + 10) * OpenSeadragon.pixelDensityRatio, + (tile.position.y + 50) * OpenSeadragon.pixelDensityRatio + ); + context.fillText( + "Size: " + tile.size.toString(), + (tile.position.x + 10) * OpenSeadragon.pixelDensityRatio, + (tile.position.y + 60) * OpenSeadragon.pixelDensityRatio + ); + context.fillText( + "Position: " + tile.position.toString(), + (tile.position.x + 10) * OpenSeadragon.pixelDensityRatio, + (tile.position.y + 70) * OpenSeadragon.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(); + } + + _drawDebugInfo( tiledImage ) { + let scene = this._tiledImageMap[tiledImage[this._uuid]]; + let level = scene.userData.currentLevel; + let tiles = tiledImage.getTileInfoForDrawing().filter(tile=>tile.level === level); + + // only draw on the highest level tiles + for ( var i = tiles.length - 1; i >= 0; i-- ) { + var tile = tiles[ i ].tile; + try { + this._drawDebugInfoOnTile(tile, tiles.length, i, tiledImage); + } catch(e) { + OpenSeadragon.console.error(e); + } + } + } + + // private + _debugRect(rect) { + if ( this.useCanvas ) { + var context = this._outputContext; + context.save(); + context.lineWidth = 2 * OpenSeadragon.pixelDensityRatio; + context.strokeStyle = this.debugGridColor[0]; + context.fillStyle = this.debugGridColor[0]; + + context.strokeRect( + rect.x * OpenSeadragon.pixelDensityRatio, + rect.y * OpenSeadragon.pixelDensityRatio, + rect.width * OpenSeadragon.pixelDensityRatio, + rect.height * OpenSeadragon.pixelDensityRatio + ); + + context.restore(); + } + } + + // private + _restoreRotationChanges() { + var context = this._outputContext; + context.restore(); + } + +} + +// Functions below do not depend on an instance of the Drawer, and can be defined outside of the class definition + +function updateOpacity(meshGroup, opacity){ + meshGroup.children.forEach(mesh=>{ + mesh.material.opacity = opacity; + if(opacity < 1 || mesh.material.userData.hasTransparency){ + mesh.material.transparent = true; + } else { + mesh.material.transparent = false; + } + }) +} + +function getTile(tileMatrix, level, x, y){ + return tileMatrix[level] && tileMatrix[level][x] && tileMatrix[level][x][y]; +} + +function addMaterialToMesh(mesh, material, opacity){ + let regionInfo = mesh.userData._tileInfo; + let materialBounds = material.userData._tileBounds; + + //update transparent and opacity properties to reflect current state + let transparent = opacity < 1 || material.userData.hasTransparency; + material.transparent = transparent; + material.opacity = opacity; + + mesh.material = material; + let uvMap = mesh.userData._tileInfo.uvMapOriginal; + let uvAttribute = mesh.geometry.attributes.uv; + + // iterate over UV map for each vertex and calculate position within material/texture + let xNew, yNew; + let regionLeft = regionInfo.x; + let regionTop = regionInfo.y; + let regionRight = regionLeft + regionInfo.width; + let regionBottom = regionTop + regionInfo.height; + + //what is needed to calculate the right uv index for each vertex? + // 1) position of the vertex in normalized coordinates + // 2) position of the entire texture area (not just non-overlapped area) in normalized coordinates + + uvMap.forEach(([x,y],i)=>{ + // x, y describe which corner of the original texture to use + if(x==0){ + xNew = (regionLeft - materialBounds.x) / materialBounds.width; + } else { + xNew = (regionRight - materialBounds.x) / materialBounds.width; + } + + if(y == 0){ + yNew = (regionTop - materialBounds.y) / materialBounds.height; + } else { + yNew = (regionBottom - materialBounds.y) / materialBounds.height; + } + + uvAttribute.setXY(i, xNew, yNew); + }); + uvAttribute.needsUpdate = true; + +} + +function createWrappingGrid(scene, tiledImage){ + let container = scene.userData.wrappedCopies; + container.clear(); + + //calculate how to tile the space + + let tiledImageBounds = tiledImage.getBounds(); + let imgBounds = {x: 0, y: 0, width: tiledImageBounds.width, height: tiledImageBounds.height }; + let drawArea = tiledImage.viewer.viewport.getBounds(true); + let center = drawArea.getCenter(); + let halfDiag = Math.sqrt(drawArea.width * drawArea.width + drawArea.height * drawArea.height); + let extraBuffer = imgBounds.width + imgBounds.height; + let left = center.x - halfDiag - extraBuffer; + let right = center.x + halfDiag + extraBuffer; + let top = center.y - halfDiag - extraBuffer; + let bottom = center.y + halfDiag + extraBuffer; + + let xMin = tiledImage.wrapHorizontal ? Math.floor(left / imgBounds.width) : imgBounds.x; + let yMin = tiledImage.wrapVertical ? Math.floor(top / imgBounds.height) : imgBounds.y; + let xMax = tiledImage.wrapHorizontal ? Math.floor(right / imgBounds.width) : imgBounds.x; + let yMax = tiledImage.wrapVertical ? Math.floor(bottom / imgBounds.height) : imgBounds.y; + + for(let x = xMin; x <= xMax; x += 1){ + for(let y = yMin; y <= yMax; y += 1){ + if(x == 0 && y == 0) { + continue; + } + let clone = scene.userData.tileContainer.clone(); + clone.position.x += x; + clone.position.y += y; + container.add(clone); + } + } +} + + +function cleanupObject(object){ + if(object.children && object.children.forEach){ + object.children.forEach(cleanupObject); + } + if(object.dispose){ + object.dispose(); + } + if(object.geometry){ + object.geometry.dispose(); + } +} + +// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136 +const _lut = [ '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '0a', '0b', '0c', '0d', '0e', '0f', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '1a', '1b', '1c', '1d', '1e', '1f', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2a', '2b', '2c', '2d', '2e', '2f', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e', '3f', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '4a', '4b', '4c', '4d', '4e', '4f', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5a', '5b', '5c', '5d', '5e', '5f', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7a', '7b', '7c', '7d', '7e', '7f', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '8a', '8b', '8c', '8d', '8e', '8f', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '9a', '9b', '9c', '9d', '9e', '9f', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'ba', 'bb', 'bc', 'bd', 'be', 'bf', 'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'ca', 'cb', 'cc', 'cd', 'ce', 'cf', 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', 'da', 'db', 'dc', 'dd', 'de', 'df', 'e0', 'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8', 'e9', 'ea', 'eb', 'ec', 'ed', 'ee', 'ef', 'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff' ]; +function generateUUID() { + + const d0 = Math.random() * 0xffffffff | 0; + const d1 = Math.random() * 0xffffffff | 0; + const d2 = Math.random() * 0xffffffff | 0; + const d3 = Math.random() * 0xffffffff | 0; + const uuid = _lut[ d0 & 0xff ] + _lut[ d0 >> 8 & 0xff ] + _lut[ d0 >> 16 & 0xff ] + _lut[ d0 >> 24 & 0xff ] + '-' + + _lut[ d1 & 0xff ] + _lut[ d1 >> 8 & 0xff ] + '-' + _lut[ d1 >> 16 & 0x0f | 0x40 ] + _lut[ d1 >> 24 & 0xff ] + '-' + + _lut[ d2 & 0x3f | 0x80 ] + _lut[ d2 >> 8 & 0xff ] + '-' + _lut[ d2 >> 16 & 0xff ] + _lut[ d2 >> 24 & 0xff ] + + _lut[ d3 & 0xff ] + _lut[ d3 >> 8 & 0xff ] + _lut[ d3 >> 16 & 0xff ] + _lut[ d3 >> 24 & 0xff ]; + + // .toLowerCase() here flattens concatenated strings to save heap memory space. + return uuid.toLowerCase(); + +} \ No newline at end of file diff --git a/test/demo/webgl-renderer.js b/test/demo/webgl-renderer.js deleted file mode 100644 index c8b0a1d5..00000000 --- a/test/demo/webgl-renderer.js +++ /dev/null @@ -1,841 +0,0 @@ -// import 'https://cdnjs.cloudflare.com/ajax/libs/three.js/0.149.0/three.min.js'; -import '../lib/three.js'; -const THREE = window.THREE; -const DEPTH_MULTIPLIER = 0.1; - -export class ThreeJSRenderer extends OpenSeadragon.Drawer{ - constructor(openSeadragonViewer, canvas){ - this._viewer = openSeadragonViewer; - this._camera = null; - this._scene = null; - this._imageContainer = null; - this._renderer = null; - - this._renderingContinously = false; - this._animationFrame = null; - - if(canvas){ - this._canvas = canvas; - } else { - let viewerCanvas = viewer.drawer.canvas; - this._canvas = viewer.drawer.canvas; - // let canvas = this._canvas = document.createElement('canvas'); - // canvas.insertBefore(viewerCanvas); - - // canvas.style.width = viewerCanvas.clientWidth+'px'; - // canvas.style.height = viewerCanvas.clientHeight+'px'; - // canvas.width = viewerCanvas.width; - // canvas.height = viewerCanvas.height; - - // //make the test canvas mirror all changes to the viewer canvas - // viewer.addHandler("resize", function(){ - // canvas.style.width = viewerCanvas.clientWidth+'px'; - // canvas.style.height = viewerCanvas.clientHeight+'px'; - // }); - } - createThreeViewer(this); - } - renderFrame(){ - if(this._animationFrame) { - cancelAnimationFrame(this._animationFrame); - } - this._animationFrame = requestAnimationFrame(()=>this.render()); - } - render(){ - // this.camera.updateProjectionMatrix(); - this._renderer.render(this._scene, this._camera); - this._animationFrame = null; - if(this._renderingContinuously){ - this.renderFrame(); - } - } - renderContinuously(continuously){ - if(continuously){ - this._renderingContinuously = true; - - } else { - this._renderingContinuously = false; - } - } - destroy(){ - //clear all resources used by the renderer, geometries, textures etc - //to do: remove handlers from viewer and dispose of any remaining textures/materials - cleanupObject(this._scene); - cleanupObject(this._renderer); - cleanupObject(this._camera); - this._scene = null; - this._renderer = null; - this._camera = null; - - } - - //// Override API from OpenSeadragon.Drawer - - /** - * 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(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(); - } - - /** - * 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; - } - - /** - * Clears the Drawer so it's ready to draw another frame. - */ - clear(){ - 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(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); - } - } - - /** - * 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( 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( 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() { - if (!this.useCanvas) { - return; - } - - this._getContext( useSketch ).save(); - } - - // private - restoreContext( useSketch ) { - if (!this.useCanvas) { - return; - } - - this._getContext( useSketch ).restore(); - } - - // private - setClip(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(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(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(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(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(); - } - } - - - -} - -/*** Create THREE.js version of rendering tiled images using WebGL ****/ -function createThreeViewer(instance){ - // Add listeners for events that require modifying the scene or camera - instance._viewer.addHandler("close", ()=>instance.destroy()); - instance._viewer.world.addHandler("add-item", ev => addTiledImage(ev, instance)); - instance._viewer.world.addHandler("remove-item", ev => removeTiledImage(ev, instance)); - instance._viewer.world.addHandler("item-index-change", ev => setItemOrder(ev, instance)); - instance._viewer.addHandler("tile-ready", ev => tileReady(ev, instance)); - instance._viewer.addHandler("tile-unloaded", ev => tileUnloaded(ev, instance)); - instance._viewer.addHandler("viewport-change", ev => viewportChange(ev, instance)); - // instance._viewer.addHandler("update-viewport", ev => instance.renderFrame()); - - //to do: support pages of sequence mode - - //to do: add handler for resize event to auto-sync - - let viewerBounds = instance._viewer.viewport.getBoundsNoRotate(); - instance._scene = new THREE.Scene(); - instance._imageContainer = new THREE.Group(); - instance._camera = new THREE.OrthographicCamera( - viewerBounds.width / -2, - viewerBounds.width / 2, - viewerBounds.height / 2, - viewerBounds.height / -2, - 0, - 10000 - ); - instance._camera.position.x = viewerBounds.x + viewerBounds.width/2; - instance._camera.position.y = -(viewerBounds.y + viewerBounds.height/2); - instance._camera.position.z = 100; - instance._camera.lookAt(instance._camera.position.x, instance._camera.position.y, 0); - instance._camera.updateProjectionMatrix(); - - var light = new THREE.AmbientLight(); - instance._scene.add(light); - instance._scene.add(instance._imageContainer); - - instance._scene.background = new THREE.Color(0.25, 0.25, 0.25); - - instance._renderer = new THREE.WebGLRenderer({canvas: instance._canvas}); -} - -//private -function tileReady(event, instance){ - let tile = event.tile; - let tiledImage = event.tiledImage; - - //create a THREE.Material with the image data for this tile - let texture = new THREE.CanvasTexture(event.tile.getCanvasContext().canvas); - texture.flipY = false; // To match OSD reference frame - let material = new THREE.MeshLambertMaterial({ - map: texture, - transparent: !!tile.hasTransparency || tiledImage.opacity < 1, - opacity: tiledImage.opacity - }); - - //attach the material to the tile so it can be queried by location using OpenSeadragon methods - tile._three = material; - - //cache the bounds for this material so it doesn't have to be recomputed every time it is used to update the scene - material.userData._tileBounds = tiledImage.source.getTileBounds(tile.level, tile.x, tile.y); - material.userData.hasTransparency = !!tile.hasTransparency; - material.userData.tile = tile; - material.userData.tiledImage = tiledImage; - - //since a new tile is available, update the image (if needed) - updateTiledImageRendering(tiledImage, tile, instance); -} - -function tileUnloaded(event, instance){ - let tile = event.tile; - cleanupObject(tile._three); - delete tile._three; - - updateTiledImageRendering(event.tiledImage, tile, instance); -} - -function viewportChange(event, instance){ - let viewer = event.eventSource; - - let viewerBounds = viewer.viewport.getBoundsNoRotate(true); - instance._camera.left = viewerBounds.width / -2; - instance._camera.right = viewerBounds.width / 2; - instance._camera.top = viewerBounds.height / 2; - instance._camera.bottom = viewerBounds.height / -2; - - let center = viewer.viewport.getCenter(true); - instance._camera.position.x = center.x; - instance._camera.position.y = -center.y; - instance._camera.rotation.z = viewer.viewport.getRotation(true) * Math.PI / 180; - - instance._camera.updateProjectionMatrix(); - - let numItems = viewer.world.getItemCount(); - let i; - for(i = 0; i < numItems; i++){ - let tiledImage = viewer.world.getItemAt(i); - updateMeshIfNeeded(tiledImage); - } - - instance.renderFrame(); -} - - -function addTiledImage(event, instance){ - let tiledImage = event.item; - - //create a Group for the tiles of this tiled image. - if(!tiledImage._three){ - let tileContainer = new THREE.Group(); - let rotationAxis = new THREE.Group(); - let positioningGroup = new THREE.Group(); - rotationAxis.add(tileContainer); - positioningGroup.add(rotationAxis); - positioningGroup.userData._tileContainer = tileContainer; - - //add the object to the group of images (i.e. add to the scene) - instance._imageContainer.add(positioningGroup); - - //save mutual references between OpenSceneGraph and ThreeJSRenderer versions of tiledImages - tiledImage._three = positioningGroup; - positioningGroup._tiledImage = tiledImage; - - //offset the tileContainer so the center of the image is at the origin of the parent group - tileContainer.position.x = -0.5; - tileContainer.position.y = -0.5 / tiledImage.source.aspectRatio; - - //undo the offset of the tileContainer, moving this back into original viewport coordinate space - rotationAxis.position.x = tileContainer.position.x * -1; - rotationAxis.position.y = tileContainer.position.y * -1; - - - updateTiledImageParameters(instance, tiledImage, positioningGroup, rotationAxis); - tiledImage.addHandler('bounds-change',()=>updateTiledImageParameters(instance, tiledImage, positioningGroup, rotationAxis, true)); - tiledImage.addHandler('opacity-change',()=>updateTiledImageParameters(instance, tiledImage, positioningGroup, rotationAxis, true)); - - } - setItemOrder(null, instance); -} - -function removeTiledImage(event){ - let tiledImage = event.item; - //to do: make sure all resources for all tiles are unloaded (even if not actively in the tile group) - tiledImage._three.removeFromParent(); - cleanupObject(tiledImage._three); - delete tiledImage._three; - -} - -function updateTiledImageParameters(instance, tiledImage, positioningGroup, rotationAxis, requestRender){ - let bounds = tiledImage.getBoundsNoRotate(true); - let rotation = tiledImage.getRotation(true); - - //set size and location - positioningGroup.scale.x = bounds.width; //scale the normalized image coordinates to match the size within the world - positioningGroup.scale.y = -bounds.width; //flip Y - positioningGroup.position.x = bounds.x; - positioningGroup.position.y = bounds.y * -1;//flip Y - - // rotate about the rotation axis - rotationAxis.rotation.z = rotation * Math.PI / 180; - rotationAxis.scale.x = tiledImage.getFlip() ? -1 : 1; - - updateOpacity(tiledImage._three.userData._tileContainer, tiledImage.opacity); - - if(requestRender){ - instance.renderFrame(); - } -} - -function updateOpacity(meshGroup, opacity){ - meshGroup.children.forEach(mesh=>{ - mesh.material.opacity = opacity; - if(opacity < 1 || mesh.material.userData.hasTransparency){ - mesh.material.transparent = true; - } else { - mesh.material.transparent = false; - } - }) -} - -function updateTiledImageRendering(tiledImage, tile, instance){ - updateMeshIfNeeded(tiledImage); - - let tileContainer = tiledImage._three.userData._tileContainer; - let level = tileContainer.userData._tiledImageLevel; - - //whether the tile was just loaded or unloaded, update any tiles that it overlaps in the current tileGrid (as needed) - let topLeft = tiledImage.source.getTileAtPoint(level, {x: tile.bounds.x, y: tile.bounds.y}); - let bottomRight = tiledImage.source.getTileAtPoint(level, {x: tile.bounds.x + tile.bounds.width, y: tile.bounds.y + tile.bounds.height}); - - //iterate over the tiles overlapped by this one - let x, y; - for(x = topLeft.x; x<= bottomRight.x; x++){ - for(y = topLeft.y; y <= bottomRight.y; y++){ - let mesh = tileContainer.userData._tileMatrix[x][y]; - loadBestImage(mesh); - } - } - - instance.renderFrame(); -} - -function setItemOrder(event, instance){ - instance._imageContainer.children.forEach(child=>{ - child.position.z = DEPTH_MULTIPLIER * instance._viewer.world.getIndexOfItem(child._tiledImage); - }); - instance.renderFrame(); -} - -function updateMeshIfNeeded(tiledImage){ - let tileContainer = tiledImage._three.userData._tileContainer; - let levelsInterval = tiledImage._getLevelsInterval(); - let level = levelsInterval.highestLevel; - - if(tileContainer.userData._tiledImageLevel === level){ - //we are already drawing the highest-resolution tiles, just return - return; - } - console.log('new level', level); - tileContainer.userData._tiledImageLevel = level; - //we need to update the grid. - //clear the old matrix - tileContainer.userData._tileMatrix = []; - //remove existing tiles - - tileContainer.children.forEach(cleanupObject); - tileContainer.clear(); - - //create new set of tiles and add to the tileContainer - let gridInfo = tiledImage.getGridDefinition(level); - let col, row; - for(col = 0; col < gridInfo.numColumns; col += 1){ - tileContainer.userData._tileMatrix[col] = []; - for(row = 0; row < gridInfo.numRows; row += 1){ - let colInfo = gridInfo.columnInfo[col]; - let rowInfo = gridInfo.rowInfo[row]; - - let left = colInfo.x; - let top = rowInfo.y; - let x = left + colInfo.width / 2; - let y = top + rowInfo.height / 2; - let z = 0; - - let tileGeometry = new THREE.PlaneGeometry(colInfo.width, rowInfo.height); - let mesh = new THREE.Mesh(tileGeometry); - - - mesh.position.set(x, y, z); - - mesh._tileInfo = { - row: row, - col: col, - level: level, - ...rowInfo, - ...colInfo, - tiledImage: tiledImage, - center: new OpenSeadragon.Point(x, y), - uvMapOriginal: [], - // uvMap - } - - let i; - let uvAttribute = tileGeometry.attributes.uv; - - for(i =0 ; i= 0){ - let tileIndex = tileSource.getTileAtPoint(queryLevel, tileInfo.center); - let tile = hasMaterial(tilesMatrix, queryLevel, tileIndex.x, tileIndex.y); - if(tile){ - addMaterialToMesh(mesh, tile, tiledImage); - break; - } - queryLevel--; - } - } -} - -function hasMaterial(tileMatrix, level, x, y){ - let tile = tileMatrix[level] && tileMatrix[level][x] && tileMatrix[level][x][y]; - return tile && tile._three ? tile : null; -} - -function addMaterialToMesh(mesh, tile, tiledImage){ - let regionInfo = mesh._tileInfo; - let material = tile._three; - let materialBounds = material.userData._tileBounds; - - //update transparent and opacity properties to reflect current state - let opacity = tiledImage.opacity; - let transparent = opacity < 1 || material.userData.hasTransparency; - material.transparent = transparent; - material.opacity = opacity; - - mesh.material = material; - let uvMap = mesh._tileInfo.uvMapOriginal; - let uvAttribute = mesh.geometry.attributes.uv; - - // iterate over UV map for each vertex and calculate position within material/texture - let xNew, yNew; - let regionLeft = regionInfo.x; - let regionTop = regionInfo.y; - let regionRight = regionLeft + regionInfo.width; - let regionBottom = regionTop + regionInfo.height; - - //what is needed to calculate the right uv index for each vertex? - // 1) position of the vertex in normalized coordinates - // 2) position of the entire texture area (not just non-overlapped area) in normalized coordinates - - uvMap.forEach(([x,y],i)=>{ - // x, y describe which corner of the original texture to use - if(x==0){ - xNew = (regionLeft - materialBounds.x) / materialBounds.width; - } else { - xNew = (regionRight - materialBounds.x) / materialBounds.width; - } - - if(y == 0){ - yNew = (regionTop - materialBounds.y) / materialBounds.height; - } else { - yNew = (regionBottom - materialBounds.y) / materialBounds.height; - } - - uvAttribute.setXY(i, xNew, yNew); - }); - uvAttribute.needsUpdate = true; - -} - - - - -function cleanupObject(object){ - if(object.children && object.children.forEach){ - object.children.forEach(cleanupObject); - } - if(object.dispose){ - object.dispose(); - } - if(object.geometry){ - object.geometry.dispose(); - } -} \ No newline at end of file diff --git a/test/demo/webgl.html b/test/demo/webgl.html index 7d082c60..f3aa1c78 100644 --- a/test/demo/webgl.html +++ b/test/demo/webgl.html @@ -16,77 +16,156 @@ -
- Simple demo page to show three.js based rendering. -
-
+
+

Use a WebGL drawer implementation (using three.js) instead of the default context2d drawer

+
+
+
+ The context2d drawing operations in core OpenSeadragon have been + consolidated from the
TiledImage
and
Tile
classes into the +
Drawer
class, which inherits from a new
DrawerBase
base class. + The
TiledImage
and
Tile
classes now handle the logic of managing + tile positioning and image data, with cleaner separation from details of the rendering + process.
DrawerBase
defines a public API that core OpenSeadragon code uses to + interact with the drawer implementation. To use a custom drawer/render, define + a new class that inherits from
DrawerBase
and implements the public API. + The constructor of this class can be passed in during construction of the viewer using the + new
customDrawer
option. +
+
+    import { ThreeJSDrawer } from './threejsdrawer.js';
 
-    
-
- - - - - - - - + let viewer = OpenSeadragon({ + ... + customDrawer: ThreeJSDrawer, + ... + }); +
+
+
-
- - - - - - - - + + +

Compare behavior of Context2d and WebGL (via three.js) drawers

+
+
+

Use default OpenSeadragon viewer to pan/zoom

+
+
+ +
+

WebGL drawer linked using event listeners

+
+
-
- - - - - - - - + + +
+

Image options (drag and drop to re-order images)

+
+ + +
+ + + + + + + + + + +
+ +
+
+ + +
+ + + + + + + + + + +
+
+
+ + +
+ + + + + + + + + + +
+
+
- +
- + diff --git a/test/demo/webgl.js b/test/demo/webgl.js index 9ed7028e..5ccd5d30 100644 --- a/test/demo/webgl.js +++ b/test/demo/webgl.js @@ -1,8 +1,8 @@ //imports -import { ThreeJSRenderer } from './webgl-renderer.js'; +import { ThreeJSDrawer } from './threejsdrawer.js'; //globals -const canvas = document.querySelector('#three-canvas'); +// const canvas = document.querySelector('#three-canvas'); const sources = { "rainbow":"../data/testpattern.dzi", "leaves":"../data/iiif_2_0_sizes/info.json", @@ -10,37 +10,38 @@ const sources = { type:'image', url: "../data/BBlue.png", }, - // "duomo":"https://openseadragon.github.io/example-images/highsmith/highsmith.dzi" } var viewer = window.viewer = OpenSeadragon({ - // debugMode: true, id: "contentDiv", prefixUrl: "../../build/openseadragon/images/", - showNavigator:true, - minZoomImageRatio:0.001, - customRenderer: true, // set this to true to use a renderer plugin instead of the built-in drawer - useCanvas: {contextType: 'webgl2'} //set this to match the context type used by the plugin renderer + minZoomImageRatio:0.01, }); -//sync size +let threeRenderer = window.threeRenderer = new ThreeJSDrawer({viewer, viewport: viewer.viewport, element:viewer.element}); -// let viewerCanvas = viewer.drawer.canvas; -// canvas.style.width = viewerCanvas.clientWidth+'px'; -// canvas.style.height = viewerCanvas.clientHeight+'px'; -// canvas.width = viewerCanvas.width; -// canvas.height = viewerCanvas.height; +var viewer2 = window.viewer2 = OpenSeadragon({ + id: "three-viewer", + prefixUrl: "../../build/openseadragon/images/", + minZoomImageRatio:0.01, + customDrawer: ThreeJSDrawer, + tileSources: sources['leaves'], + imageSmoothingEnabled: false, +}); -// //make the test canvas mirror all changes to the viewer canvas -// viewer.addHandler("resize", function(){ -// canvas.style.width = viewerCanvas.clientWidth+'px'; -// canvas.style.height = viewerCanvas.clientHeight+'px'; -// }) -let noCanvas; +//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 threeRenderer = window.threeRenderer = new ThreeJSRenderer(viewer, noCanvas); // viewer.addHandler("open", () => viewer.world.getItemAt(0).source.hasTransparency = function(){ return true; }); - +$('#three-viewer').resizable(true); $('#contentDiv').resizable(true); $('#image-picker').sortable({ update: function(event, ui){ @@ -90,9 +91,82 @@ $('#image-picker input:not(.toggle)').on('change',function(){ tiledImage.setOpacity(Number(value)); } else if (field == 'flipped'){ tiledImage.setFlip($(this).prop('checked')); + } else if (field == 'cropped'){ + if( $(this).prop('checked') ){ + let croppingPolygons = [ [{x:200, y:200}, {x:800, y:200}, {x:500, y:800}] ]; + tiledImage.setCroppingPolygons(croppingPolygons); + } else { + tiledImage.resetCroppingPolygons(); + } + } else if (field == 'clipped'){ + if( $(this).prop('checked') ){ + let clipRect = new OpenSeadragon.Rect(2000, 0, 3000, 4000); + tiledImage.setClip(clipRect); + } else { + tiledImage.setClip(null); + } + } + else if (field == 'debug'){ + if( $(this).prop('checked') ){ + tiledImage.debugMode = true; + } else { + tiledImage.debugMode = false; + } } } -}) +}); + +$('.image-options select[data-field=composite]').append(getCompositeOperationOptions()).on('change',function(){ + let data = $(this).data(); + let tiledImage = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item'); + if(tiledImage){ + tiledImage.setCompositeOperation(this.value == 'null' ? null : this.value); + } +}).trigger('change'); + +$('.image-options select[data-field=wrapping]').append(getWrappingOptions()).on('change',function(){ + let data = $(this).data(); + let tiledImage = $(`#image-picker input.toggle[data-image=${data.image}]`).data('item'); + if(tiledImage){ + switch(this.value){ + case "None": tiledImage.wrapHorizontal = tiledImage.wrapVertical = false; break; + case "Horizontal": tiledImage.wrapHorizontal = true; tiledImage.wrapVertical = false; break; + case "Vertical": tiledImage.wrapHorizontal = false; tiledImage.wrapVertical = true; break; + case "Both": tiledImage.wrapHorizontal = tiledImage.wrapVertical = true; break; + } + tiledImage.viewer.raiseEvent('opacity-change');//trigger a redraw for the webgl renderer. TODO: fix this hack. + } +}).trigger('change'); + +function getWrappingOptions(){ + let opts = ['None', 'Horizontal', 'Vertical', 'Both']; + let elements = opts.map((opt, i)=>{ + let el = $('