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.).
*