tween.js icon indicating copy to clipboard operation
tween.js copied to clipboard

Option for Tween instances to remember last values

Open trusktr opened this issue 5 years ago • 5 comments

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):

example

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.

trusktr avatar May 29 '20 18:05 trusktr

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.

trusktr avatar May 29 '20 18:05 trusktr

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.

trusktr avatar May 29 '20 19:05 trusktr

Should stop reset things, instead of having a new reset method? I need to verify what currently happens with stop/start.

trusktr avatar May 30 '20 07:05 trusktr

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

dalisoft avatar May 31 '20 09:05 dalisoft

In CSS, a feature similar to this is configurable and is called animation-fill-mode.

trusktr avatar Jun 03 '20 00:06 trusktr

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.

trusktr avatar Apr 21 '23 20:04 trusktr

Closing, as recent versions of Tween.js have a .startFromCurrentValues() method that does what the OP desired.

trusktr avatar Jan 15 '24 03:01 trusktr