chenglou / react-motion

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

Interpolator function #153

Open akre54 opened 9 years ago

akre54 commented 9 years ago

I am trying to animate an SVG path element's d attribute using d3.interpolate. It looks like react-motion only supports numerical values at the moment, and it also looks like there isn't a way to set a custom interpolator function. Any plants to fix either of these?

chenglou commented 9 years ago

Yep! The next next version will recognize special key names in your object, such as height and [insert your whatever path attribute here]. height, for example, will interpolate in the domain of integers since browsers round the number anyway. Got an API in mind for path?

akre54 commented 9 years ago

Awesome to hear! I was thinking more like an arbitrary value interpolator, so that as long as you return the values in a way your own code can use you'd be fine.

<path>'s d attribute takes a string in the form "M50,100...", where the numbers are distinct values meant to be read separately. I'd like to be able to use d3.interpolateString to interpolate each value piecewise. This could work similar for 12px -> 24px or other united strings.

Now either react-motion could add its own interpolateString function, or it could allow the interpolator to be overridden, but by default use interpolateNumber behind the scenes. My preference is towards the latter.

chenglou commented 9 years ago

cc @threepointone who's interested in this.

akre54 commented 9 years ago

I can try to whip something up on Monday if nobody gets to it before then.

Basically I'd like:

