tween.js
tween.js copied to clipboard
Clock synchronization issue
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.
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!
Do you have a code snippet that demonstrates what issue you had?
I don't have a code snippet, but here is how you can recreate it:
- 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. - Set up an animation that will take 5 seconds to complete. Do not pass any arguments to
start(). - 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.
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).
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.
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/
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.
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.
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...
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)
})