From 4b13cf32fd19a417054492a867c4e20270c300f5 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Fri, 30 Oct 2020 23:27:26 +0000 Subject: [PATCH 01/10] Introduce getTileBounds method for tiledImage This wraps the implementation in tileSource but provides support for wrapping. It does not support getting the source bounds. Using this function instead of the tileSource version allows the viewport clipping optimization to work with wrapping. --- src/tiledimage.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index 74ffacef..b89a0d8e 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -392,6 +392,23 @@ $.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); + 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. */ @@ -1258,14 +1275,9 @@ function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity, 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; - } + if (drawArea.intersection(tiledImage.getTileBounds(level, x, y)) === null) { + // This tile is outside of the viewport, no need to draw it + continue; } best = updateTile( @@ -1450,7 +1462,7 @@ function getTile( if ( !tilesMatrix[ level ][ x ][ y ] ) { 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 +1481,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, From afa8c2d1bdced1c5ec2fdef5c92822f3b76b2314 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Thu, 29 Oct 2020 20:02:35 +0000 Subject: [PATCH 02/10] Store the flipped state in each tile and render it as such This will flip each individual tile on a per image bases. However the tiles are now drawn in the wrong locations. Clipping etc works. this is implemented for Canvas and HTML renderers. --- src/tile.js | 16 +++++++++++++++- src/tiledimage.js | 8 ++++++++ src/viewer.js | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) 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 b89a0d8e..2b9efd26 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -849,6 +849,13 @@ $.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; + }, + /** * @returns {Number} The TiledImage's current opacity. */ @@ -1701,6 +1708,7 @@ function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tile.size = sizeC; tile.squaredDistance = tileSquaredDistance; tile.visibility = levelVisibility; + tile.flipped = tiledImage.getFlip(); } /** 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, From 3c57063632dd56606ce4872170bf739533719bd5 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Sat, 31 Oct 2020 00:23:41 +0000 Subject: [PATCH 03/10] Render the flipped columns in reverse order This completes the per-image flip implementation. Tile bounds are re-positioned within the image. When rendering, the x ordinals are remapped to the flipped ones. To use, set "flipped" on the image instead of the viewer. The code is compatible with rotations and wrapping. Implements #1553 --- src/tiledimage.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index 2b9efd26..1e55b914 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -404,6 +404,9 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag 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; @@ -1279,10 +1282,32 @@ 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++) { - if (drawArea.intersection(tiledImage.getTileBounds(level, x, y)) === null) { + 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; } @@ -1291,7 +1316,7 @@ function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity, tiledImage, drawLevel, haveDrawn, - x, y, + flippedX, y, level, levelOpacity, levelVisibility, From b2b95e8556454a1831f45747f36722d95ed8603c Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Sat, 31 Oct 2020 01:59:00 +0000 Subject: [PATCH 04/10] Correctly set the rightmost tile property when flipped This ensures that seams are not visible in Firefox and Safari when the image is wrapped horizontally and also flipped. --- src/tiledimage.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index 1e55b914..c3712aca 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1526,8 +1526,14 @@ 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) { From 53052c8c08eabaf73ac93a55771c593e9be8657e Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Fri, 19 Mar 2021 15:45:21 +0000 Subject: [PATCH 05/10] Add flipping example This isn't complete - the flip toggles do not work as setFlip() is not implemented. The second image is currently always flipped. --- test/demo/flipping.html | 110 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 test/demo/flipping.html diff --git a/test/demo/flipping.html b/test/demo/flipping.html new file mode 100644 index 00000000..6d5f1469 --- /dev/null +++ b/test/demo/flipping.html @@ -0,0 +1,110 @@ + + + + OpenSeadragon Flipping Demo + + + + + +
+ Simple demo page to show image flipping. +
+
+ + +
+
+ First +
+ + +
+
+ + +
+
+ +
+ Second +
+ + +
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ + + From 3161808a9d0ad841072a08d1975784fda62702d3 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Fri, 19 Mar 2021 16:33:10 +0000 Subject: [PATCH 06/10] Add a basic setFlip method to TiledImage This doesn't fully work - even raising a bounds-change doesn't seem to be enough. --- src/tiledimage.js | 10 ++++++++++ test/demo/flipping.html | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index c3712aca..61de4e08 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -859,6 +859,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag 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. */ diff --git a/test/demo/flipping.html b/test/demo/flipping.html index 6d5f1469..080d0f68 100644 --- a/test/demo/flipping.html +++ b/test/demo/flipping.html @@ -36,7 +36,7 @@ First
- +
@@ -48,7 +48,7 @@ Second
- +
From 7552806a4717f59aaf93b7d584b624228476a1cd Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Mon, 22 Mar 2021 06:30:06 +0000 Subject: [PATCH 07/10] Force reload tiles when the tile's flip doesn't match the image Flipping an image changes the bounds of each tile. The existing code assumes that cannot happen. getTile() calculates the tile bounds the first time it is asked for a particular tile. It then caches and returns the same time on every subsequent call. getTile() has a check to test if a tile exists in the cache. If it does not, the tile is created and inserted. In order to make tiles be rebuilt after a flip, we only need to check if the tile's flip matches the image's flip. If not, we can recreate the tile as if it did not exist. To make this a bit clearer, the tile's flipped flag is now set in getTile() rather than positionTile(). This makes setFlip() work. --- src/tiledimage.js | 6 +++--- test/demo/flipping.html | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index 61de4e08..e2459660 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -866,7 +866,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag setFlip: function(flip) { this.flipped = !!flip; this._needsDraw = true; - this._raiseBoundsChange(); }, /** @@ -1501,7 +1500,7 @@ 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 = tiledImage.getTileBounds( level, x, y ); @@ -1550,6 +1549,8 @@ function getTile( tile.isBottomMost = true; } + tile.flipped = tiledImage.flipped; + tilesMatrix[ level ][ x ][ y ] = tile; } @@ -1749,7 +1750,6 @@ function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tile.size = sizeC; tile.squaredDistance = tileSquaredDistance; tile.visibility = levelVisibility; - tile.flipped = tiledImage.getFlip(); } /** diff --git a/test/demo/flipping.html b/test/demo/flipping.html index 080d0f68..f31b8ebc 100644 --- a/test/demo/flipping.html +++ b/test/demo/flipping.html @@ -36,7 +36,7 @@ First
- +
@@ -48,7 +48,7 @@ Second
- +
From 409620fa388c4c067e83964ee50b72117ecc01f7 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Tue, 23 Mar 2021 02:26:06 +0000 Subject: [PATCH 08/10] Tidy up the tile/image flip check Don't need double negation and brackets here. --- src/tiledimage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index e2459660..f6eb36f2 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1500,7 +1500,7 @@ function getTile( tilesMatrix[ level ][ x ] = {}; } - if ( !tilesMatrix[ level ][ x ][ y ] || ((!!tilesMatrix[ level ][ x ][ y ].flipped) !== (!!tiledImage.flipped)) ) { + 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 = tiledImage.getTileBounds( level, x, y ); From eebfdc139135ec3ead2387059794d7732b856482 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Tue, 23 Mar 2021 03:12:58 +0000 Subject: [PATCH 09/10] Improve the flipping example Adds tiled images to the viewer at creation so that they are properly centred. This also checks the current state of the test checkboxes on loading. Some browsers, notably Firefox, will remember the value of checkboxes across reloads. This can lead to the checkboxes being out of sync with with viewer after a reload. An alternative is to set autocomplete="off" on each checkbox element. This will force the browser to reset the field to the default specified in the HTML. However I think checking the actual value is preferable as it means the defaults are only specified in one place. --- test/demo/flipping.html | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/test/demo/flipping.html b/test/demo/flipping.html index f31b8ebc..3d3eb99e 100644 --- a/test/demo/flipping.html +++ b/test/demo/flipping.html @@ -20,8 +20,6 @@ margin: 0.3em; } - - @@ -57,6 +55,7 @@
+ Viewport
@@ -73,21 +72,25 @@ // debugMode: true, id: "contentDiv", prefixUrl: "../../build/openseadragon/images/", - showNavigator:true + showNavigator:true, + tileSources: [ + { + tileSource: "../data/testpattern.dzi", + x: 0, + y: 0, + flipped: document.getElementById("ffirst").checked, + degrees: document.getElementById("rfirst").checked * 45, + }, { + tileSource: "../data/testpattern.dzi", + x: 1, + y: 0, + flipped: document.getElementById("fsecond").checked, + degrees: document.getElementById("rsecond").checked * 45, + } + ] }); - var first = viewer.addTiledImage({ - tileSource: "../data/testpattern.dzi", - x: 0, - y: 0, - }); - - var second = viewer.addTiledImage({ - tileSource: "../data/testpattern.dzi", - x: 1, - y: 0, - flipped: true, - }); + viewer.viewport.setFlip(document.getElementById("fview").checked); function debug(v) { viewer.setDebugMode(v); From e6725871b8138e0c253ac8b177ddf6eefb1433c7 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Fri, 26 Mar 2021 11:18:21 +0000 Subject: [PATCH 10/10] Make setFlip() update the navigator Makes setFlip() raise a bounds change, and makes the navigator copy the image flip in addition to the other properties, when receiving the bounds signal. --- src/navigator.js | 1 + src/tiledimage.js | 1 + 2 files changed, 2 insertions(+) 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/tiledimage.js b/src/tiledimage.js index f6eb36f2..82e35ed7 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -866,6 +866,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag setFlip: function(flip) { this.flipped = !!flip; this._needsDraw = true; + this._raiseBoundsChange(); }, /**