tweenjs / tween.js

JavaScript/TypeScript animation engine
https://tweenjs.github.io/tween.js/
Other
9.83k stars 1.41k forks source link

support complex tweens #260

Closed usefulthink closed 4 years ago

usefulthink commented 8 years ago

I did use tween.js a lot in combination with three.js, where there are quite often tweens that need to manipulate properties of different objects at the same time.

What I did mostly was to handle this using a custom onUpdate-function like this:

var object = new THREE.Mesh(/* ... */);
var tweenParams = {rotationX: 0, positionZ: 0};
var tween = new TWEEN.Tween(tweenParams).onUpdate(function() {
  object.rotation.x = this.rotationX;
  object.position.z = this.positionZ;
});
tween.to({rotationX: Math.PI, positionZ: 10}).start();

But it would be nice if that could be made a bit easier.

There are two possible solutions I thought of:

1) allow tweens with complex objects

var tween = new TWEEN.Tween(object).to({
  position: { z: 10 },
  rotation: { x: Math.PI }
}).start();

2) something like tweenGroups (i.e. tweens that always start and end together)

var tween = new TWEEN.Group({
  position: new TWEEN.Tween(object.position),
   rotation: new TWEEN.Tween(object.rotation)
});

tween.to({
  position: { z: 10 },
  rotation: { x: Math.PI }
}).start();

The group would expose an interface identical to the regular tweens and forward most calls to the individual tweens.

what do you think?

usefulthink commented 8 years ago

I would be very happy to provide an implementation and docs/examples for either variant if you like it.

mikebolt commented 8 years ago

I like option 1 better. The only thing it changes is allowing "nested" or "recursive" properties to be tweened. I would also prefer that "tween groups" actually be groups of TWEEN.Tween objects, if/when they are implemented. Synchronous tweens would then be possible with some kind of "to" and "startAll" method of the tween group.

Option 2 is potentially faster than option 1, because option 1 requires checking the recursive properties in the onUpdate method, or at least checking if each property is an object. This will be a very minor performance hit for tweens without nested properties.

Please implement option 1 when you have the time, and make a pull request. Try to change as little of the code as possible and keep the "hot path" efficient.

usefulthink commented 8 years ago

@mikebolt I think the first option has a nicer interface, but implementation-wise I would prefer the group-variant, because

I think it could be possible to have both variants supported with the same codebase (the first option would then be a syntactical sugar variant for the second).

This is how I thought:

That way there would be the simple use-case (i.e. all tween-settings identical) supported with a simple syntax and the user would have the option to manually create a group if more configuration is required.

mikebolt commented 8 years ago

OK, I can see your reasoning. Let's draft an API for tween groups. Once we have a good draft I think we should do TDD, as in write the tests before implementing it.

I implemented a version of tween groups a while back, but it was never merged. My main goal was to allow separate updating, and therefore separate pausing. On May 5, 2016 3:01 AM, "Martin Schuhfuss" notifications@github.com wrote:

@mikebolt https://github.com/mikebolt I think the first option has a nicer interface, but implementation-wise I would prefer the group-variant, because

  • it's easier to get right (at least i think so)
  • no need to change existing behavior – it could even be implemented externally if desired (thus keeping the complexity and speed of the update-function as it is).
  • the individual Tweens in the group could have different easing-equations or even different durations configured, just sharing the control-interface (i.e. start/stop/callbacks..)

I think it could be possible to have both variants supported with the same codebase (the first option would then be a syntactical sugar variant for the second).

This is how I thought:

  • in the start-function add a check for complex values (i.e. nested objects)
  • when there are nested objects, automatically create a structure of tween-groups
  • add that structure to the scheduler instead of the tween itself.

That way there would be the simple use-case (i.e. all tween-settings identical) supported with a simple syntax and the user would have the option to manually create a group if more configuration is required.

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/tweenjs/tween.js/issues/260#issuecomment-217116450

usefulthink commented 8 years ago

So, just what I had in mind in more detail:

The basic Idea for a group is to make a set of tweens controllable together. They should behave identical to regular tweens with regards to the exposed interface.

first, the tween-group

// creating a group
var group = new TWEEN.Group({
  tween1: new TWEEN.Tween(object1),
  tween2: new TWEEN.Tween(object2)
});

// children are stored by key in a children-object of the group so they could be 
// accessed independently
group.children === {tween1: ..., tween2: ...};

// maybe we should also add methods add/remove/contains to modify 
// the list of child-tweens? What do you think?

// groups have the same methods and behavior as regular tweens.
// The `to`-method is special though, as it will accept values for all child-tweens 
// and pass them on to the childs based on the top-level key.
group.to({
  tween1: {value: 1, otherValue: 10},
  tween2: {value: 2, otherValue: 20}
}, 500);

