diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ed126ad3..bae9361f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -19,5 +19,5 @@ jobs:
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- - run: npm install
+ - run: npm clean-install
- run: npm test
diff --git a/README.md b/README.md
index 48ef9633..9211e396 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ JavaScript (TypeScript) tweening engine for easy animations, incorporating optim
More languages: [English](./README.md), [简体中文](./README_zh-CN.md)
----
+# Example
```html
@@ -48,7 +48,10 @@ More languages: [English](./README.md), [简体中文](./README_zh-CN.md)
```
-[Try this example on CodePen](https://codepen.io/trusktr/pen/KKGaBVz?editors=1000)
+[Try the above example on CodePen](https://codepen.io/trusktr/pen/KKGaBVz?editors=1000)
+
+Animate numbers in any JavaScript object. For example, [rotate a 3D box made
+with Three.js](https://codepen.io/trusktr/pen/ExJqvgZ):
# Installation
diff --git a/dist/tween.amd.js b/dist/tween.amd.js
index 4b43ab07..7697fba2 100644
--- a/dist/tween.amd.js
+++ b/dist/tween.amd.js
@@ -678,13 +678,11 @@ define(['exports'], (function (exports) { 'use strict';
* it is still playing, just paused).
*/
Tween.prototype.update = function (time, autoStart) {
- var _this = this;
var _a;
if (time === void 0) { time = now(); }
if (autoStart === void 0) { autoStart = true; }
if (this._isPaused)
return true;
- var property;
var endTime = this._startTime + this._duration;
if (!this._goToEnd && !this._isPlaying) {
if (time > endTime)
@@ -711,72 +709,85 @@ define(['exports'], (function (exports) { 'use strict';
var elapsedTime = time - this._startTime;
var durationAndDelay = this._duration + ((_a = this._repeatDelayTime) !== null && _a !== void 0 ? _a : this._delayTime);
var totalTime = this._duration + this._repeat * durationAndDelay;
- var calculateElapsedPortion = function () {
- if (_this._duration === 0)
- return 1;
- if (elapsedTime > totalTime) {
- return 1;
- }
- var timesRepeated = Math.trunc(elapsedTime / durationAndDelay);
- var timeIntoCurrentRepeat = elapsedTime - timesRepeated * durationAndDelay;
- // TODO use %?
- // const timeIntoCurrentRepeat = elapsedTime % durationAndDelay
- var portion = Math.min(timeIntoCurrentRepeat / _this._duration, 1);
- if (portion === 0 && elapsedTime === _this._duration) {
- return 1;
- }
- return portion;
- };
- var elapsed = calculateElapsedPortion();
+ var elapsed = this._calculateElapsedPortion(elapsedTime, durationAndDelay, totalTime);
var value = this._easingFunction(elapsed);
- // properties transformations
+ var status = this._calculateCompletionStatus(elapsedTime, durationAndDelay);
+ if (status === 'repeat') {
+ // the current update is happening after the instant the tween repeated
+ this._processRepetition(elapsedTime, durationAndDelay);
+ }
this._updateProperties(this._object, this._valuesStart, this._valuesEnd, value);
+ if (status === 'about-to-repeat') {
+ // the current update is happening at the exact instant the tween is going to repeat
+ // the values should match the end of the tween, not the beginning,
+ // that's why _processRepetition happens after _updateProperties
+ this._processRepetition(elapsedTime, durationAndDelay);
+ }
if (this._onUpdateCallback) {
this._onUpdateCallback(this._object, elapsed);
}
- if (this._duration === 0 || elapsedTime >= this._duration) {
- if (this._repeat > 0) {
- var completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat);
- if (isFinite(this._repeat)) {
- this._repeat -= completeCount;
- }
- // Reassign starting values, restart by making startTime = now
- for (property in this._valuesStartRepeat) {
- if (!this._yoyo && typeof this._valuesEnd[property] === 'string') {
- this._valuesStartRepeat[property] =
- // eslint-disable-next-line
- // @ts-ignore FIXME?
- this._valuesStartRepeat[property] + parseFloat(this._valuesEnd[property]);
- }
- if (this._yoyo) {
- this._swapEndStartRepeatValues(property);
- }
- this._valuesStart[property] = this._valuesStartRepeat[property];
- }
- if (this._yoyo) {
- this._reversed = !this._reversed;
- }
- this._startTime += durationAndDelay * completeCount;
- if (this._onRepeatCallback) {
- this._onRepeatCallback(this._object);
- }
- this._onEveryStartCallbackFired = false;
- return true;
+ if (status === 'repeat' || status === 'about-to-repeat') {
+ if (this._onRepeatCallback) {
+ this._onRepeatCallback(this._object);
}
- else {
- if (this._onCompleteCallback) {
- this._onCompleteCallback(this._object);
- }
- for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
- // Make the chained tweens start exactly at the time they should,
- // even if the `update()` method was called way past the duration of the tween
- this._chainedTweens[i].start(this._startTime + this._duration, false);
- }
- this._isPlaying = false;
- return false;
+ this._onEveryStartCallbackFired = false;
+ }
+ else if (status === 'completed') {
+ this._isPlaying = false;
+ if (this._onCompleteCallback) {
+ this._onCompleteCallback(this._object);
+ }
+ for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
+ // Make the chained tweens start exactly at the time they should,
+ // even if the `update()` method was called way past the duration of the tween
+ this._chainedTweens[i].start(this._startTime + this._duration, false);
}
}
- return true;
+ return status !== 'completed';
+ };
+ Tween.prototype._calculateElapsedPortion = function (elapsedTime, durationAndDelay, totalTime) {
+ if (this._duration === 0 || elapsedTime > totalTime) {
+ return 1;
+ }
+ var timeIntoCurrentRepeat = elapsedTime % durationAndDelay;
+ var portion = Math.min(timeIntoCurrentRepeat / this._duration, 1);
+ if (portion === 0 && elapsedTime !== 0 && elapsedTime % this._duration === 0) {
+ return 1;
+ }
+ return portion;
+ };
+ Tween.prototype._calculateCompletionStatus = function (elapsedTime, durationAndDelay) {
+ if (this._duration !== 0 && elapsedTime < this._duration) {
+ return 'playing';
+ }
+ if (this._repeat <= 0) {
+ return 'completed';
+ }
+ if (elapsedTime === this._duration) {
+ return 'about-to-repeat';
+ }
+ return 'repeat';
+ };
+ Tween.prototype._processRepetition = function (elapsedTime, durationAndDelay) {
+ var completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat);
+ if (isFinite(this._repeat)) {
+ this._repeat -= completeCount;
+ }
+ // Reassign starting values, restart by making startTime = now
+ for (var property in this._valuesStartRepeat) {
+ var valueEnd = this._valuesEnd[property];
+ if (!this._yoyo && typeof valueEnd === 'string') {
+ this._valuesStartRepeat[property] = this._valuesStartRepeat[property] + parseFloat(valueEnd);
+ }
+ if (this._yoyo) {
+ this._swapEndStartRepeatValues(property);
+ }
+ this._valuesStart[property] = this._valuesStartRepeat[property];
+ }
+ if (this._yoyo) {
+ this._reversed = !this._reversed;
+ }
+ this._startTime += durationAndDelay * completeCount;
};
Tween.prototype._updateProperties = function (_object, _valuesStart, _valuesEnd, value) {
for (var property in _valuesEnd) {
diff --git a/dist/tween.cjs b/dist/tween.cjs
index 1470fcbc..86c8ee1e 100644
--- a/dist/tween.cjs
+++ b/dist/tween.cjs
@@ -680,13 +680,11 @@ var Tween = /** @class */ (function () {
* it is still playing, just paused).
*/
Tween.prototype.update = function (time, autoStart) {
- var _this = this;
var _a;
if (time === void 0) { time = now(); }
if (autoStart === void 0) { autoStart = true; }
if (this._isPaused)
return true;
- var property;
var endTime = this._startTime + this._duration;
if (!this._goToEnd && !this._isPlaying) {
if (time > endTime)
@@ -713,72 +711,85 @@ var Tween = /** @class */ (function () {
var elapsedTime = time - this._startTime;
var durationAndDelay = this._duration + ((_a = this._repeatDelayTime) !== null && _a !== void 0 ? _a : this._delayTime);
var totalTime = this._duration + this._repeat * durationAndDelay;
- var calculateElapsedPortion = function () {
- if (_this._duration === 0)
- return 1;
- if (elapsedTime > totalTime) {
- return 1;
- }
- var timesRepeated = Math.trunc(elapsedTime / durationAndDelay);
- var timeIntoCurrentRepeat = elapsedTime - timesRepeated * durationAndDelay;
- // TODO use %?
- // const timeIntoCurrentRepeat = elapsedTime % durationAndDelay
- var portion = Math.min(timeIntoCurrentRepeat / _this._duration, 1);
- if (portion === 0 && elapsedTime === _this._duration) {
- return 1;
- }
- return portion;
- };
- var elapsed = calculateElapsedPortion();
+ var elapsed = this._calculateElapsedPortion(elapsedTime, durationAndDelay, totalTime);
var value = this._easingFunction(elapsed);
- // properties transformations
+ var status = this._calculateCompletionStatus(elapsedTime, durationAndDelay);
+ if (status === 'repeat') {
+ // the current update is happening after the instant the tween repeated
+ this._processRepetition(elapsedTime, durationAndDelay);
+ }
this._updateProperties(this._object, this._valuesStart, this._valuesEnd, value);
+ if (status === 'about-to-repeat') {
+ // the current update is happening at the exact instant the tween is going to repeat
+ // the values should match the end of the tween, not the beginning,
+ // that's why _processRepetition happens after _updateProperties
+ this._processRepetition(elapsedTime, durationAndDelay);
+ }
if (this._onUpdateCallback) {
this._onUpdateCallback(this._object, elapsed);
}
- if (this._duration === 0 || elapsedTime >= this._duration) {
- if (this._repeat > 0) {
- var completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat);
- if (isFinite(this._repeat)) {
- this._repeat -= completeCount;
- }
- // Reassign starting values, restart by making startTime = now
- for (property in this._valuesStartRepeat) {
- if (!this._yoyo && typeof this._valuesEnd[property] === 'string') {
- this._valuesStartRepeat[property] =
- // eslint-disable-next-line
- // @ts-ignore FIXME?
- this._valuesStartRepeat[property] + parseFloat(this._valuesEnd[property]);
- }
- if (this._yoyo) {
- this._swapEndStartRepeatValues(property);
- }
- this._valuesStart[property] = this._valuesStartRepeat[property];
- }
- if (this._yoyo) {
- this._reversed = !this._reversed;
- }
- this._startTime += durationAndDelay * completeCount;
- if (this._onRepeatCallback) {
- this._onRepeatCallback(this._object);
- }
- this._onEveryStartCallbackFired = false;
- return true;
+ if (status === 'repeat' || status === 'about-to-repeat') {
+ if (this._onRepeatCallback) {
+ this._onRepeatCallback(this._object);
}
- else {
- if (this._onCompleteCallback) {
- this._onCompleteCallback(this._object);
- }
- for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
- // Make the chained tweens start exactly at the time they should,
- // even if the `update()` method was called way past the duration of the tween
- this._chainedTweens[i].start(this._startTime + this._duration, false);
- }
- this._isPlaying = false;
- return false;
+ this._onEveryStartCallbackFired = false;
+ }
+ else if (status === 'completed') {
+ this._isPlaying = false;
+ if (this._onCompleteCallback) {
+ this._onCompleteCallback(this._object);
+ }
+ for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
+ // Make the chained tweens start exactly at the time they should,
+ // even if the `update()` method was called way past the duration of the tween
+ this._chainedTweens[i].start(this._startTime + this._duration, false);
}
}
- return true;
+ return status !== 'completed';
+ };
+ Tween.prototype._calculateElapsedPortion = function (elapsedTime, durationAndDelay, totalTime) {
+ if (this._duration === 0 || elapsedTime > totalTime) {
+ return 1;
+ }
+ var timeIntoCurrentRepeat = elapsedTime % durationAndDelay;
+ var portion = Math.min(timeIntoCurrentRepeat / this._duration, 1);
+ if (portion === 0 && elapsedTime !== 0 && elapsedTime % this._duration === 0) {
+ return 1;
+ }
+ return portion;
+ };
+ Tween.prototype._calculateCompletionStatus = function (elapsedTime, durationAndDelay) {
+ if (this._duration !== 0 && elapsedTime < this._duration) {
+ return 'playing';
+ }
+ if (this._repeat <= 0) {
+ return 'completed';
+ }
+ if (elapsedTime === this._duration) {
+ return 'about-to-repeat';
+ }
+ return 'repeat';
+ };
+ Tween.prototype._processRepetition = function (elapsedTime, durationAndDelay) {
+ var completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat);
+ if (isFinite(this._repeat)) {
+ this._repeat -= completeCount;
+ }
+ // Reassign starting values, restart by making startTime = now
+ for (var property in this._valuesStartRepeat) {
+ var valueEnd = this._valuesEnd[property];
+ if (!this._yoyo && typeof valueEnd === 'string') {
+ this._valuesStartRepeat[property] = this._valuesStartRepeat[property] + parseFloat(valueEnd);
+ }
+ if (this._yoyo) {
+ this._swapEndStartRepeatValues(property);
+ }
+ this._valuesStart[property] = this._valuesStartRepeat[property];
+ }
+ if (this._yoyo) {
+ this._reversed = !this._reversed;
+ }
+ this._startTime += durationAndDelay * completeCount;
};
Tween.prototype._updateProperties = function (_object, _valuesStart, _valuesEnd, value) {
for (var property in _valuesEnd) {
diff --git a/dist/tween.d.ts b/dist/tween.d.ts
index 19e9095d..7fdeba02 100644
--- a/dist/tween.d.ts
+++ b/dist/tween.d.ts
@@ -137,6 +137,9 @@ declare class Tween {
* it is still playing, just paused).
*/
update(time?: number, autoStart?: boolean): boolean;
+ private _calculateElapsedPortion;
+ private _calculateCompletionStatus;
+ private _processRepetition;
private _updateProperties;
private _handleRelativeValue;
private _swapEndStartRepeatValues;
diff --git a/dist/tween.esm.js b/dist/tween.esm.js
index ba359a49..86a97403 100644
--- a/dist/tween.esm.js
+++ b/dist/tween.esm.js
@@ -676,13 +676,11 @@ var Tween = /** @class */ (function () {
* it is still playing, just paused).
*/
Tween.prototype.update = function (time, autoStart) {
- var _this = this;
var _a;
if (time === void 0) { time = now(); }
if (autoStart === void 0) { autoStart = true; }
if (this._isPaused)
return true;
- var property;
var endTime = this._startTime + this._duration;
if (!this._goToEnd && !this._isPlaying) {
if (time > endTime)
@@ -709,72 +707,85 @@ var Tween = /** @class */ (function () {
var elapsedTime = time - this._startTime;
var durationAndDelay = this._duration + ((_a = this._repeatDelayTime) !== null && _a !== void 0 ? _a : this._delayTime);
var totalTime = this._duration + this._repeat * durationAndDelay;
- var calculateElapsedPortion = function () {
- if (_this._duration === 0)
- return 1;
- if (elapsedTime > totalTime) {
- return 1;
- }
- var timesRepeated = Math.trunc(elapsedTime / durationAndDelay);
- var timeIntoCurrentRepeat = elapsedTime - timesRepeated * durationAndDelay;
- // TODO use %?
- // const timeIntoCurrentRepeat = elapsedTime % durationAndDelay
- var portion = Math.min(timeIntoCurrentRepeat / _this._duration, 1);
- if (portion === 0 && elapsedTime === _this._duration) {
- return 1;
- }
- return portion;
- };
- var elapsed = calculateElapsedPortion();
+ var elapsed = this._calculateElapsedPortion(elapsedTime, durationAndDelay, totalTime);
var value = this._easingFunction(elapsed);
- // properties transformations
+ var status = this._calculateCompletionStatus(elapsedTime, durationAndDelay);
+ if (status === 'repeat') {
+ // the current update is happening after the instant the tween repeated
+ this._processRepetition(elapsedTime, durationAndDelay);
+ }
this._updateProperties(this._object, this._valuesStart, this._valuesEnd, value);
+ if (status === 'about-to-repeat') {
+ // the current update is happening at the exact instant the tween is going to repeat
+ // the values should match the end of the tween, not the beginning,
+ // that's why _processRepetition happens after _updateProperties
+ this._processRepetition(elapsedTime, durationAndDelay);
+ }
if (this._onUpdateCallback) {
this._onUpdateCallback(this._object, elapsed);
}
- if (this._duration === 0 || elapsedTime >= this._duration) {
- if (this._repeat > 0) {
- var completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat);
- if (isFinite(this._repeat)) {
- this._repeat -= completeCount;
- }
- // Reassign starting values, restart by making startTime = now
- for (property in this._valuesStartRepeat) {
- if (!this._yoyo && typeof this._valuesEnd[property] === 'string') {
- this._valuesStartRepeat[property] =
- // eslint-disable-next-line
- // @ts-ignore FIXME?
- this._valuesStartRepeat[property] + parseFloat(this._valuesEnd[property]);
- }
- if (this._yoyo) {
- this._swapEndStartRepeatValues(property);
- }
- this._valuesStart[property] = this._valuesStartRepeat[property];
- }
- if (this._yoyo) {
- this._reversed = !this._reversed;
- }
- this._startTime += durationAndDelay * completeCount;
- if (this._onRepeatCallback) {
- this._onRepeatCallback(this._object);
- }
- this._onEveryStartCallbackFired = false;
- return true;
+ if (status === 'repeat' || status === 'about-to-repeat') {
+ if (this._onRepeatCallback) {
+ this._onRepeatCallback(this._object);
}
- else {
- if (this._onCompleteCallback) {
- this._onCompleteCallback(this._object);
- }
- for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
- // Make the chained tweens start exactly at the time they should,
- // even if the `update()` method was called way past the duration of the tween
- this._chainedTweens[i].start(this._startTime + this._duration, false);
- }
- this._isPlaying = false;
- return false;
+ this._onEveryStartCallbackFired = false;
+ }
+ else if (status === 'completed') {
+ this._isPlaying = false;
+ if (this._onCompleteCallback) {
+ this._onCompleteCallback(this._object);
+ }
+ for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
+ // Make the chained tweens start exactly at the time they should,
+ // even if the `update()` method was called way past the duration of the tween
+ this._chainedTweens[i].start(this._startTime + this._duration, false);
}
}
- return true;
+ return status !== 'completed';
+ };
+ Tween.prototype._calculateElapsedPortion = function (elapsedTime, durationAndDelay, totalTime) {
+ if (this._duration === 0 || elapsedTime > totalTime) {
+ return 1;
+ }
+ var timeIntoCurrentRepeat = elapsedTime % durationAndDelay;
+ var portion = Math.min(timeIntoCurrentRepeat / this._duration, 1);
+ if (portion === 0 && elapsedTime !== 0 && elapsedTime % this._duration === 0) {
+ return 1;
+ }
+ return portion;
+ };
+ Tween.prototype._calculateCompletionStatus = function (elapsedTime, durationAndDelay) {
+ if (this._duration !== 0 && elapsedTime < this._duration) {
+ return 'playing';
+ }
+ if (this._repeat <= 0) {
+ return 'completed';
+ }
+ if (elapsedTime === this._duration) {
+ return 'about-to-repeat';
+ }
+ return 'repeat';
+ };
+ Tween.prototype._processRepetition = function (elapsedTime, durationAndDelay) {
+ var completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat);
+ if (isFinite(this._repeat)) {
+ this._repeat -= completeCount;
+ }
+ // Reassign starting values, restart by making startTime = now
+ for (var property in this._valuesStartRepeat) {
+ var valueEnd = this._valuesEnd[property];
+ if (!this._yoyo && typeof valueEnd === 'string') {
+ this._valuesStartRepeat[property] = this._valuesStartRepeat[property] + parseFloat(valueEnd);
+ }
+ if (this._yoyo) {
+ this._swapEndStartRepeatValues(property);
+ }
+ this._valuesStart[property] = this._valuesStartRepeat[property];
+ }
+ if (this._yoyo) {
+ this._reversed = !this._reversed;
+ }
+ this._startTime += durationAndDelay * completeCount;
};
Tween.prototype._updateProperties = function (_object, _valuesStart, _valuesEnd, value) {
for (var property in _valuesEnd) {
diff --git a/dist/tween.umd.js b/dist/tween.umd.js
index 82c7babb..9d1e42b7 100644
--- a/dist/tween.umd.js
+++ b/dist/tween.umd.js
@@ -682,13 +682,11 @@
* it is still playing, just paused).
*/
Tween.prototype.update = function (time, autoStart) {
- var _this = this;
var _a;
if (time === void 0) { time = now(); }
if (autoStart === void 0) { autoStart = true; }
if (this._isPaused)
return true;
- var property;
var endTime = this._startTime + this._duration;
if (!this._goToEnd && !this._isPlaying) {
if (time > endTime)
@@ -715,72 +713,85 @@
var elapsedTime = time - this._startTime;
var durationAndDelay = this._duration + ((_a = this._repeatDelayTime) !== null && _a !== void 0 ? _a : this._delayTime);
var totalTime = this._duration + this._repeat * durationAndDelay;
- var calculateElapsedPortion = function () {
- if (_this._duration === 0)
- return 1;
- if (elapsedTime > totalTime) {
- return 1;
- }
- var timesRepeated = Math.trunc(elapsedTime / durationAndDelay);
- var timeIntoCurrentRepeat = elapsedTime - timesRepeated * durationAndDelay;
- // TODO use %?
- // const timeIntoCurrentRepeat = elapsedTime % durationAndDelay
- var portion = Math.min(timeIntoCurrentRepeat / _this._duration, 1);
- if (portion === 0 && elapsedTime === _this._duration) {
- return 1;
- }
- return portion;
- };
- var elapsed = calculateElapsedPortion();
+ var elapsed = this._calculateElapsedPortion(elapsedTime, durationAndDelay, totalTime);
var value = this._easingFunction(elapsed);
- // properties transformations
+ var status = this._calculateCompletionStatus(elapsedTime, durationAndDelay);
+ if (status === 'repeat') {
+ // the current update is happening after the instant the tween repeated
+ this._processRepetition(elapsedTime, durationAndDelay);
+ }
this._updateProperties(this._object, this._valuesStart, this._valuesEnd, value);
+ if (status === 'about-to-repeat') {
+ // the current update is happening at the exact instant the tween is going to repeat
+ // the values should match the end of the tween, not the beginning,
+ // that's why _processRepetition happens after _updateProperties
+ this._processRepetition(elapsedTime, durationAndDelay);
+ }
if (this._onUpdateCallback) {
this._onUpdateCallback(this._object, elapsed);
}
- if (this._duration === 0 || elapsedTime >= this._duration) {
- if (this._repeat > 0) {
- var completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat);
- if (isFinite(this._repeat)) {
- this._repeat -= completeCount;
- }
- // Reassign starting values, restart by making startTime = now
- for (property in this._valuesStartRepeat) {
- if (!this._yoyo && typeof this._valuesEnd[property] === 'string') {
- this._valuesStartRepeat[property] =
- // eslint-disable-next-line
- // @ts-ignore FIXME?
- this._valuesStartRepeat[property] + parseFloat(this._valuesEnd[property]);
- }
- if (this._yoyo) {
- this._swapEndStartRepeatValues(property);
- }
- this._valuesStart[property] = this._valuesStartRepeat[property];
- }
- if (this._yoyo) {
- this._reversed = !this._reversed;
- }
- this._startTime += durationAndDelay * completeCount;
- if (this._onRepeatCallback) {
- this._onRepeatCallback(this._object);
- }
- this._onEveryStartCallbackFired = false;
- return true;
+ if (status === 'repeat' || status === 'about-to-repeat') {
+ if (this._onRepeatCallback) {
+ this._onRepeatCallback(this._object);
}
- else {
- if (this._onCompleteCallback) {
- this._onCompleteCallback(this._object);
- }
- for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
- // Make the chained tweens start exactly at the time they should,
- // even if the `update()` method was called way past the duration of the tween
- this._chainedTweens[i].start(this._startTime + this._duration, false);
- }
- this._isPlaying = false;
- return false;
+ this._onEveryStartCallbackFired = false;
+ }
+ else if (status === 'completed') {
+ this._isPlaying = false;
+ if (this._onCompleteCallback) {
+ this._onCompleteCallback(this._object);
+ }
+ for (var i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
+ // Make the chained tweens start exactly at the time they should,
+ // even if the `update()` method was called way past the duration of the tween
+ this._chainedTweens[i].start(this._startTime + this._duration, false);
}
}
- return true;
+ return status !== 'completed';
+ };
+ Tween.prototype._calculateElapsedPortion = function (elapsedTime, durationAndDelay, totalTime) {
+ if (this._duration === 0 || elapsedTime > totalTime) {
+ return 1;
+ }
+ var timeIntoCurrentRepeat = elapsedTime % durationAndDelay;
+ var portion = Math.min(timeIntoCurrentRepeat / this._duration, 1);
+ if (portion === 0 && elapsedTime !== 0 && elapsedTime % this._duration === 0) {
+ return 1;
+ }
+ return portion;
+ };
+ Tween.prototype._calculateCompletionStatus = function (elapsedTime, durationAndDelay) {
+ if (this._duration !== 0 && elapsedTime < this._duration) {
+ return 'playing';
+ }
+ if (this._repeat <= 0) {
+ return 'completed';
+ }
+ if (elapsedTime === this._duration) {
+ return 'about-to-repeat';
+ }
+ return 'repeat';
+ };
+ Tween.prototype._processRepetition = function (elapsedTime, durationAndDelay) {
+ var completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat);
+ if (isFinite(this._repeat)) {
+ this._repeat -= completeCount;
+ }
+ // Reassign starting values, restart by making startTime = now
+ for (var property in this._valuesStartRepeat) {
+ var valueEnd = this._valuesEnd[property];
+ if (!this._yoyo && typeof valueEnd === 'string') {
+ this._valuesStartRepeat[property] = this._valuesStartRepeat[property] + parseFloat(valueEnd);
+ }
+ if (this._yoyo) {
+ this._swapEndStartRepeatValues(property);
+ }
+ this._valuesStart[property] = this._valuesStartRepeat[property];
+ }
+ if (this._yoyo) {
+ this._reversed = !this._reversed;
+ }
+ this._startTime += durationAndDelay * completeCount;
};
Tween.prototype._updateProperties = function (_object, _valuesStart, _valuesEnd, value) {
for (var property in _valuesEnd) {
diff --git a/package.json b/package.json
index 8560a18f..15affe45 100644
--- a/package.json
+++ b/package.json
@@ -39,10 +39,10 @@
"tsc": "tsc",
"tsc-watch": "tsc --watch",
"examples": "npx serve .",
- "test": "npm run build && npm run test-lint && npm run test-unit",
+ "test": "npm run build && npm run format-check && npm run test-unit",
"test-unit": "nodeunit test/unit/nodeunitheadless.cjs",
- "test-lint": "npm run prettier -- --check",
- "lint": "npm run prettier -- --write",
+ "format-check": "npm run prettier -- --check",
+ "format": "npm run prettier -- --write",
"prettier": "prettier .",
"prepare": "npm run build",
"version": "npm test && git add .",
diff --git a/src/Tween.ts b/src/Tween.ts
index 2cc49bb0..0c73854a 100644
--- a/src/Tween.ts
+++ b/src/Tween.ts
@@ -400,8 +400,6 @@ export class Tween {
update(time = now(), autoStart = true): boolean {
if (this._isPaused) return true
- let property
-
const endTime = this._startTime + this._duration
if (!this._goToEnd && !this._isPlaying) {
@@ -435,87 +433,106 @@ export class Tween {
const durationAndDelay = this._duration + (this._repeatDelayTime ?? this._delayTime)
const totalTime = this._duration + this._repeat * durationAndDelay
- const calculateElapsedPortion = () => {
- if (this._duration === 0) return 1
- if (elapsedTime > totalTime) {
- return 1
- }
+ const elapsed = this._calculateElapsedPortion(elapsedTime, durationAndDelay, totalTime)
+ const value = this._easingFunction(elapsed)
- const timesRepeated = Math.trunc(elapsedTime / durationAndDelay)
- const timeIntoCurrentRepeat = elapsedTime - timesRepeated * durationAndDelay
- // TODO use %?
- // const timeIntoCurrentRepeat = elapsedTime % durationAndDelay
+ const status = this._calculateCompletionStatus(elapsedTime, durationAndDelay)
- const portion = Math.min(timeIntoCurrentRepeat / this._duration, 1)
- if (portion === 0 && elapsedTime === this._duration) {
- return 1
- }
- return portion
+ if (status === 'repeat') {
+ // the current update is happening after the instant the tween repeated
+ this._processRepetition(elapsedTime, durationAndDelay)
}
- const elapsed = calculateElapsedPortion()
- const value = this._easingFunction(elapsed)
- // properties transformations
this._updateProperties(this._object, this._valuesStart, this._valuesEnd, value)
+ if (status === 'about-to-repeat') {
+ // the current update is happening at the exact instant the tween is going to repeat
+ // the values should match the end of the tween, not the beginning,
+ // that's why _processRepetition happens after _updateProperties
+ this._processRepetition(elapsedTime, durationAndDelay)
+ }
+
if (this._onUpdateCallback) {
this._onUpdateCallback(this._object, elapsed)
}
- if (this._duration === 0 || elapsedTime >= this._duration) {
- if (this._repeat > 0) {
- const completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat)
- if (isFinite(this._repeat)) {
- this._repeat -= completeCount
- }
+ if (status === 'repeat' || status === 'about-to-repeat') {
+ if (this._onRepeatCallback) {
+ this._onRepeatCallback(this._object)
+ }
- // Reassign starting values, restart by making startTime = now
- for (property in this._valuesStartRepeat) {
- if (!this._yoyo && typeof this._valuesEnd[property] === 'string') {
- this._valuesStartRepeat[property] =
- // eslint-disable-next-line
- // @ts-ignore FIXME?
- this._valuesStartRepeat[property] + parseFloat(this._valuesEnd[property])
- }
+ this._onEveryStartCallbackFired = false
+ } else if (status === 'completed') {
+ this._isPlaying = false
- if (this._yoyo) {
- this._swapEndStartRepeatValues(property)
- }
+ if (this._onCompleteCallback) {
+ this._onCompleteCallback(this._object)
+ }
- this._valuesStart[property] = this._valuesStartRepeat[property]
- }
+ for (let i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
+ // Make the chained tweens start exactly at the time they should,
+ // even if the `update()` method was called way past the duration of the tween
+ this._chainedTweens[i].start(this._startTime + this._duration, false)
+ }
+ }
+ return status !== 'completed'
+ }
- if (this._yoyo) {
- this._reversed = !this._reversed
- }
+ private _calculateElapsedPortion(elapsedTime: number, durationAndDelay: number, totalTime: number) {
+ if (this._duration === 0 || elapsedTime > totalTime) {
+ return 1
+ }
- this._startTime += durationAndDelay * completeCount
+ const timeIntoCurrentRepeat = elapsedTime % durationAndDelay
- if (this._onRepeatCallback) {
- this._onRepeatCallback(this._object)
- }
+ const portion = Math.min(timeIntoCurrentRepeat / this._duration, 1)
+ if (portion === 0 && elapsedTime !== 0 && elapsedTime % this._duration === 0) {
+ return 1
+ }
+ return portion
+ }
- this._onEveryStartCallbackFired = false
+ private _calculateCompletionStatus(elapsedTime: number, durationAndDelay: number) {
+ if (this._duration !== 0 && elapsedTime < this._duration) {
+ return 'playing'
+ }
- return true
- } else {
- if (this._onCompleteCallback) {
- this._onCompleteCallback(this._object)
- }
+ if (this._repeat <= 0) {
+ return 'completed'
+ }
- for (let i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
- // Make the chained tweens start exactly at the time they should,
- // even if the `update()` method was called way past the duration of the tween
- this._chainedTweens[i].start(this._startTime + this._duration, false)
- }
+ if (elapsedTime === this._duration) {
+ return 'about-to-repeat'
+ }
- this._isPlaying = false
+ return 'repeat'
+ }
+
+ private _processRepetition(elapsedTime: number, durationAndDelay: number) {
+ const completeCount = Math.min(Math.trunc((elapsedTime - this._duration) / durationAndDelay) + 1, this._repeat)
+ if (isFinite(this._repeat)) {
+ this._repeat -= completeCount
+ }
- return false
+ // Reassign starting values, restart by making startTime = now
+ for (const property in this._valuesStartRepeat) {
+ const valueEnd = this._valuesEnd[property]
+ if (!this._yoyo && typeof valueEnd === 'string') {
+ this._valuesStartRepeat[property] = this._valuesStartRepeat[property] + parseFloat(valueEnd)
}
+
+ if (this._yoyo) {
+ this._swapEndStartRepeatValues(property)
+ }
+
+ this._valuesStart[property] = this._valuesStartRepeat[property]
+ }
+
+ if (this._yoyo) {
+ this._reversed = !this._reversed
}
- return true
+ this._startTime += durationAndDelay * completeCount
}
private _updateProperties(
diff --git a/src/tests.ts b/src/tests.ts
index 6fbcd744..4ce30c71 100644
--- a/src/tests.ts
+++ b/src/tests.ts
@@ -1283,6 +1283,46 @@ export const tests = {
test.done()
},
+ 'Test repeat behaves the same with quick and slow updates'(test: Test): void {
+ TWEEN.removeAll()
+
+ const makeTween = (obj: {x: number}) => new TWEEN.Tween(obj).to({x: 100}, 100).repeat(20).start(0)
+
+ const obj1 = {x: 0}
+ const tween1 = makeTween(obj1)
+
+ for (let t = 0; t <= 300; t += 25) {
+ tween1.update(t)
+
+ const obj2 = {x: 0}
+ const tween2 = makeTween(obj2)
+ tween2.update(t)
+ test.equal(obj1.x, obj2.x, `t=${t}: ${obj1.x} === ${obj2.x}`)
+ }
+
+ test.done()
+ },
+
+ 'Test repeat+delay behaves the same with quick and slow updates'(test: Test): void {
+ TWEEN.removeAll()
+
+ const makeTween = (obj: {x: number}) => new TWEEN.Tween(obj).to({x: 100}, 100).delay(50).repeat(20).start(0)
+
+ const obj1 = {x: 0}
+ const tween1 = makeTween(obj1)
+
+ for (let t = 0; t <= 300; t += 25) {
+ tween1.update(t)
+
+ const obj2 = {x: 0}
+ const tween2 = makeTween(obj2)
+ tween2.update(t)
+ test.equal(obj1.x, obj2.x, `t=${t}: ${obj1.x} === ${obj2.x}`)
+ }
+
+ test.done()
+ },
+
'Test yoyo with repeat Infinity happens forever'(test: Test): void {
TWEEN.removeAll()
@@ -1409,6 +1449,69 @@ export const tests = {
test.done()
},
+ 'Test yoyo reverses at right instant'(test: Test): void {
+ TWEEN.removeAll()
+
+ const obj = {x: 0}
+ new TWEEN.Tween(obj).to({x: 100}, 100).repeat(1).yoyo(true).start(0)
+
+ TWEEN.update(98)
+ test.equal(obj.x, 98)
+
+ TWEEN.update(99)
+ test.equal(obj.x, 99)
+
+ // Previously this would fail, the first update after 100 would happen as if yoyo=false
+ TWEEN.update(101)
+ test.equal(obj.x, 99)
+
+ TWEEN.update(101)
+ test.equal(obj.x, 99)
+
+ TWEEN.update(102)
+ test.equal(obj.x, 98)
+
+ test.done()
+ },
+
+ 'Test yoyo callbacks happen on right order'(test: Test): void {
+ TWEEN.removeAll()
+
+ let events: string[] = []
+ const obj = {x: 0}
+
+ new TWEEN.Tween(obj)
+ .to({x: 100}, 100)
+ .repeat(1)
+ .yoyo(true)
+ .easing(TWEEN.Easing.Linear.None)
+ .onUpdate(() => events.push('update'))
+ .onStart(() => events.push('start'))
+ .onEveryStart(() => events.push('everystart'))
+ .onRepeat(() => events.push('repeat'))
+ .onComplete(() => events.push('complete'))
+ .start(0)
+
+ function testAndReset(expected: string[]) {
+ test.deepEqual(events, expected)
+ events = []
+ }
+
+ testAndReset([])
+ TWEEN.update(99)
+ testAndReset(['start', 'everystart', 'update'])
+ TWEEN.update(101)
+ testAndReset(['update', 'repeat'])
+ TWEEN.update(150)
+ testAndReset(['everystart', 'update'])
+ TWEEN.update(199)
+ testAndReset(['update'])
+ TWEEN.update(201)
+ testAndReset(['update', 'complete'])
+
+ test.done()
+ },
+
'Test TWEEN.Tween.stopChainedTweens()'(test: Test): void {
const t = new TWEEN.Tween({}),
t2 = new TWEEN.Tween({})
diff --git a/test/unit/nodeunit.html b/test/unit/nodeunit.html
index 69c7595b..130bf914 100644
--- a/test/unit/nodeunit.html
+++ b/test/unit/nodeunit.html
@@ -7,6 +7,13 @@
+
+