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 @@ + +