Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support complex tweens #260

Closed
usefulthink opened this issue May 4, 2016 · 22 comments · Fixed by #536
Closed

support complex tweens #260

usefulthink opened this issue May 4, 2016 · 22 comments · Fixed by #536

Comments

@usefulthink
Copy link

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
Copy link
Author

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

@mikebolt
Copy link
Contributor

mikebolt commented May 4, 2016

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
Copy link
Author

@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.

@mikebolt
Copy link
Contributor

mikebolt commented May 5, 2016

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" [email protected] 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
#260 (comment)

@usefulthink
Copy link
Author

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
Copy link
Contributor

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
Copy link
Collaborator

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
Copy link
Member

sole commented Jun 21, 2016

@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
Copy link
Member

sole commented Jun 21, 2016

@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: tweenjs/discuss#1

@dalisoft
Copy link
Collaborator

@sole, sorry

@usefulthink
Copy link
Author

@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
Copy link
Collaborator

@usefulthink @sole
What about this idea?
tweenjs/discuss#1 (comment)

@sole
Copy link
Member

sole commented Oct 3, 2016

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

@usefulthink
Copy link
Author

usefulthink commented Dec 10, 2016

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:

  • The default-behaviour would be fitted into a NumericInterpolator.
  • We could have a AngleInterpolator that handles shortest paths and wrapping at 2π for angles properly.
  • I can imagine things like interpolators for string-values (css-colors, typing-effects and such, incrementing numbers, ...)

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
Copy link
Contributor

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
Copy link
Collaborator

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
Copy link
Author

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.

@dalisoft dalisoft mentioned this issue Jan 1, 2017
@miltoncandelero
Copy link

miltoncandelero commented Apr 21, 2020

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
Copy link
Member

trusktr commented May 30, 2020

@miltoncandelero Yep! Looking to get that merged soon!

@trusktr
Copy link
Member

trusktr commented May 30, 2020

Actually #520 is more updated.

@miltoncandelero
Copy link

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 trusktr self-assigned this May 31, 2020
@trusktr
Copy link
Member

trusktr commented May 31, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants