diff --git a/src/drawer.js b/src/drawer.js index fca4b956..30d7fcf2 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -344,15 +344,18 @@ $.Drawer.prototype = { * 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. */ - drawTile: function(tile, drawingHandler, useSketch, scale, translate) { + drawTile: function(tile, drawingHandler, useSketch, scale, translate, shouldRoundPositionAndSize) { $.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); + tile.drawCanvas(context, drawingHandler, scale, translate, shouldRoundPositionAndSize); } else { tile.drawHTML( this.canvas ); } diff --git a/src/openseadragon.js b/src/openseadragon.js index f84ebfb1..4f982eba 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -209,6 +209,17 @@ * You can pass a CSS color value like "#FF8800". * When passing a function the tiledImage and canvas context are available as argument which is useful when you draw a gradient or pattern. * + * @property {Object} [subPixelRoundingForTransparency=null] + * Determines when subpixel rounding should be applied for tiles when rendering images that support transparency. + * This property is a subpixel rounding enum values dictionary [{@link BROWSERS}] --> {@link SUBPIXEL_ROUNDING_OCCURRENCES}. + * The key is a {@link BROWSERS} value, and the value is one of {@link SUBPIXEL_ROUNDING_OCCURRENCES}, + * indicating, for a given browser, when to apply subpixel rounding. + * Key '*' is the fallback value for any browser not specified in the dictionary. + * This property has a simple mode, and one can set it directly to + * {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER}, {@link SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST} or {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS} + * in order to apply this rule for all browser. The values {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS} would be equivalent to { '*', SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS }. + * The default is {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} for all browsers, for backward compatibility reason. + * * @property {Number} [degrees=0] * Initial rotation. * @@ -1258,11 +1269,12 @@ function OpenSeadragon( options ){ flipped: false, // APPEARANCE - opacity: 1, - preload: false, - compositeOperation: null, - imageSmoothingEnabled: true, - placeholderFillStyle: null, + opacity: 1, + preload: false, + compositeOperation: null, + imageSmoothingEnabled: true, + placeholderFillStyle: null, + subPixelRoundingForTransparency: null, //REFERENCE STRIP SETTINGS showReferenceStrip: false, @@ -1403,6 +1415,20 @@ function OpenSeadragon( options ){ CHROMEEDGE: 7 }, + /** + * An enumeration of when subpixel rounding should occur. + * @static + * @type {Object} + * @property {Number} NEVER Never apply subpixel rounding for transparency. + * @property {Number} ONLY_AT_REST Do not apply subpixel rounding for transparency during animation (panning, zoom, rotation) and apply it once animation is over. + * @property {Number} ALWAYS Apply subpixel rounding for transparency during animation and when animation is over. + */ + SUBPIXEL_ROUNDING_OCCURRENCES: { + NEVER: 0, + ONLY_AT_REST: 1, + ALWAYS: 2 + }, + /** * Keep track of which {@link Viewer}s have been created. * - Key: {@link Element} to which a Viewer is attached. diff --git a/src/tile.js b/src/tile.js index 701db750..f003c157 100644 --- a/src/tile.js +++ b/src/tile.js @@ -318,8 +318,11 @@ $.Tile.prototype = { * 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. */ - drawCanvas: function( context, drawingHandler, scale, translate ) { + drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize ) { var position = this.position.times($.pixelDensityRatio), size = this.size.times($.pixelDensityRatio), @@ -363,6 +366,14 @@ $.Tile.prototype = { //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._hasTransparencyChannel()) { + 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( diff --git a/src/tiledimage.js b/src/tiledimage.js index 1942c75c..2817710f 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -160,24 +160,25 @@ $.TiledImage = function( options ) { _hasOpaqueTile: false, // Do we have even one fully opaque tile? _tilesLoading: 0, // The number of pending tile requests. //configurable settings - springStiffness: $.DEFAULT_SETTINGS.springStiffness, - animationTime: $.DEFAULT_SETTINGS.animationTime, - minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, - wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, - wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, - immediateRender: $.DEFAULT_SETTINGS.immediateRender, - blendTime: $.DEFAULT_SETTINGS.blendTime, - alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, - minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, - smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom, - iOSDevice: $.DEFAULT_SETTINGS.iOSDevice, - debugMode: $.DEFAULT_SETTINGS.debugMode, - crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, - ajaxWithCredentials: $.DEFAULT_SETTINGS.ajaxWithCredentials, - placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, - opacity: $.DEFAULT_SETTINGS.opacity, - preload: $.DEFAULT_SETTINGS.preload, - compositeOperation: $.DEFAULT_SETTINGS.compositeOperation + springStiffness: $.DEFAULT_SETTINGS.springStiffness, + animationTime: $.DEFAULT_SETTINGS.animationTime, + minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, + wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, + wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, + immediateRender: $.DEFAULT_SETTINGS.immediateRender, + blendTime: $.DEFAULT_SETTINGS.blendTime, + alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, + minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, + smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom, + iOSDevice: $.DEFAULT_SETTINGS.iOSDevice, + debugMode: $.DEFAULT_SETTINGS.debugMode, + crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, + ajaxWithCredentials: $.DEFAULT_SETTINGS.ajaxWithCredentials, + placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, + opacity: $.DEFAULT_SETTINGS.opacity, + preload: $.DEFAULT_SETTINGS.preload, + compositeOperation: $.DEFAULT_SETTINGS.compositeOperation, + subPixelRoundingForTransparency: $.DEFAULT_SETTINGS.subPixelRoundingForTransparency }, options ); this._preload = this.preload; @@ -1951,6 +1952,73 @@ function compareTiles( previousBest, tile ) { return previousBest; } +/** + * @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); +} + /** * @private * @inner @@ -2099,9 +2167,20 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer.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 = tiledImage.viewer && tiledImage.viewer.isAnimating(); + shouldRoundPositionAndSize = !isAnimating; + } + for (var i = lastDrawn.length - 1; i >= 0; i--) { tile = lastDrawn[ i ]; - tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate ); + tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate, shouldRoundPositionAndSize ); tile.beingDrawn = true; if( tiledImage.viewer ){ diff --git a/src/viewer.js b/src/viewer.js index 9bc939aa..b1a60446 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -713,6 +713,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } THIS[ this.hash ].animating = false; + this.world.removeAll(); this.imageLoader.clear(); @@ -1503,7 +1504,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, ajaxWithCredentials: queueItem.options.ajaxWithCredentials, loadTilesWithAjax: queueItem.options.loadTilesWithAjax, ajaxHeaders: queueItem.options.ajaxHeaders, - debugMode: _this.debugMode + debugMode: _this.debugMode, + subPixelRoundingForTransparency: _this.subPixelRoundingForTransparency }); if (_this.collectionMode) { @@ -2357,6 +2359,10 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } this.goToPage( next ); }, + + isAnimating: function () { + return THIS[ this.hash ].animating; + }, }); @@ -3503,7 +3509,9 @@ function updateOnce( viewer ) { animated = viewer.referenceStrip.update( viewer.viewport ) || animated; } - if ( !THIS[ viewer.hash ].animating && animated ) { + var currentAnimating = THIS[ viewer.hash ].animating; + + if ( !currentAnimating && animated ) { /** * Raised when any spring animation starts (zoom, pan, etc.). * @@ -3517,7 +3525,13 @@ function updateOnce( viewer ) { abortControlsAutoHide( viewer ); } - if ( animated || THIS[ viewer.hash ].forceRedraw || viewer.world.needsDraw() ) { + var isAnimationFinished = currentAnimating && !animated; + + if ( isAnimationFinished ) { + THIS[ viewer.hash ].animating = false; + } + + if ( animated || isAnimationFinished || THIS[ viewer.hash ].forceRedraw || viewer.world.needsDraw() ) { drawWorld( viewer ); viewer._drawOverlays(); if( viewer.navigator ){ @@ -3541,7 +3555,7 @@ function updateOnce( viewer ) { } } - if ( THIS[ viewer.hash ].animating && !animated ) { + if ( isAnimationFinished ) { /** * Raised when any spring animation ends (zoom, pan, etc.). *