// would internally call
tween1.to({value: 1, otherValue: 10}, 500);
tween2.to({value: 2, otherValue: 20}, 500);

// other methods will set the same values / call the corrensponding methods 
// on all child-tweens
group.easing(TWEEN.Easing.Quadratic.In);
group.delay(100);
group.start();

// we might consider here the possibility to pass objects to these methods in 
// order to control child-tweens independently
group.easing({
  tween1: TWEEN.Easing.Quadratic.In, 
  tween2: TWEEN.Easing.Quadratic.InOut
});
group.delay({tween1: 0, tween2: 500});

// or to set independent durations
group.to({...}, { tween1: 1000, tween2: 2000 });

this is still not a complete proposal, but I think it's enough to get an Idea and to collect opinions.

mikebolt commented 8 years ago

Thank you. I agree that we should collect more opinions. Here's mine:

I agree that groups should make groups of tweens controllable together, and that they should have all the methods that single tweens have.

However, I think that allowing grouped tweens to have keys would cause serious confusion. I think that tween groups should behave like a set of tweens, not a map of tweens. If grouped tweens must have names or keys, then it seems to me like there is no reason to put them in a group. You could have just made multiple separate tweens yourself. Yeah, you can control them together, but there's another problem...

One common use case is to make a bunch of tweens algorithmically, in some kind of loop:

var particleTweens = [];
for (var i = 0; i < NUM_TWEENS; ++i) {
  particleTweens.push(new TWEEN.Tween(particle[i]).to(particle[i].target));
}

Now suppose I want to group the tweens together, so that I can set the easing with one call, and start them all at once. With your proposal, I have to invent a key for each one, then remember those keys, and it would look something like this:

var particleTweens = [];
var groupInitializer = {};
for (var i = 0; i < NUM_TWEENS; ++i) {
  particleTweens.push(new TWEEN.Tween(particles[i]).to(particles[i].target));
  particles[i].tweenKey = 'particle_tween#' + i;
  groupInitializer[particles[i].tweenKey] = particleTweens[i];
}
particleTweenGroup = new TWEEN.Group(groupInitializer);
particleTweenGroup.easing(TWEEN.Easing.Quadratic.In);
particleTweenGroup.start();

In general, I think that if you want to have named tweens, you can do that with JavaScript, and if you want to control tweens separately, then you can do that by making separate tweens and controlling them separately. The only purpose of the groups should be to blindly perform actions on an entire set of tweens simultaneously.

Here's how I would like the above code to be written:

var particleTweenGroup = new TWEEN.Group();
for (var i = 0; i < NUM_TWEENS; ++i) {
  particleTweenGroup.add(new TWEEN.Tween(particle[i]).to(particle[i].target));
}
particleTweenGroup.easing(TWEEN.Easing.Quadratic.In);
particleTweenGroup.start();

Or, alternatively:

var particleTweens = [];
for (var i = 0; i < NUM_TWEENS; ++i) {
  particleTweens.push(new TWEEN.Tween(particle[i]).to(particle[i].target));
}
var particleTweenGroup = new TWEEN.Group(particleTweens);
particleTweenGroup.easing(TWEEN.Easing.Quadratic.In);
particleTweenGroup.start();
dalisoft commented 8 years ago

Hi @usefulthink I have better than your idea, check out Unim.js - Most powerful and feature-rich animation engine based-on Tween.js. So, rememeber, it's just for use. NOT A DIST, RE-SELL, OR ETC.

sole commented 8 years ago

@dalisoft please, please keep discussions to the scope of tween.js. If people want to use your project, they will. Talking about something else just distracts us from the issue we are trying to discuss.

sole commented 8 years ago

@usefulthink I like the idea that Tween.Group creates tweens internally! I also like @mikebolt idea + observations.

Also, I am thinking that this could totally live on its own project, somewhere like tweenjs/group in the organisation? I have written some ideas here discussing this possible structure: https://github.com/tweenjs/discuss/issues/1

dalisoft commented 8 years ago

@sole, sorry

usefulthink commented 8 years ago

@sole Thanks! Yes, I would fully agree with that tweenjs/group thing. I was already considering implementing a standalone module for this as there are zero requirements to anything in the Manager or Tween-modules itself.

Also considering @mikebolt's take on it, I think it should be possible without too much problems to simply join both use-cases (groups as arrays vs. groups as objects). I will probably just sketch out such a module.

dalisoft commented 8 years ago

@usefulthink @sole What about this idea? https://github.com/tweenjs/discuss/issues/1#issuecomment-227749172

