diff --git a/src/navigator.js b/src/navigator.js index 9b64d2bd..5414a53e 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -452,6 +452,7 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* myItem.setWidth(bounds.width, immediately); myItem.setRotation(theirItem.getRotation(), immediately); myItem.setClip(theirItem.getClip()); + myItem.setFlip(theirItem.getFlip()); }, // private diff --git a/src/tile.js b/src/tile.js index 1ea7c5d9..701db750 100644 --- a/src/tile.js +++ b/src/tile.js @@ -176,6 +176,12 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja * @memberof OpenSeadragon.Tile# */ this.size = null; + /** + * Whether to flip the tile when rendering. + * @member {Boolean} flipped + * @memberof OpenSeadragon.Tile# + */ + this.flipped = false; /** * The start time of this tile's blending. * @member {Number} blendStart @@ -296,6 +302,10 @@ $.Tile.prototype = { 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 ); }, @@ -376,13 +386,17 @@ $.Tile.prototype = { 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, - position.x, + -size.x / 2, position.y, size.x, size.y diff --git a/src/tiledimage.js b/src/tiledimage.js index 74ffacef..82e35ed7 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -392,6 +392,26 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag return bounds.rotate(this.getRotation(current), this._getRotationPoint(current)); }, + /** + * @function + * @param {Number} level + * @param {Number} x + * @param {Number} y + * @returns {OpenSeadragon.Rect} Where this tile fits (in normalized coordinates). + */ + getTileBounds: function( level, x, y ) { + var numTiles = this.source.getNumTiles(level); + var xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + var yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + var bounds = this.source.getTileBounds(level, xMod, yMod); + if (this.getFlip()) { + bounds.x = 1 - bounds.x - bounds.width; + } + bounds.x += (x - xMod) / numTiles.x; + bounds.y += (this._worldHeightCurrent / this._worldWidthCurrent) * ((y - yMod) / numTiles.y); + return bounds; + }, + /** * @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels. */ @@ -832,6 +852,23 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this.raiseEvent('clip-change'); }, + /** + * @returns {Boolean} Whether the TiledImage should be flipped before rendering. + */ + getFlip: function() { + return !!this.flipped; + }, + + /** + * @param {Boolean} flip Whether the TiledImage should be flipped before rendering. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setFlip: function(flip) { + this.flipped = !!flip; + this._needsDraw = true; + this._raiseBoundsChange(); + }, + /** * @returns {Number} The TiledImage's current opacity. */ @@ -1255,24 +1292,41 @@ function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity, var viewportCenter = tiledImage.viewport.pixelFromPoint( tiledImage.viewport.getCenter()); + + if (tiledImage.getFlip()) { + // The right-most tile can be narrower than the others. When flipped, + // this tile is now on the left. Because it is narrower than the normal + // left-most tile, the subsequent tiles may not be wide enough to completely + // fill the viewport. Fix this by rendering an extra column of tiles. If we + // are not wrapping, make sure we never render more than the number of tiles + // in the image. + bottomRightTile.x += 1; + if (!tiledImage.wrapHorizontal) { + bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1); + } + } + for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) { for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) { - // Optimisation disabled with wrapping because getTileBounds does not - // work correctly with x and y outside of the number of tiles - if (!tiledImage.wrapHorizontal && !tiledImage.wrapVertical) { - var tileBounds = tiledImage.source.getTileBounds(level, x, y); - if (drawArea.intersection(tileBounds) === null) { - // This tile is outside of the viewport, no need to draw it - continue; - } + var flippedX; + if (tiledImage.getFlip()) { + var xMod = ( numberOfTiles.x + ( x % numberOfTiles.x ) ) % numberOfTiles.x; + flippedX = x + numberOfTiles.x - xMod - xMod - 1; + } else { + flippedX = x; + } + + if (drawArea.intersection(tiledImage.getTileBounds(level, flippedX, y)) === null) { + // This tile is outside of the viewport, no need to draw it + continue; } best = updateTile( tiledImage, drawLevel, haveDrawn, - x, y, + flippedX, y, level, levelOpacity, levelVisibility, @@ -1447,10 +1501,10 @@ function getTile( tilesMatrix[ level ][ x ] = {}; } - if ( !tilesMatrix[ level ][ x ][ y ] ) { + if ( !tilesMatrix[ level ][ x ][ y ] || !tilesMatrix[ level ][ x ][ y ].flipped !== !tiledImage.flipped ) { xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; - bounds = tileSource.getTileBounds( level, xMod, yMod ); + bounds = tiledImage.getTileBounds( level, x, y ); sourceBounds = tileSource.getTileBounds( level, xMod, yMod, true ); exists = tileSource.tileExists( level, xMod, yMod ); url = tileSource.getTileUrl( level, xMod, yMod ); @@ -1469,9 +1523,6 @@ function getTile( context2D = tileSource.getContext2D ? tileSource.getContext2D(level, xMod, yMod) : undefined; - bounds.x += ( x - xMod ) / numTiles.x; - bounds.y += (worldHeight / worldWidth) * (( y - yMod ) / numTiles.y); - tile = new $.Tile( level, x, @@ -1485,14 +1536,22 @@ function getTile( sourceBounds ); - if (xMod === numTiles.x - 1) { - tile.isRightMost = true; + if (tiledImage.getFlip()) { + if (xMod === 0) { + tile.isRightMost = true; + } + } else { + if (xMod === numTiles.x - 1) { + tile.isRightMost = true; + } } if (yMod === numTiles.y - 1) { tile.isBottomMost = true; } + tile.flipped = tiledImage.flipped; + tilesMatrix[ level ][ x ][ y ] = tile; } diff --git a/src/viewer.js b/src/viewer.js index 29b2054a..9592abf9 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1301,6 +1301,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @param {Boolean} [options.preload=false] Default switch for loading hidden images (true loads, false blocks) * @param {Number} [options.degrees=0] Initial rotation of the tiled image around * its top left corner in degrees. + * @param {Boolean} [options.flipped=false] Whether to horizontally flip the image. * @param {String} [options.compositeOperation] How the image is composited onto other images. * @param {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image, * overriding viewer.crossOriginPolicy. @@ -1463,6 +1464,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, opacity: queueItem.options.opacity, preload: queueItem.options.preload, degrees: queueItem.options.degrees, + flipped: queueItem.options.flipped, compositeOperation: queueItem.options.compositeOperation, springStiffness: _this.springStiffness, animationTime: _this.animationTime, diff --git a/test/demo/flipping.html b/test/demo/flipping.html new file mode 100644 index 00000000..3d3eb99e --- /dev/null +++ b/test/demo/flipping.html @@ -0,0 +1,113 @@ + + + + OpenSeadragon Flipping Demo + + + + + +
+ Simple demo page to show image flipping. +
+
+ + +
+
+ First +
+ + +
+
+ + +
+
+ +
+ Second +
+ + +
+
+ + +
+
+ +
+ Viewport +
+ + +
+ +
+ + +
+
+ + +