chenglou / react-tween-state

React animation.
Other
1.74k stars 70 forks source link

Animate arrays and objects #22

Closed andreaferretti closed 9 years ago

andreaferretti commented 9 years ago

A rather common case for animation involves tweening more than one field together inside some nested data structure.

For instance, I am trying to port to React the demo for my library Paths.js. If you look - say - at the first chart, you will notice that when you click on a slice, it opens, while other slices close at the same time. This is done by using a vector of coefficients, which initially looks like

getInitialState: function() {
  return {
    expanded: [0, 0, 0, 0, 0]
  }
}

If you click on the second slice, it is animated to [0, 1, 0, 0, 0]. Then if you click on the fourth slice, it is animated towards [0, 0, 0, 1, 0], and so on.

In this particular case, I could fake the effect by having a fixed number of coefficients (namely 5) in the state. But I would like to write a reusable component, hence the number of slices is not known in advance.

Right now, it seems that react-tween-state works fine for animating scalar properties. It would be nice to support the animation of nested data structures. Ractive does this and makes it a breeze to write animated components

chenglou commented 9 years ago

I designed the second form of the API for this exact reason: animating nested data structure.

But I guess it's time to inject a bit of pragmatism into this library. Maybe animating an array of element is a frequent use case. You could totally write your own helper today that does this: traverses through an array/object and tweenState each item (is that want you want?). The only question is: is it that frequent of a use-case?

@twobit (and @jiyinyiyong. That might make your use-case less boilerplat-y).

andreaferretti commented 9 years ago

Yes, in this particular case I could just iterate over the array, but in general the data may be nested, and a recursive version in needed. I think this is best left to the tween library itself.

I cannot speak for other people, but the examples on my demo page more or less all require nested data.

For simplicity, I just wrote a simple mixin that interpolates arbitrarily nested structures here. You can see it in use in this component.

Of course, it is very rough: it does not stack animations together nicely as does react-tween-state, and every instance will call its own requestAnimationFrame (as opposed to keeping a single call, and updating everything that needs animation, even in separate components), but it works for me, at least temporarily.

By the way, the version I linked just calls setState on every frame - are there any reasons why you avoid doing this?

chenglou commented 9 years ago

Just to make it clear: that API I linked to already can achieve what you want. You just need to write a wrapper on top of it.

Not sure what you mean by your question? What did I avoid doing?

andreaferretti commented 9 years ago

I agree that what I want to do is possible with the advanced form. It is just that it is inconvenient, and in any case I would end end always calling the wrapper instead of the mixin, which is why I feel it should be part of the mixin itself.

About my second question: this mixin immediately calls setState with the final value, and then one can access the intermediate value with this.getTweeningValue(stateNameString). I thought the obvious approach would be to call setState repeatedly with the intermediate values, so that one could access the state normally. This would also make it easier to switch between no animation and animation - just change the calls from setState to tweenState.

I think that you probably have good reasons to keep the intermediate state in a different place than this.state. Would you mind to explain this bit?

chenglou commented 9 years ago

@andreaferretti @twobit what do you think of this API?

tweenState(['path', 'to', 'collection'], [0, 1, 2], config)

First param is unrelated: it's the new path API I'm think about (akin to mori/immutable-js get/update in a nested collection). Second part is an optional array of names you want animated (mandatory if the path doesn't point to a scalar but a collection, of course).

So the above example would animate the item 0 1 and 2 inside state {path: {to: {collection: ['a', 'b', 'c']}}} (collection could be an object with keys 0 1 2 as well).

I could have gone with an API that detects you're animating an array and just automagically animate each element. But I feel this is too magical and less flexible than the above counterpart. Suggestions?

twobit commented 9 years ago

Clojure style makes sense, also maybe like the React update style:

tweenState({path: {to: {collection: {$tweenArray: [...]}}}})

In any case, here's a snippet of how I've been tweening arrays for the time being. Might help somebody:

  arrayTween: function(a0, a1) {
    var points = a0.map(function(p, i) {
      return d3.interpolate(p, a1[i]);
    });

    return function(t) {
      return t < 1 ? points.map(function(p) {return p(t);}) : a1;
    };
  },
andreaferretti commented 9 years ago

I am not convinced. It is still much more cumbersome than it needs to be.

My first two priorities are, in order:

I have written a few lines that do exactly what I need here. It can be used with essentially no changes with respect to a version without animation, as in this example - see the live demo by clicking on the red dots.

Having a smooth interpolation for the case where two animations must play together is a nice plus, but it is of lower priority for my case. If anyone is interested, I can publish the mixin

chenglou commented 9 years ago

Clojure style array for path is now implemented. React-motion now implements transitioning between arbitrary nested data!