render() {
  var prevD = line(this.state.prevData);
  var d = line(this.props.data);

  <Motion defaultValue={{val: prevD}} endValue={{val: d}} interpolator={d3.interpolateString}>
    {interpolated => <path d={interpolated.value} />
  </Motion>
}
akre54 commented 9 years ago

The tests on master appear to be broken, but would something like this work?

diff --git a/src/components.js b/src/components.js
index 3e50106..23a3eb9 100644
--- a/src/components.js
+++ b/src/components.js
@@ -151,6 +151,12 @@ export default function components(React) {
       children: PropTypes.func,
     },

+    getDefaultProps() {
+      return {
+        interpolate: interpolateValue,
+      }
+    },
+
     getInitialState() {
       const {defaultStyle, style} = this.props;
       const currentStyle = defaultStyle || style;
@@ -195,13 +201,14 @@ export default function components(React) {
     },

     animationRender(alpha, nextState, prevState) {
+      const {interpolate} = this.props;
       // `this.hasUnmounted` might be true in the following condition:
       // user does some checks in `style` and calls an owner handler
       // owner sets state in the callback, triggering a re-render
       // unmounts Motion
       if (!this.hasUnmounted) {
         this.setState({
-          currentStyle: interpolateValue(
+          currentStyle: interpolate(
             alpha,
             nextState.currentStyle,
             prevState.currentStyle,
@@ -242,6 +249,7 @@ export default function components(React) {
       return {
         willEnter: (key, value) => value,
         willLeave: () => null,
+        interpolate: interpolateValue,
       };
     },

@@ -297,10 +305,11 @@ export default function components(React) {
     },

     animationRender(alpha, nextState, prevState) {
+      const {interpolate} = this.props;
       // See comment in Motion.
       if (!this.hasUnmounted) {
         this.setState({
-          currentStyles: interpolateValue(
+          currentStyles: interpolate(
             alpha,
             nextState.currentStyles,
             prevState.currentStyles,
chenglou commented 9 years ago

(Tests on master should be passing btw, just checked)

akre54 commented 9 years ago

Ah ok, I just pulled down the latest. I'll add some tests then send a pull in a sec.

I'm having a hard time wrapping my head around the val and currentStyles parts of the API. Why not have transitions simply be generic interpolators capable of transitioning between arbitrary values?

ccblaisdell commented 8 years ago

@akre54 Did you manage to get custom interpolators working, or did you decide to go a different direction?

hnqso commented 8 years ago

@akre54 as @ccblaisdell mention also keen to know if you found a solution.

akre54 commented 8 years ago

I started changing some things around and got into the weeds before just falling back to using d3 for child animations, which has some pretty severe drawbacks.

We're still really interested in this (mostly for animating between reparented components), but I haven't had time in a while to look into it.

hnqso commented 8 years ago

Thanks for the update @akre54 👍

jstcki commented 8 years ago

You can use custom interpolators just fine with React Motion by treating styles as arbitrary values (I'm not a fan of the term styles because of this). For example: http://bl.ocks.org/herrstucki/27dc76b6f8411b4725bb. Of course it would be nice if RM would support them directly.

Also, interpolators should be specific to each style property. One interpolator per component wouldn't work. I could imagine two ways:

a) A map of interpolators:

<Motion
  defaultStyle={{color: '#f00'}}
  style={{
    color: spring('#0f0')
  }}
  interpolate={{
    color: interpolateHcl
  }}
>{...}</Motion>

b) or as part of the spring config:

<Motion
  defaultStyle={{color: '#f00'}}
  style={{
    color: spring('#0f0', {
      interpolate: interpolateHcl
    })
  }}
>{...}</Motion>
akre54 commented 8 years ago

I prefer the second way (that way it can be extracted into a wrapper function or composed).

mrasoahaingo commented 7 years ago

Hi, any update about interpolating SVG path? Thanks a lot!

tannerlinsley commented 7 years ago

This would be great to have. I think I would lean way more towards an interpolate key for spring options.

I would also wager that most people would love to be able to just drop in compatible interpolators as plugins at a higher level and have them work out of the box. D3 has tons of interpolators available, we could honestly just make an i/o to utilize them.

As easy as it would be to provide the interpolator per spring config, it would be so much cooler if we were able to handle interpolators automagically. Imagine utilizing a similar algorithm as https://github.com/d3/d3-interpolate#interpolate, using d3 interpolate itself haha!

Thoughts? This is a large weakness of React-Motion that is talked about most often. If we could nail this, I think you could have a lot of people considering its usage in many other scenarios.

tannerlinsley commented 7 years ago

Am I right to assume that we would need to build a standardized interpolator interface to replace the operations located here: https://github.com/chenglou/react-motion/blob/master/src/Motion.js#L173?

tannerlinsley commented 7 years ago

I was able to work up a prototype abstraction of <Motion /> to work with d3-interpolators. By no means is it optimized, but it's a pretty cool start. It works by mapping non-numeric value changes to auto-incrementing integers, then it coerces those integers back to a percentage between the old and new value. Then it applies the d3-interpolator of choice behind the scenes, defaulting to the standard auto interpolator.

I would love to get some feedback on this, as it seems like this could be a start to supporting non-numeric values.

Right now I'm using it to interpolate colors, paths, and strings in d3.

(Update to support interrupted updates)

import React from 'react'
import { Motion, spring } from 'react-motion'
import { interpolate } from 'd3-interpolate'

export default React.createClass({
  oldValues: {},
  newInters: {},
  currentStepValues: {},
  stepValues: {},
  stepInterpolators: {},
  render () {
    const {
      style,
      children,
      ...rest
    } = this.props

    const MagicSpring = (value, config) => {
      if (typeof value !== 'number') {
        return {
          value,
          config,
          interpolator: (config && config.interpolator) ? config.interpolator : interpolate
        }
      }
      return spring(value, config)
    }

    const resolvedStyle = style(MagicSpring)
    for (let key in resolvedStyle) {
      if (
        // If key is a non-numeric interpolation
        resolvedStyle[key] &&
        resolvedStyle[key].interpolator
      ) {
        // Make sure the steps start at 0
        this.currentStepValues[key] = this.currentStepValues[key] || 0
        if (
          // And the value has changed
          typeof this.newInters[key] === 'undefined' ||
          resolvedStyle[key].value !== this.newInters[key].value
        ) {
          // Save the new value
          this.newInters[key] = resolvedStyle[key]

          // Increment the stepInterValue for this key by 1
          this.stepValues[key] = this.currentStepValues[key] + 1

          // Set up the new interpolator
          this.stepInterpolators[key] = this.newInters[key].interpolator(
            this.oldValues[key],
            this.newInters[key].value
          )
        }
        // Return the spring with the destination stepValue and spring config
        resolvedStyle[key] = spring(this.stepValues[key], this.newInters[key].config)
        // console.log(resolvedStyle[key])
      }
    }

    return (
      <Motion
        {...rest}
        style={resolvedStyle}
      >
        {values => {
          const newValues = {}
          for (let key in values) {
            if (this.stepValues[key]) {
              // Save the currentStepValue
              this.currentStepValues[key] = values[key]
              // Figure the percentage
              let percentage = this.currentStepValues[key] - this.stepValues[key] + 1
              // Save the current value and replace the value in the interpolated object
              this.oldValues[key] = newValues[key] = this.stepInterpolators[key](percentage)
            }
          }
          return children({
            ...values,
            ...newValues
          })
        }}
      </Motion>
    )
  }
})

mar-07-2017 16-50-48

souporserious commented 7 years ago

@tannerlinsley that looks amazing! @chenglou is that something you would accept as a start to a PR? This seems extremely powerful. I'm willing to help where I can 😁

tannerlinsley commented 7 years ago

I've just updated my snippet above to support interrupted updates. All this means is that it should function exactly like react motion does, it will always shift inertia towards the new value from where the current value is :)

tannerlinsley commented 7 years ago

@chenglou, honestly it would be so nice to have access to the percentage from the last issued float to the destination float. The wrapper above works pretty well for the simple <Motion /> component, but quickly becomes unwieldy if the same concept is applied to <TransitionMotion />. There is so much duplicate tracking of values that it seems like a fair ask to include in the core. Really, anything that would expose more flexibility. Thoughts?

chenglou commented 7 years ago

Sorry super busy right now. Leaving a comment here. Will check again next week.

tannerlinsley commented 7 years ago

I got really stuck working on adding interpolation to the TransitionMotion component via the wrapper before I had to dig deeper into the source. The architecture I needed to achieve this flexible interpolation took me on a journey that ended up porting as much of the animation cycle and physics stepper over to a new repo called react-move. Anyone interested can take a look at the source there. It supports interpolating anything that d3 can handle, duration/easing motion, and staggering. It does not yet support chain staggering (eg. the chat heads example) yet. It also does not yet account fo velocity reversal for numbers, but should soon. @chenglou, we should chat very soon.