sole commented 8 years ago

@usefulthink did you think more about this? :) thanks!

usefulthink commented 7 years ago

I just recalled this thread when I was thinking about how to solve tweening for a very specific usecase:

When working with three.js, I basically just want to be able to do things like

const tween = new TWEEN.Tween(object.material)
    .to(someOtherMaterial).start();

or (and this is in essence what inspired the initial Idea):

const tween = new TWEEN.Tween(object)
    .to({position: somePosition, quaternion: andARotation});

Now, this obviously doesn't work as of now. But: in all these cases (including the ones I mentioned in the comments before) this could be solved if there were some configurable part of this library that allows it to handle any data-type, not just numbers. Let's call them interpolators for now.

If we now make tween.js aware of the interpolators to use, this could look something like this:

const tween = new TWEEN.Tween(object)
    .to({position: somePosition, quaternion: andARotation});
    .interpolators({position: TWEEN.ThreeVector3, quaternion: TWEEN.ThreeQuaternion})
    .start();

Those interpolators could be supplied via external libraries for any datatype and library. They would take care of any value-access for the property, so tween.js wouldn't need to care about what kind of value it is.

The Idea just came up and I didn't write a PoC or something like that, but I believe they would look a bit like this (I just thought about this for half an hour):

class ThreeVector3 {
  constructor(target, to) {
    // instance is created for every property interpolated by this interpolator 
    // when a tween is started.
    // target: the value-instance that will be written to (also starting-value)
    // to: the specified end-value for the tween
  }

  update(value) {
    // value: the current progress-value ([0..1]) after applied easing-calculation
    // updates the target-property with the interpolated value
  }
};

Bonus features:

What do you think?

(I'm really open for better suggestions regarding the naming, structure and so on, just wanted to get the Idea out).

mikebolt commented 7 years ago

I like it. The code currently does interpolation. However, currently interpolation is only performed on numeric properties. We basically have a function like this:

f(start, end, progress) = <interpolated value>

Currently start and end must be numeric values, and the result is a numeric value. We achieve object-to-object tweening by applying this to all of an object's numeric properties.

If we changed this to an "anything to anything" function, as you propose, then we don't need to apply the interpolation to each property, necessarily. Would the interpolator return an interpolated object, or would it be responsible for modifying the tweened object?

This may be hard to do without breaking backwards-compatibility. You are welcome to try, but maybe we should save this for the next version.

dalisoft commented 7 years ago

I don't know about interpolation of objects, but i tried some string interpolation.

You can look on My tweening engine based on tween.js, not best as Phaser or other projects based on tween.js, but i think enough for support some complex tweening.

Object tweening code lines: L384-L447

I hope you find this useful, thanks for amazing library.

I thinked about make PR, but all of my PR isn't accepted due of some tests.

Sorry for bad english

usefulthink commented 7 years ago

Would the interpolator return an interpolated object, or would it be responsible for modifying the tweened object?

I think it's better if the interpolator only gets access to the value of a single property of the tweened object and it's specified target value. If this is a primitive value, it has to return a value for the library to update the tweened object.

But in case of animated Vectors (or any other object- or array-type) this is not strictly neccessary because the Vector-properties will most likely be updated in place to prevent unneccessary object-allocations.

I would probably implement something like this:

// replacing Tween.js#L340-L355
const interpolator = this.getInterpolator(propertyName);
const interpolated = interpolator.update(value);
if (typeof interpolated !== 'undefined') {
  _object[property] = interpolated;
}

But yes, I will maybe find some time for a proof-of-concept implementation over the weekend or the holidays. Let's discuss this further when that is done.

API-wise it should be possible to get this done without breaking compatibility, although I still have to find a solution for the .to({prop: [1,2,3]}) usecase.

miltoncandelero commented 4 years ago

May I ask what happened to the original idea of nesting properties? A simple recursive function could do it. Are we interested on this? shall I make a PR?

Edit: it seems there is a PR waiting, #366

trusktr commented 4 years ago

@miltoncandelero Yep! Looking to get that merged soon!

trusktr commented 4 years ago

Actually https://github.com/tweenjs/tween.js/pull/520 is more updated.

miltoncandelero commented 4 years ago

Yes, since we needed the feature I asked Malows to make it happen and we are using tween.js directly from his fork now. If the feature gets merged we can go back to depending on this one

trusktr commented 4 years ago

Alright, the nested properties PR is merged!

I think there were also some other ideas above, like

const interpolator = this.getInterpolator(propertyName);
const interpolated = interpolator.update(value);
if (typeof interpolated !== 'undefined') {
  _object[property] = interpolated;
}

Please feel free to open new issues for the specific ideas if still desired.