tween.js
tween.js copied to clipboard
Option for Tween instances to remember last values
I have an idea for API improvement, with backwards compatibility:
new TWEEN.Tween(objectWithInitialValues, group, {rememberLastValues: true})
where rememberLastValues would make it so any time you call start() it will start with the last value where it previously was.
This will greatly improve the dev experience (f.e. it will make end user code much cleaner because instead of having multiple Tween instances we can have just one).
Here is a before and after code example to achieve the same thing (adapted from an actual project):
before, 51 lines of code (you have to do something like this currently):
class ExampleBeforeAPIChanges {
openTween = null
closeTween = null
menuOpen = false
tweenRAF = null
makeOpenTween() {
this.openTween = new TWEEN.Tween({menuPosition: (someObject && someObject.position.x) || 0})
.onComplete(() => this.openTween.stop())
.onUpdate(obj => (someObject.position.x = obj.menuPosition))
.easing(TWEEN.Easing.Exponential.Out)
}
makeCloseTween() {
this.closeTween = new TWEEN.Tween({menuPosition: (someObject && someObject.position.x) || -MENU_WIDTH})
.onComplete(() => this.closeTween.stop())
.onUpdate(obj => (someObject.position.x = obj.menuPosition))
.easing(TWEEN.Easing.Exponential.Out)
}
openMenu = () => {
this.menuOpen = true
if (this.closeTween && this.closeTween.isPlaying()) this.closeTween.stop()
this.makeOpenTween()
this.openTween.to({menuPosition: -MENU_WIDTH}, 800).start()
this.tweenLoop()
}
closeMenu = () => {
this.menuOpen = false
if (this.openTween && this.openTween.isPlaying()) this.openTween.stop()
this.makeCloseTween()
this.closeTween.to({menuPosition: 0}, 800).start()
this.tweenLoop()
}
toggleMenu = () => (this.menuOpen ? this.closeMenu() : this.openMenu())
tweenLoop() {
if (this.tweenRAF) return
this.tweenRAF = requestAnimationFrame(function loop(t) {
if (this.openTween && this.openTween.isPlaying()) this.openTween.update(t)
else if (this.closeTween && this.closeTween.isPlaying()) this.closeTween.update(t)
else return (this.tweenRAF = null)
this.tweenRAF = requestAnimationFrame(loop)
})
}
}
after, 37 lines of code (will be cleaner, simplifies logic, more efficient):
class ExampleAfterAPIChanges {
tween = new TWEEN.Tween({menuPosition: 0}, {group, rememberLastValues: true})
.onComplete(() => this.openTween.stop())
.onUpdate(obj => (someObject.position.x = obj.menuPosition))
.easing(TWEEN.Easing.Exponential.Out)
menuOpen = false
tweenRAF = null
openMenu = () => {
this.menuOpen = true
if (this.tween.isPlaying()) this.tween.stop()
this.tween.to({menuPosition: -MENU_WIDTH}, 800).start()
this.tweenLoop()
}
closeMenu = () => {
this.menuOpen = false
if (this.tween.isPlaying()) this.tween.stop()
this.tween.to({menuPosition: -MENU_WIDTH}, 800).start()
this.tweenLoop()
}
toggleMenu = () => (this.menuOpen ? this.closeMenu() : this.openMenu())
tweenLoop() {
if (this.tweenRAF) return
this.tweenRAF = requestAnimationFrame(function loop(t) {
this.tween.update(t)
if (this.tween.isPlaying()) this.tweenRAF = requestAnimationFrame(loop)
else this.tweenRAF = null
})
}
}
This is what the above code does (transitioning back and forth any time I click):

What we could do is for backwards compatibility, allow new TWEEN.Tween(obj, group, {rememberLastValues: true})
Then, once we're ready for a breaking change (let's move to the future!) then we can make that the default behavior in an options parameter, so people just write new TWEEN.Tween(obj, {group}) or new TWEEN.Tween(obj, {group, rememberLastValues: false}) and additionally add a new reset() method to reset back to initial state.
To make this work, it also depends on https://github.com/tweenjs/tween.js/issues/512 being fixed, moving the state modification inside of Tween instead of Group, otherwise in my after example the checks for isPlaying() will not work as expected.
Tracking this in the new General Project Board. Feel free to start adding issues into the board, and we can prioritize by dragging more important items to the top.
Should stop reset things, instead of having a new reset method? I need to verify what currently happens with stop/start.
Yep, this makes development easier. Also this can be implemented without loss of much performance (which i did some years ago for my library). Good idea
In CSS, a feature similar to this is configurable and is called animation-fill-mode.
Here's a current way to write the same thing as the OP 37-line example without this feature with the same number of lines:
37 lines:
class ExampleBeforeAPIChangesUsingStartMethod {
tween = new TWEEN.Tween({menuPosition: 0}, group)
.onComplete(() => this.openTween.stop())
.onUpdate(obj => (someObject.position.x = obj.menuPosition))
.easing(TWEEN.Easing.Exponential.Out)
menuOpen = false
tweenRAF = null
openMenu = () => {
this.menuOpen = true
if (this.tween.isPlaying()) this.tween.stop()
this.tween.to({menuPosition: -MENU_WIDTH}, 800).start(performance.now(), true) // THIS
this.tweenLoop()
}
closeMenu = () => {
this.menuOpen = false
if (this.tween.isPlaying()) this.tween.stop()
this.tween.to({menuPosition: -MENU_WIDTH}, 800).start(performance.now(), true) // THIS
this.tweenLoop()
}
toggleMenu = () => (this.menuOpen ? this.closeMenu() : this.openMenu())
tweenLoop() {
if (this.tweenRAF) return
this.tweenRAF = requestAnimationFrame(function loop(t) {
this.tween.update(t)
if (this.tween.isPlaying()) this.tweenRAF = requestAnimationFrame(loop)
else this.tweenRAF = null
})
}
}
The start(time, true) calls tell the tweens to start from the last known values.
So we have a few things to think about before making a final solution:
- https://github.com/tweenjs/tween.js/issues/512 is fixed, so that
start()resets values by default, and this matches with the current behavior of.chain().- start(time, true)
- reset() method resets values.
- new possible option for new Tween
- If we add this, then does it modify the default behavior of .chain(), .start(), .yoyo() (etc?) so that all APIs remember the last values? Need to enumerate which API parts should carry the behavior from this new option, and if we still need reset().
We want the result API to be clean, without piling on a new API and not thinking through all these parts.
Closing, as recent versions of Tween.js have a .startFromCurrentValues() method that does what the OP desired.