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

Clock synchronization issue

Open christopherjbaker opened this issue 5 years ago • 9 comments

When controlling the clock by passing a value to TWEEN.update(...), the default _startTime for a tween started with .start() still uses the browser clock, which means tweens do not behave as expected. I've fixed this in my own code by redefining TWEEN.now to return the last value I sent to .update(...). It seems to me that if its in a "controlled mode", everything should use those controlled values.

christopherjbaker avatar Jul 29 '20 21:07 christopherjbaker

That change may work for your case, but I think it may break individually-controlled Tweens. TWEEN.now() is merely supposed to be an API in place of performance.now() in case some browser doesn't have that.

If interested to make a PR but adding this feature behind an API more like TWEEN.lastTime() or similar, and update the tests, that would be great!

trusktr avatar Sep 24 '20 20:09 trusktr

Do you have a code snippet that demonstrates what issue you had?

trusktr avatar Sep 24 '20 20:09 trusktr

I don't have a code snippet, but here is how you can recreate it:

  1. Set up a RAF that calls TWEEN.update(Date.now() + 2500). This sets up the internal clock so that it will not line up with the browser's clock. In my case, I'm using threejs's clock, which tracks in seconds instead of ms.
  2. Set up an animation that will take 5 seconds to complete. Do not pass any arguments to start().
  3. Observe that it does not behave as intended. I believe in this case, it will start halfway done, because it "started" at the current time, but the update was called with the current time + 2.5 seconds.

You've provided control of the internal clock, but you don't use the custom clock values when you start a tween, so they start in a different timeframe than the updates.

christopherjbaker avatar Sep 29 '20 05:09 christopherjbaker

Interesting, haven't used it like that before. Normally what I do is control every tween individually, which may be a workaround for now:

const tween = new Tween(...)....start(startTime)
// ...
tween.update(newTime)

That way each tween can operate with its own custom time being passed in (plus easier to encapsulate the tween to a particular component of your UI, pause it individually, update it individually, etc, while other tweens in other components can also do their own thing).

trusktr avatar Oct 12 '20 04:10 trusktr

I'm going to try and write a PR that addresses this, because I've been bitten by it too when trying to programmatically step through time while using tween.js.

audiodude avatar Nov 22 '20 03:11 audiodude

Here's the basic idea of what I'm trying to do:

EDIT: Fixed the fiddle and actually it seems to work just fine, https://jsfiddle.net/audiodude/anL0tzux/37/

audiodude avatar Nov 22 '20 04:11 audiodude

Sorry for all the comments. I figured out that the problem with my actual code is that I call start later after creating the tween, which I'm having trouble producing a minimal example of. I guess the workaround is to call start with an explicit startTime.

audiodude avatar Nov 22 '20 04:11 audiodude

Here's an idea. We could add a property to Group that would tell tween.js to expect that the group will have its time value manually controlled.

mikebolt avatar Dec 30 '20 02:12 mikebolt

Yeah, I thought the same. Basically if you use a Group, the Group can be resonsible for passing in the times to each tween instead of passing undefined and letting the tweens determine time.

Also it seems as if Tween is trying to do everything, but maybe in a breaking change we can split concepts into Timeline, Keyframe (Timeline has Keyframes, and we remove the .chain, .repeat, and .yoyo APIs from Tween because then we just make Timelines and Timelines them selves can repeat), Tween (does nothing but tweening and does it well, and not much else), etc. In this new setup, Tweens could still be used individually, or in Groups, but chains and yoyos would be done a different way (with Timelines).

We'd still want to be able to use Tween directly, because we might subclass those as special properties on other types of objects. For example:

const mesh = new Mesh
mesh.rotation
  .to({x: 360}) // mesh.rotation is a Tween, but it also has its own x, y, z properties, for example.
  .start()

// ...
mesh.rotation.update(time)
// or mesh is a Group perhaps, and
mesh.update(time) // updates rotation, position, etc

// or

const anim = new Timeline(
  new Keyframe({...}, mesh.rotation),
  new Keyframe({...}, mesh.rotation),
)

// or

const anim = new Timeline(
  new Keyframe({...}, mesh.rotation),
  new Keyframe({...}, otherMesh.rotation), // animation jumps from one mesh to the other (like in a track relay)
)

// ...

anim.update(time)

Basically we have the best of different worlds: using Tween individually without Timelines is convenient for different use cases like making animatable objects that are more convenient to use than making Timelines, but we can still stick them into Timelines to orchestrate things on a bigger picture. Contrast with Timeline APIs that encapsulate Tweens, and timeline animations have to be mapped to other objects that are unaware of time or animation (i.e. you can not use the library and easily have a concept like mesh.rotation being both a tweenable thing and a vector3).

The API is totally up for discussion, but this is some food to get thoughts started...

trusktr avatar Dec 31 '20 04:12 trusktr

Will close after thinking again about this because the expectation is that if we want to manage time ourselves, we can pass in the values to both start(time) and update(time), knowing exactly what we want. F.e.

tween.start(performance.now() + 2500)

requestAnimationFrame(function loop(now) {
  tween.update(now + 2500)
})

trusktr avatar Apr 23 '23 06:04 trusktr