chenglou / react-motion

A spring that solves your animation problems.
MIT License
21.69k stars 1.16k forks source link

Feeling bored/curious? #9

Closed chenglou closed 9 years ago

chenglou commented 9 years ago

This is a very new library. If you were curious as to how its design came to be, you're at the right place!

This issue's focused on the declarative tweening itself. I'll make another one for unmounting animation. For reference, check my React-Europe talk on animation.

I've partitioned the possible APIs into representative categories, then trimmed away the invalid ones to come to what roughly looks like the current state of the API. The APIs are all focused on the physics of a spring.

Note: if there's ever an oversight in my reasoning that results in my trimming a possibly valid API, please do point out!

Note 2: this is a simplified version. Feel free to ask questions on the unclear part.

Here are the specific criteria:

Please also point out if my criteria are too extreme and cover unnecessary use-cases.

1 Put style directly on children

1.1 Directly style on DOM components

1.1.1
<Spring>
  <div style={realStyle}>a</div>
</Spring>

Can't interpolate.

1.1.2
<Spring>
  <div style={{transform: `scale(${scalarValue}px, 0, 0)`}}>a</div>
</Spring>

Means scalarValue has to be in some configuration, in which case the constraint of styling directly on DOM components doesn't apply. Scratched. Generalized version below in point 2.

1.2 Use wrapper component

1.2.1

<Spring>
  <Thing style={top: interpolateValue(destinationValue, interpolationConfig)}
    <div>a</div>
  </Thing>
</Spring>

Thing wrapper provided by Spring, somehow. It'll interpolate from whatever current value (stored in Thing's state) to the destinationValue. Unfortunately, can't animate top unless Thing passes props to child div; next point.

1.2.2

<Spring>
  <Thing style={{transform: `scale(${interpolate(10)}px, 0, 0)`}}>b</Thing>
  <Thing style={{transform: {scale: [interpolate(10), 0, 0]}}}>b</Thing>
</Spring>

Compute Thing using HOC. Supports both flat list and tree children. How to do dependencies on currently transitioning values? Can't, unless with a dedicated binding API. Let's not do bindings for now?

2 Put style inside a configuration + let user manipulate and generate children

This implies the configuration is stored somewhere inside owner state, callback passed to generate children, context, etc. They're mostly equivalent (?).

2.1 Use wrapper component

<Spring>
  <Thing>
    <div style={configPart}>a</div>
  </Thing>
</Spring>

No need for wrapper. We can directly point to where we want to style.

2.2 No wrapper

2.2.1

<Spring destinationValue={prevTickTops => {a: {top: 1}, b: {top: 2}}}>
  {({a, b}) =>
    <div>
      <div style={a}>a</div>
      <div style={b}>b</div>
    </div>
  )}
</Spring>

Supports both flat list and tree children. Works with dependencies between final values: generate your data however you want before passing it to destinationValue. Doesn't work with dependencies between current values (this is the real criteria). You don't know what x's current interpolated value is until you pass the final structure to spring for the interpolation. The chat head demo cheats by using the previous tick's interpolated value (exposed here as prevTickTops, so there's actually a small delay. But maybe this is good enough for most cases? Can't control granularity. Scratched.

2.2.2 Above, take 2

<Spring destinationValue={prevTickTops => ({
  a: {top: {destValue: 1, springConfig: {stiffness: 10}}},
  b: {top: {destValue: 2, springConfig: {stiffness: 20}}}
})}>
  {({a, b}) =>
    <div>
      <div style={a}>a</div>
      <div style={b}>b</div>
    </div>
  )}
</Spring>

More or less what this library uses. This solves the previous granularity problem.

3 Mixin + state

3.3 tween-state

this.tweenState(['path', 'to', 'state'], config);

"Scratched" in the sense that this is already an established library. Mixins are getting deprecated so this won't be a viable solution in the future. Children functions (as seen above in 2.2.1 and 2.2.2) are a good alternative, which keeps the state inside the wrapper component instead of in the owner's state. The only caveat of keeping the state inside the wrapper is that others can't easily read into that state; you can still do this though:

<Wrapper>
  {interpolatedState => <div onClick={this.handleClick.bind(null, interpolatedState)} />}
</Wrapper>

Which is enough for most cases, from my experience.

dariocravero commented 9 years ago

Quick question, is there any reason why the call to rAF couldn't be disabled on componentDidUpdate and enabled when componentWillUpdate (apart from componentDidMount)?

chenglou commented 9 years ago

Not sure what you mean? Currently the rAF doesn't stop at all until unmount. But still, why disabling it on didUpdate?

dariocravero commented 9 years ago

Because it will re-render everything down the tree on every cycle and if I want the animation to be a once off, then it should stop right after it reached the end value, shouldn't it? I don't know if you remember about the panels question I asked you at the end of the conference regarding snapping, essentially, every panel renders custom things inside itself. If I'm surrounding them by Spring I get never-ending render calls to every one of its children :(. Am I trying to use the library in a way it wasn't intended?

chenglou commented 9 years ago

Yes, of course I will stop rAF-ing when I detect that the velocities have all gone to zero =). I just didn't do it now because there are other stuff to fix, simple as that. #12

dariocravero commented 9 years ago

:D perfect, thanks! Thought I was missing something there...

souporserious commented 9 years ago

Not sure if it's in the works, but I think it would be beneficial to take care of all vendor prefixing and allow users to pass values similar to these:

<Spring destinationValue={prevTickTops => ({
  a: {translateX: {destValue: 35, springConfig: {stiffness: 10}}},
  b: {scale: {destValue: 1, springConfig: {stiffness: 20}}}
})}>

This is taking inspiration from animation libraries like http://julian.com/research/velocity/. Not sure how the final API will look, but will there be wrappers that allow appear, enter, and leave animations like TransitionGroup does? I tried taking this on here with Velocity https://github.com/souporserious/velocitytransitiongroup. So being able to provide a map of values that should change on appear, maybe something like this:

<Spring destinationValue={prevTickTops => ({
  appear: {
    opacity: {destValue: 1, springConfig: {stiffness: 10}},
    translateY: {destValue: 0, springConfig: {stiffness: 10}},
  }
})}>

Also, what about being able to provide predefined animations that people could create and share. This was one reason I've been trying to get Velocity to play well with React. Loved that a set of animations could be created and reused/updated in one place.

chenglou commented 9 years ago

TransitionSpring works and is currently undocumented. Stay tuned =). There won't be a core built-in API for taking care of vendor prefixes, because I made sure doing so would be trivial:

<div style={...prefix('translate', [0, currentValues.x., 0]), top: otherValue}/>

This is trivial to implement. It returns an object that you'll spread onto your existing style. You generate these at the bottom level when you receive your interpolated values, instead of prescribing them into your (arbitrary) initial data structure. If there's enough demand I'll expose that as a helper.

Regarding predefined animation: is this enough? Create a new component that wraps around Spring by pre-setting some values for destinationValue. That should do what you want, right?

souporserious commented 9 years ago

Awesome. Yeah I was just thinking of giving users that don't want to set up anything and just add animations an easy way to do so. Creating a wrapper component for whatever predefined animations you need makes total sense though. Then you could just have custom props for whatever your animation is. Excited to see more docs on it :)

chenglou commented 9 years ago

Closed because not an issue. But will always welcome questioning of the general API here!