From fc69c6568d8a27df04fd72b2e65fb15a464875fc Mon Sep 17 00:00:00 2001
From: Antoine Vandecreme <ant.vand@gmail.com>
Date: Sat, 7 Jan 2017 19:24:29 +0100
Subject: [PATCH 1/3] Animate rotation

---
 src/drawer.js     |  6 ++--
 src/spring.js     |  4 +++
 src/tiledimage.js | 78 +++++++++++++++++++++++++++--------------------
 3 files changed, 52 insertions(+), 36 deletions(-)

diff --git a/src/drawer.js b/src/drawer.js
index 8321b6fe..0289019a 100644
--- a/src/drawer.js
+++ b/src/drawer.js
@@ -500,9 +500,9 @@ $.Drawer.prototype = {
         if ( this.viewport.degrees !== 0 ) {
             this._offsetForRotation({degrees: this.viewport.degrees});
         }
-        if (tiledImage.getRotation() !== 0) {
+        if (tiledImage.getRotation(true) !== 0) {
             this._offsetForRotation({
-                degrees: tiledImage.getRotation(),
+                degrees: tiledImage.getRotation(true),
                 point: tiledImage.viewport.pixelFromPointNoRotate(
                     tiledImage._getRotationPoint(true), true)
             });
@@ -569,7 +569,7 @@ $.Drawer.prototype = {
         if ( this.viewport.degrees !== 0 ) {
             this._restoreRotationChanges();
         }
-        if (tiledImage.getRotation() !== 0) {
+        if (tiledImage.getRotation(true) !== 0) {
             this._restoreRotationChanges();
         }
         context.restore();
diff --git a/src/spring.js b/src/spring.js
index c4342892..30a8956d 100644
--- a/src/spring.js
+++ b/src/spring.js
@@ -206,6 +206,7 @@ $.Spring.prototype = {
 
     /**
      * @function
+     * @returns true if the value got updated, false otherwise
      */
     update: function() {
         this.current.time  = $.now();
@@ -229,11 +230,14 @@ $.Spring.prototype = {
                     ( this.target.time - this.start.time )
                 );
 
+        var oldValue = this.current.value;
         if (this._exponential) {
             this.current.value = Math.exp(currentValue);
         } else {
             this.current.value = currentValue;
         }
+
+        return oldValue != this.current.value;
     },
 
     /**
diff --git a/src/tiledimage.js b/src/tiledimage.js
index 2fa48782..c6cdd7ec 100644
--- a/src/tiledimage.js
+++ b/src/tiledimage.js
@@ -132,7 +132,7 @@ $.TiledImage = function( options ) {
     var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER;
     delete options.fitBoundsPlacement;
 
-    this._degrees = $.positiveModulo(options.degrees || 0, 360);
+    var degrees = $.positiveModulo(options.degrees || 0, 360);
     delete options.degrees;
 
     $.extend( true, this, {
@@ -186,6 +186,12 @@ $.TiledImage = function( options ) {
         animationTime: this.animationTime
     });
 
+    this._degreesSpring = new $.Spring({
+        initial: degrees,
+        springStiffness: this.springStiffness,
+        animationTime: this.animationTime
+    });
+
     this._updateForScale();
 
     if (fitBounds) {
@@ -269,16 +275,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
      * @returns {Boolean} Whether the TiledImage animated.
      */
     update: function() {
-        var oldX = this._xSpring.current.value;
-        var oldY = this._ySpring.current.value;
-        var oldScale = this._scaleSpring.current.value;
+        var xUpdated = this._xSpring.update();
+        var yUpdated = this._ySpring.update();
+        var scaleUpdated = this._scaleSpring.update();
+        var degreesUpdated = this._degreesSpring.update();
 
-        this._xSpring.update();
-        this._ySpring.update();
-        this._scaleSpring.update();
-
-        if (this._xSpring.current.value !== oldX || this._ySpring.current.value !== oldY ||
-                this._scaleSpring.current.value !== oldScale) {
+        if (xUpdated || yUpdated || scaleUpdated || degreesUpdated) {
             this._updateForScale();
             this._needsDraw = true;
             return true;
@@ -313,7 +315,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
      */
     getBounds: function(current) {
         return this.getBoundsNoRotate(current)
-            .rotate(this._degrees, this._getRotationPoint(current));
+            .rotate(this.getRotation(current), this._getRotationPoint(current));
     },
 
     /**
@@ -362,7 +364,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
                 clip.width,
                 clip.height);
         }
-        return bounds.rotate(this._degrees, this._getRotationPoint(current));
+        return bounds.rotate(this.getRotation(current), this._getRotationPoint(current));
     },
 
     /**
@@ -397,7 +399,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
             point = new $.Point(viewerX, viewerY);
         }
 
-        point = point.rotate(-this._degrees, this._getRotationPoint(current));
+        point = point.rotate(-this.getRotation(current), this._getRotationPoint(current));
         return current ?
             this._viewportToImageDelta(
                 point.x - this._xSpring.current.value,
@@ -439,7 +441,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
             point.y += this._ySpring.target.value;
         }
 
-        return point.rotate(this._degrees, this._getRotationPoint(current));
+        return point.rotate(this.getRotation(current), this._getRotationPoint(current));
     },
 
     /**
@@ -453,7 +455,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
      * @param {Boolean} [current=false] - Pass true to use the current location; false for target location.
      * @return {OpenSeadragon.Rect} A rect representing the coordinates in the viewport.
      */
-    imageToViewportRectangle: function( imageX, imageY, pixelWidth, pixelHeight, current ) {
+    imageToViewportRectangle: function(imageX, imageY, pixelWidth, pixelHeight, current) {
         var rect = imageX;
         if (rect instanceof $.Rect) {
             //they passed a rect instead of individual components
@@ -470,7 +472,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
             coordA.y,
             coordB.x,
             coordB.y,
-            rect.degrees + this._degrees
+            rect.degrees + this.getRotation(current)
         );
     },
 
@@ -502,7 +504,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
             coordA.y,
             coordB.x,
             coordB.y,
-            rect.degrees - this._degrees
+            rect.degrees - this.getRotation(current)
         );
     },
 
@@ -555,7 +557,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
     // coordinates (x in [0, 1] and y in [0, aspectRatio])
     _viewportToTiledImageRectangle: function(rect) {
         var scale = this._scaleSpring.current.value;
-        rect = rect.rotate(-this.getRotation(), this._getRotationPoint(true));
+        rect = rect.rotate(-this.getRotation(true), this._getRotationPoint(true));
         return new $.Rect(
             (rect.x - this._xSpring.current.value) / scale,
             (rect.y - this._ySpring.current.value) / scale,
@@ -769,24 +771,34 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
     },
 
     /**
-     * Get the current rotation of this tiled image in degrees.
-     * @returns {Number} the current rotation of this tiled image in degrees.
+     * Get the rotation of this tiled image in degrees.
+     * @param {Boolean} [current=false] True for current rotation, false for target.
+     * @returns {Number} the rotation of this tiled image in degrees.
      */
-    getRotation: function() {
-        return this._degrees;
+    getRotation: function(current) {
+        return current ?
+            this._degreesSpring.current.value :
+            this._degreesSpring.target.value;
     },
 
     /**
      * Set the current rotation of this tiled image in degrees.
      * @param {Number} degrees the rotation in degrees.
+     * @param {Boolean} [immediately=false] Whether to animate to the new angle
+     * or rotate immediately.
      * @fires OpenSeadragon.TiledImage.event:bounds-change
      */
-    setRotation: function(degrees) {
+    setRotation: function(degrees, immediately) {
         degrees = $.positiveModulo(degrees, 360);
-        if (this._degrees === degrees) {
+        if (this._degreesSpring.target.value === degrees &&
+            this._degreesSpring.isAtTargetValue()) {
             return;
         }
-        this._degrees = degrees;
+        if (immediately) {
+            this._degreesSpring.resetTo(degrees);
+        } else {
+            this._degreesSpring.springTo(degrees);
+        }
         this._needsDraw = true;
         this._raiseBoundsChange();
     },
@@ -1705,7 +1717,7 @@ function drawTiles( tiledImage, lastDrawn ) {
     if (lastDrawn.length > 1 &&
         imageZoom > tiledImage.smoothTileEdgesMinZoom &&
         !tiledImage.iOSDevice &&
-        tiledImage.getRotation() === 0 &&
+        tiledImage.getRotation(true) === 0 &&
         $.supportsCanvas) {
         // When zoomed in a lot (>100%) the tile edges are visible.
         // So we have to composite them at ~100% and scale them up together.
@@ -1739,9 +1751,9 @@ function drawTiles( tiledImage, lastDrawn ) {
                 useSketch: useSketch
             });
         }
-        if (tiledImage._degrees !== 0) {
+        if (tiledImage.getRotation(true) !== 0) {
             tiledImage._drawer._offsetForRotation({
-                degrees: tiledImage._degrees,
+                degrees: tiledImage.getRotation(true),
                 point: tiledImage.viewport.pixelFromPointNoRotate(
                     tiledImage._getRotationPoint(true), true),
                 useSketch: useSketch
@@ -1754,7 +1766,7 @@ function drawTiles( tiledImage, lastDrawn ) {
         tiledImage._drawer.saveContext(useSketch);
 
         var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true);
-        box = box.rotate(-tiledImage._degrees, tiledImage._getRotationPoint());
+        box = box.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true));
         var clipRect = tiledImage._drawer.viewportToDrawerRectangle(box);
         if (sketchScale) {
             clipRect = clipRect.times(sketchScale);
@@ -1816,7 +1828,7 @@ function drawTiles( tiledImage, lastDrawn ) {
     }
 
     if (!sketchScale) {
-        if (tiledImage._degrees !== 0) {
+        if (tiledImage.getRotation(true) !== 0) {
             tiledImage._drawer._restoreRotationChanges(useSketch);
         }
         if (tiledImage.viewport.degrees !== 0) {
@@ -1832,9 +1844,9 @@ function drawTiles( tiledImage, lastDrawn ) {
                     useSketch: false
                 });
             }
-            if (tiledImage._degrees !== 0) {
+            if (tiledImage.getRotation(true) !== 0) {
                 tiledImage._drawer._offsetForRotation({
-                    degrees: tiledImage._degrees,
+                    degrees: tiledImage.getRotation(true),
                     point: tiledImage.viewport.pixelFromPointNoRotate(
                         tiledImage._getRotationPoint(true), true),
                     useSketch: false
@@ -1849,7 +1861,7 @@ function drawTiles( tiledImage, lastDrawn ) {
             bounds: bounds
         });
         if (sketchScale) {
-            if (tiledImage._degrees !== 0) {
+            if (tiledImage.getRotation(true) !== 0) {
                 tiledImage._drawer._restoreRotationChanges(false);
             }
             if (tiledImage.viewport.degrees !== 0) {

From b62d4a7bc15d0b01b076fac902209368ed487457 Mon Sep 17 00:00:00 2001
From: Antoine Vandecreme <ant.vand@gmail.com>
Date: Sun, 8 Jan 2017 18:36:37 +0100
Subject: [PATCH 2/3] Fix navigator with rotation and clip

---
 src/navigator.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/navigator.js b/src/navigator.js
index 61d50734..45379fa1 100644
--- a/src/navigator.js
+++ b/src/navigator.js
@@ -369,9 +369,11 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /*
 
     // private
     _matchBounds: function(myItem, theirItem, immediately) {
-        var bounds = theirItem.getBounds();
+        var bounds = theirItem.getBoundsNoRotate();
         myItem.setPosition(bounds.getTopLeft(), immediately);
         myItem.setWidth(bounds.width, immediately);
+        myItem.setRotation(theirItem.getRotation(), immediately);
+        myItem.setClip(theirItem.getClip());
     }
 });
 

From 63a8a2ffa6232a1c8a8e5495dc63b5d90198916b Mon Sep 17 00:00:00 2001
From: Antoine Vandecreme <ant.vand@gmail.com>
Date: Sat, 21 Jan 2017 19:58:21 +0100
Subject: [PATCH 3/3] Allow tiled image rotation outside the  0 to 360 range

---
 src/drawer.js              |  4 ++--
 src/tiledimage.js          | 15 +++++++--------
 test/modules/tiledimage.js | 13 ++++++++++---
 3 files changed, 19 insertions(+), 13 deletions(-)

diff --git a/src/drawer.js b/src/drawer.js
index 0289019a..4485da98 100644
--- a/src/drawer.js
+++ b/src/drawer.js
@@ -500,7 +500,7 @@ $.Drawer.prototype = {
         if ( this.viewport.degrees !== 0 ) {
             this._offsetForRotation({degrees: this.viewport.degrees});
         }
-        if (tiledImage.getRotation(true) !== 0) {
+        if (tiledImage.getRotation(true) % 360 !== 0) {
             this._offsetForRotation({
                 degrees: tiledImage.getRotation(true),
                 point: tiledImage.viewport.pixelFromPointNoRotate(
@@ -569,7 +569,7 @@ $.Drawer.prototype = {
         if ( this.viewport.degrees !== 0 ) {
             this._restoreRotationChanges();
         }
-        if (tiledImage.getRotation(true) !== 0) {
+        if (tiledImage.getRotation(true) % 360 !== 0) {
             this._restoreRotationChanges();
         }
         context.restore();
diff --git a/src/tiledimage.js b/src/tiledimage.js
index c6cdd7ec..70859ec0 100644
--- a/src/tiledimage.js
+++ b/src/tiledimage.js
@@ -132,7 +132,7 @@ $.TiledImage = function( options ) {
     var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER;
     delete options.fitBoundsPlacement;
 
-    var degrees = $.positiveModulo(options.degrees || 0, 360);
+    var degrees = options.degrees || 0;
     delete options.degrees;
 
     $.extend( true, this, {
@@ -789,7 +789,6 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
      * @fires OpenSeadragon.TiledImage.event:bounds-change
      */
     setRotation: function(degrees, immediately) {
-        degrees = $.positiveModulo(degrees, 360);
         if (this._degreesSpring.target.value === degrees &&
             this._degreesSpring.isAtTargetValue()) {
             return;
@@ -1713,11 +1712,11 @@ function drawTiles( tiledImage, lastDrawn ) {
 
     var zoom = tiledImage.viewport.getZoom(true);
     var imageZoom = tiledImage.viewportToImageZoom(zoom);
-    // TODO: support tile edge smoothing with tiled image rotation.
+
     if (lastDrawn.length > 1 &&
         imageZoom > tiledImage.smoothTileEdgesMinZoom &&
         !tiledImage.iOSDevice &&
-        tiledImage.getRotation(true) === 0 &&
+        tiledImage.getRotation(true) % 360 === 0 && // TODO: support tile edge smoothing with tiled image rotation.
         $.supportsCanvas) {
         // When zoomed in a lot (>100%) the tile edges are visible.
         // So we have to composite them at ~100% and scale them up together.
@@ -1751,7 +1750,7 @@ function drawTiles( tiledImage, lastDrawn ) {
                 useSketch: useSketch
             });
         }
-        if (tiledImage.getRotation(true) !== 0) {
+        if (tiledImage.getRotation(true) % 360 !== 0) {
             tiledImage._drawer._offsetForRotation({
                 degrees: tiledImage.getRotation(true),
                 point: tiledImage.viewport.pixelFromPointNoRotate(
@@ -1828,7 +1827,7 @@ function drawTiles( tiledImage, lastDrawn ) {
     }
 
     if (!sketchScale) {
-        if (tiledImage.getRotation(true) !== 0) {
+        if (tiledImage.getRotation(true) % 360 !== 0) {
             tiledImage._drawer._restoreRotationChanges(useSketch);
         }
         if (tiledImage.viewport.degrees !== 0) {
@@ -1844,7 +1843,7 @@ function drawTiles( tiledImage, lastDrawn ) {
                     useSketch: false
                 });
             }
-            if (tiledImage.getRotation(true) !== 0) {
+            if (tiledImage.getRotation(true) % 360 !== 0) {
                 tiledImage._drawer._offsetForRotation({
                     degrees: tiledImage.getRotation(true),
                     point: tiledImage.viewport.pixelFromPointNoRotate(
@@ -1861,7 +1860,7 @@ function drawTiles( tiledImage, lastDrawn ) {
             bounds: bounds
         });
         if (sketchScale) {
-            if (tiledImage.getRotation(true) !== 0) {
+            if (tiledImage.getRotation(true) % 360 !== 0) {
                 tiledImage._drawer._restoreRotationChanges(false);
             }
             if (tiledImage.viewport.degrees !== 0) {
diff --git a/test/modules/tiledimage.js b/test/modules/tiledimage.js
index 1b05b557..b2206341 100644
--- a/test/modules/tiledimage.js
+++ b/test/modules/tiledimage.js
@@ -319,10 +319,16 @@
 
         function testDefaultRotation() {
             var image = viewer.world.getItemAt(0);
-            strictEqual(image.getRotation(), 0, 'image has default rotation');
+            strictEqual(image.getRotation(true), 0, 'image has default current rotation');
+            strictEqual(image.getRotation(false), 0, 'image has default target rotation');
 
             image.setRotation(400);
-            strictEqual(image.getRotation(), 40, 'rotation is set correctly');
+            strictEqual(image.getRotation(true), 0, 'current rotation is not changed');
+            strictEqual(image.getRotation(false), 400, 'target rotation is set correctly');
+
+            image.setRotation(200, true);
+            strictEqual(image.getRotation(true), 200, 'current rotation is set correctly');
+            strictEqual(image.getRotation(false), 200, 'target rotation is set correctly');
 
             viewer.addOnceHandler('open', testTileSourceRotation);
             viewer.open({
@@ -333,7 +339,8 @@
 
         function testTileSourceRotation() {
             var image = viewer.world.getItemAt(0);
-            strictEqual(image.getRotation(), 300, 'image has correct rotation');
+            strictEqual(image.getRotation(true), -60, 'image has correct current rotation');
+            strictEqual(image.getRotation(false), -60, 'image has correct target rotation');
             start();
         }