diff --git a/changelog.txt b/changelog.txt
index e416df23..5ff344fb 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -56,6 +56,7 @@ OPENSEADRAGON CHANGELOG
* Fixed an issue with zero size viewers in IE8 (#609)
* Fixed: Cross Origin policy not working (#613)
* Optimized tile loading by clearing the queue on a re-draw when imageLoaderLimit is set (#616)
+* Now animating zoom spring exponentially (#631)
1.2.1:
diff --git a/src/spring.js b/src/spring.js
index 3320ecc2..2c89ee07 100644
--- a/src/spring.js
+++ b/src/spring.js
@@ -38,10 +38,14 @@
* @class Spring
* @memberof OpenSeadragon
* @param {Object} options - Spring configuration settings.
- * @param {Number} options.initial - Initial value of spring, default to 0 so
- * spring is not in motion initally by default.
- * @param {Number} options.springStiffness - Spring stiffness.
- * @param {Number} options.animationTime - Animation duration per spring.
+ * @param {Number} options.springStiffness - Spring stiffness. Must be greater than zero.
+ * The closer to zero, the closer to linear animation.
+ * @param {Number} options.animationTime - Animation duration per spring, in seconds.
+ * Must be greater than zero.
+ * @param {Number} [options.initial=0] - Initial value of spring.
+ * @param {Boolean} [options.exponential=false] - Whether this spring represents
+ * an exponential scale (such as zoom) and should be animated accordingly. Note that
+ * exponential springs must have non-zero values.
*/
$.Spring = function( options ) {
var args = arguments;
@@ -52,7 +56,7 @@ $.Spring = function( options ) {
options = {
initial: args.length && typeof ( args[ 0 ] ) == "number" ?
args[ 0 ] :
- 0,
+ undefined,
/**
* Spring stiffness.
* @member {Number} springStiffness
@@ -72,6 +76,17 @@ $.Spring = function( options ) {
};
}
+ $.console.assert(typeof options.springStiffness === "number" && options.springStiffness !== 0,
+ "[OpenSeadragon.Spring] options.springStiffness must be a non-zero number");
+
+ $.console.assert(typeof options.animationTime === "number" && options.springStiffness !== 0,
+ "[OpenSeadragon.Spring] options.animationTime must be a non-zero number");
+
+ if (options.exponential) {
+ this._exponential = true;
+ delete options.exponential;
+ }
+
$.extend( true, this, options);
/**
@@ -83,10 +98,13 @@ $.Spring = function( options ) {
this.current = {
value: typeof ( this.initial ) == "number" ?
this.initial :
- 0,
+ (this._exponential ? 0 : 1),
time: $.now() // always work in milliseconds
};
+ $.console.assert(!this._exponential || this.current.value !== 0,
+ "[OpenSeadragon.Spring] value must be non-zero for exponential springs");
+
/**
* @member {Object} start
* @memberof OpenSeadragon.Spring#
@@ -108,6 +126,12 @@ $.Spring = function( options ) {
value: this.current.value,
time: this.current.time
};
+
+ if (this._exponential) {
+ this.start._logValue = Math.log(this.start.value);
+ this.target._logValue = Math.log(this.target.value);
+ this.current._logValue = Math.log(this.current.value);
+ }
};
$.Spring.prototype = /** @lends OpenSeadragon.Spring.prototype */{
@@ -117,8 +141,17 @@ $.Spring.prototype = /** @lends OpenSeadragon.Spring.prototype */{
* @param {Number} target
*/
resetTo: function( target ) {
+ $.console.assert(!this._exponential || target !== 0,
+ "[OpenSeadragon.Spring.resetTo] target must be non-zero for exponential springs");
+
this.start.value = this.target.value = this.current.value = target;
this.start.time = this.target.time = this.current.time = $.now();
+
+ if (this._exponential) {
+ this.start._logValue = Math.log(this.start.value);
+ this.target._logValue = Math.log(this.target.value);
+ this.current._logValue = Math.log(this.current.value);
+ }
},
/**
@@ -126,10 +159,18 @@ $.Spring.prototype = /** @lends OpenSeadragon.Spring.prototype */{
* @param {Number} target
*/
springTo: function( target ) {
+ $.console.assert(!this._exponential || target !== 0,
+ "[OpenSeadragon.Spring.springTo] target must be non-zero for exponential springs");
+
this.start.value = this.current.value;
this.start.time = this.current.time;
this.target.value = target;
this.target.time = this.start.time + 1000 * this.animationTime;
+
+ if (this._exponential) {
+ this.start._logValue = Math.log(this.start.value);
+ this.target._logValue = Math.log(this.target.value);
+ }
},
/**
@@ -139,6 +180,27 @@ $.Spring.prototype = /** @lends OpenSeadragon.Spring.prototype */{
shiftBy: function( delta ) {
this.start.value += delta;
this.target.value += delta;
+
+ if (this._exponential) {
+ $.console.assert(this.target.value !== 0 && this.start.value !== 0,
+ "[OpenSeadragon.Spring.shiftBy] spring value must be non-zero for exponential springs");
+
+ this.start._logValue = Math.log(this.start.value);
+ this.target._logValue = Math.log(this.target.value);
+ }
+ },
+
+ setExponential: function(value) {
+ this._exponential = value;
+
+ if (this._exponential) {
+ $.console.assert(this.current.value !== 0 && this.target.value !== 0 && this.start.value !== 0,
+ "[OpenSeadragon.Spring.setExponential] spring value must be non-zero for exponential springs");
+
+ this.start._logValue = Math.log(this.start.value);
+ this.target._logValue = Math.log(this.target.value);
+ this.current._logValue = Math.log(this.current.value);
+ }
},
/**
@@ -146,15 +208,31 @@ $.Spring.prototype = /** @lends OpenSeadragon.Spring.prototype */{
*/
update: function() {
this.current.time = $.now();
- this.current.value = (this.current.time >= this.target.time) ?
- this.target.value :
- this.start.value +
- ( this.target.value - this.start.value ) *
+
+ var startValue, targetValue;
+ if (this._exponential) {
+ startValue = this.start._logValue;
+ targetValue = this.target._logValue;
+ } else {
+ startValue = this.start.value;
+ targetValue = this.target.value;
+ }
+
+ var currentValue = (this.current.time >= this.target.time) ?
+ targetValue :
+ startValue +
+ ( targetValue - startValue ) *
transform(
this.springStiffness,
( this.current.time - this.start.time ) /
( this.target.time - this.start.time )
);
+
+ if (this._exponential) {
+ this.current.value = Math.exp(currentValue);
+ } else {
+ this.current.value = currentValue;
+ }
}
};
diff --git a/src/viewport.js b/src/viewport.js
index ff91ad29..b882580e 100644
--- a/src/viewport.js
+++ b/src/viewport.js
@@ -127,6 +127,7 @@ $.Viewport = function( options ) {
animationTime: this.animationTime
});
this.zoomSpring = new $.Spring({
+ exponential: true,
initial: 1,
springStiffness: this.springStiffness,
animationTime: this.animationTime
diff --git a/test/coverage.html b/test/coverage.html
index fe75c797..9d61c48f 100644
--- a/test/coverage.html
+++ b/test/coverage.html
@@ -72,6 +72,7 @@
+
diff --git a/test/modules/spring.js b/test/modules/spring.js
new file mode 100644
index 00000000..b627f832
--- /dev/null
+++ b/test/modules/spring.js
@@ -0,0 +1,77 @@
+/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog, propEqual, console */
+
+(function () {
+
+ var originalNow;
+ var now;
+
+ module("spring", {
+ setup: function () {
+ now = 0;
+ originalNow = OpenSeadragon.now;
+
+ OpenSeadragon.now = function() {
+ return now;
+ };
+ },
+ teardown: function () {
+ OpenSeadragon.now = originalNow;
+ }
+ });
+
+ asyncTest('regular spring', function() {
+ var spring = new OpenSeadragon.Spring({
+ initial: 5,
+ animationTime: 1,
+ springStiffness: 0.000001
+ });
+
+ equal(spring.current.value, 5, 'initial current value');
+ equal(spring.target.value, 5, 'initial target value');
+
+ spring.springTo(6);
+ equal(spring.current.value, 5, 'current value after springTo');
+ equal(spring.target.value, 6, 'target value after springTo');
+
+ now = 500;
+ spring.update();
+ Util.assessNumericValue(5.5, spring.current.value, 0.00001, 'current value after first update');
+ equal(spring.target.value, 6, 'target value after first update');
+
+ now = 1000;
+ spring.update();
+ equal(spring.current.value, 6, 'current value after second update');
+ equal(spring.target.value, 6, 'target value after second update');
+
+ start();
+ });
+
+ asyncTest('exponential spring', function() {
+ var spring = new OpenSeadragon.Spring({
+ exponential: true,
+ initial: 1,
+ animationTime: 1,
+ springStiffness: 0.000001
+ });
+
+ equal(spring.current.value, 1, 'initial current value');
+ equal(spring.target.value, 1, 'initial target value');
+
+ spring.springTo(2);
+ equal(spring.current.value, 1, 'current value after springTo');
+ equal(spring.target.value, 2, 'target value after springTo');
+
+ now = 500;
+ spring.update();
+ Util.assessNumericValue(1.41421, spring.current.value, 0.00001, 'current value after first update');
+ equal(spring.target.value, 2, 'target value after first update');
+
+ now = 1000;
+ spring.update();
+ equal(spring.current.value, 2, 'current value after second update');
+ equal(spring.target.value, 2, 'target value after second update');
+
+ start();
+ });
+
+})();
diff --git a/test/test.html b/test/test.html
index f1f1716c..ae4863e8 100644
--- a/test/test.html
+++ b/test/test.html
@@ -37,6 +37,7 @@
+