pmndrs / react-spring

✌️ A spring physics based React animation library
http://www.react-spring.dev/
MIT License
27.81k stars 1.18k forks source link

FLIP-style animations #9

Open techniq opened 6 years ago

techniq commented 6 years ago

Discussed briefly in https://github.com/drcmda/react-spring/issues/5#issuecomment-378661614, but it would be nice if react-spring could calculate the to/from states if not explicitly provided, similar to react-flip-move which uses the FLIP technique (ultimately getBoundingClientRect)

For example, in react-move you can do this (just by changing the class/styles on the items, they "react")

<FlipMove className="items"
  duration={350}
  staggerDurationBy={20}
  staggerDelayBy={20}
>
  {items.map(item => (
    <div
      className={item.key === selected ? "item selected" : "item"}
      onClick={() => this.selectItem(item.key)}
      key={item.key}>{item.key}</div>
  ))}
</FlipMove>

Here is the discussion I had with the author of react-flip-move while creating this - https://github.com/joshwcomeau/react-flip-move/issues/69

A similar thing could probably be done like react-morph which is useful to morph shared elements between route states (like Android does a good bit with it's material design

Maybe if you do not provide a from/to to <Transition> it calculates the top/left/width/height automatically (or just diffs the styles and lets the spring transitions them). With flip you apply them, capture the styles, then invert them.

Anyways, I don't have any specific need right now, but thought it was worth having a conversation. Thanks again for the awesome library.

drcmda commented 6 years ago

@techniq i fixed some bugs today in the old transition implementation so that it allows for endless elements to fade out even if elements with the same key come in again. And when that worked it had me thinking, isn't that behavior all we need for morph/flip? I mean at the lowest level.

For instance this demo: https://codesandbox.io/s/n8797r26l

Of course it would need a primitive now that uses Transition to make it easier to define a "screen", mark elements that bear a relation and distribute values between them so that they can move towards one another. Have to think a little bit how an api would look like - i've seen react-morph but there must be an easier, more intuitive way ...

natew commented 5 years ago

@drcmda this would be very interesting. Your demo went down. Shouldn't we keep the ticket open though given it's a valid concern? I'd love this feature.

dbismut commented 4 years ago

Moving conversation on FLIP from #642 to here.


Message from @ShanonJackson on December 14:

@dbismut @aleclarson

Yeah i do know about react-use-gesture we use it heavily at the moment to create multi column drag-and-drop. Love the library.

This is going to be a long detailed post just around spit balling ideas i have a lot of these but these are just a few.

Feature: Layout Animation

Framer Motion has positionTransition which is a prop they've added to their own animated.div style component, with it you can do animation between layout states without "math"; By math based layouts i mean https://codesandbox.io/s/20yw31pyxj?from-embed where each sibling is position with translate3d + absolute, the problem with positioning elements this way is that you essentially have to write all the code yourself in the parent to do css's job for it without access to everything css has access to. I.E Try doing a math based layout where each item has a dynamic height.

This previously would be very very hard to achieve with react but now with useLayoutEffect it becomes a lot easier in-fact you can implement it with react-spring in very little code here's a terrible demo i threw together for you https://codesandbox.io/s/drag-and-drop-spring-j2r9j

Problems with this implementation (Framer Motion shares this problem):

How to fix it:

Create a component that controls transitions between single and multi layouts, it renders nothing but its children wrapped in a context in which they store their state to persist it between remounts.

How it works:

Why is this useful

Feature: animate auto width and auto height with hooks Currently, there's some issue floating around somewhere where the react-spring says that because hooks don't control the render phase animations to "auto" couldn't be implemented and will remain solely as a feature of the render props API.

This is true, however work arounds are possible i'll leave it up to peoples imagination as to what the API should actually look like but just an idea of how this can be achieved is something like this...

const AnimateAuto = () => {
        const auto = useSpring({width: 0,  to: {width: "auto"}});
        return auto((styles) => {
                <animated.div style={styles}>
                </animated.div>
         })
}

(where auto is implemented as wrapping the children in a fragment, and injecting into that fragment that fixAuto stuff) Obvious issues: useSpring currently doesn't return a function, i'm just merely using it as a hypothetical of how it could be achieved with hooks.

Why?

I have a lot more ideas and would love to share our actual internal code to show you where our pain points are at the moment with react-spring. I absolutely adore the library i just want to reiterate on that fact as i know my earlier post could be seen as an attack but really i just want this library to flourish.

Here is a snapshot of our component library from 1 year ago, we're about to open source an incredibly polished version in January, just posting it here to give you guys an idea of how we use react-spring.

http://qrious-storybook.s3-website-ap-southeast-2.amazonaws.com/

Wish i had a deployment floating around of our Component Library today to show how heavily we use it now as apposed to 1 year ago. DatatableEdit would shrink its source code by about 70% if we didn't have to calculate all that position math for drag and drop.


Another message from @ShanonJackson after I mention react-flip-toolkit:

I have used the old "auto" animation stuff in react-spring and based on my experience trying a lot of other methods out there react-spring is the most reliable (by other methods i mean resize observer polyfill and react-resize-aware).

Now after you've linked that i understand what you mean by the FLIP technique; No i haven't tried that library it looks interesting from a sense that their using transform: matrix instead of translate3d i'm assuming its because matrix performs better. However they're also storing state on the DOM elements themselves with data- properties which is extremely dirty. Also after a quick skim read of the source they're also not leveraging useLayoutEffect which makes layout stuff alot more trivial.

Would be great if this was built into react-spring as i feel like layout animations are an extremely common use case of this library.

dbismut commented 4 years ago

it looks interesting from a sense that their using transform: matrix instead of translate3d i'm assuming its because matrix performs better

@ShanonJackson I'm no expert but FLIP is not only used to move objects, but also to efficiently scale objects. matrix allows to pass all transforms in one line.

As I was saying in the other thread, we are going to investigate this, but it's going to take some time as it's a tricky issue if we want to make it work in complex use cases without automagic.

ShanonJackson commented 4 years ago

Definitely investigate, like i mentioned though a solution in which "lastPosition" isn't stored in state but rather in context in a higher parent will allow multi-column transitions (by animating fixed relative to the body or absolute relative to the body and in a portal to avoid position: relative parents). In combination with useLayoutEffect.

The simple sandbox i posted above shows how it can already be possible in react-spring but is still abit buggy.

I don't know if anyone is actually asking for scale objects, i definitely don't need this as part of my use case and in my mind it makes the entire solution more complicated because then you have to apply inverse scale to children to offset the scaled parent which is extremely dirty. (Not to mention all the other edge cases that come with transform: scale like the fact the box model will remain as the unscaled box model unless you calculate negative margins to normalize the box model to the scaled value).

Absolutely agree that it is a tricky issue and providing a fluid API that covers all the edge cases that come with a feature like this is going to be hard but i feel like it will be a compelling edition to the library and realistically can be implemented by combining primitives that already exist in react-spring.

satvikpendem commented 4 years ago

Have you guys seen how framer-motion handles this? I just watched this talk by Matt Perry, who made Popmotion, Pose and now is working on framer-motion, who outlines how the feature could kind of work, or what having a more declarative API would be like: https://www.youtube.com/watch?v=BSbVB14riQI

As well, I think framer-motion has some cool primitives that could be added into react-spring (or react-use-gesture, not sure yet), such as whileTap, whileHover, which I think currently you'd need to manually set onMouseEnter, onMouseLeave, as noted at 1:15 in the video above. It might be worth going through the Framer Motion API to take a look at any good ideas that could be transferred to react-spring in a react-spring way.

dbismut commented 4 years ago

@satvikpendem Hey. Yes we are aware of framer motion and yes this lib does things with a different API approach. The talk you’ve linked to is over a year old and before framer motion even came out. I believe framer motion is indeed the answer to what Matt is saying.

As you hinted, React use gesture exists in the react spring family precisely for that purpose: simplifying how to handle events using hooks. What you’re describing can easily be achieved with useHover.

React-spring doesn’t deal with human interactions and I don’t think it will ever do.

All in all framer motion is a fantastic lib. It is definitely simpler to grasp than the react spring ecosystem because it binds interaction and animation internally.

React spring and react use gesture gives you more control over your logic.

Edit: oops mistakingly closed the issue from my phone, I guess both GitHub CTA are too close from each other.

satvikpendem commented 4 years ago

My apologies David, I think my comment comes across as more flippant and careless than I had intended it to. I am sure of course you all know about the framer motion library, didn't mean to imply you didn't, just wanted to put it out there. I also just now saw the useHover hook, I should read the docs more thoroughly. Is there a separate website for the docs or is it all in the README for now?

dbismut commented 4 years ago

It’s just a Readme for now but I’m actively working on a separate documentation website for v7. I should be able to release it shortly in a beta state (within 24h).

ShanonJackson commented 4 years ago

Definitely off-topic but yes iv'e seen how framer motion handles this and like i said they handle it through internal state by storing the last position in memory; They then leverage keys to make sure the component is not remounted and therefore last position is not lost.

The problem with this strategy as mentioned above are that your element can not change parents otherwise it will disregard the key and remount and therefore not animate.

I'm proposing if we do this feature in react-spring we do it with a overall wrapping context say <Layout> and that it's only purpose is to store positions of layout animated children.

When a child moves, it triggers useLayoutEffect in which it asks its useContext(LayoutContext) for its last position and uses getBoundingClientRect for its new position. It then animates relative to the BODY from its old to its new position.

This strategy allows for multi-column, multi-layout transitions. Or in short transition from 1 position in the DOM to any other position in the DOM.

I put together a simple sandbox in which i showed how this is possible in react-spring but it only stores lastPosition in state and not in context.

https://codesandbox.io/s/drag-and-drop-spring-j2r9j

Some just stuff i'd like to add that is obvious is that. It may be wise to animate inside a ReactDOM Portal until the animation finishes, as this will avoid any overflow: hidden and position: relative parents that may block animation from occurring naturally

dbismut commented 4 years ago

@ShanonJackson this is now definitely on topic, since we are precisely discussing FLIP. I’d be curious to see where framer motion uses useLayoutEffect? As far as I know it uses getSnapshotBeforeUpdate which makes sure you get the latest position of your component (right before the render cycle).

The problem with storing the last position with useLayoutEffect is that it might change as a side effect resulting in the stored position being unsynced.

Anyway there is this lib that you might know that uses some of the principles you mentioned: https://github.com/jlkiri/react-easy-flip

ShanonJackson commented 4 years ago

Mmmm good point iv'e migrated so deep into hooks iv'e forgotten about getSnapshotBeforeUpdate; You're actually very right i only skim read react-motion's source code and found that useLayoutEffect in "use-layout-sync.ts" and didn't track it any further assuming that's how they were doing it. However on further study of the source they do indeed use getSnapshotBeforeUpdate.

getSnapshotBeforeUpdate will definitely be easier to implement because of side-effect control as you mentioned its very easy to end up in an infinite loop or a closure with useLayoutEffect which can be avoided using useRef. getSnapshotBeforeUpdate wont have these issues.

react-easy-flip looks good, they do it in 850B (minified and gzipped) which is really impressive. However they're using standard css physics (css transitions), and dirty manual DOM manipulation (appending styles dynamically to the dom reference). Didn't check for support for multi-column position transitions but i'm going to assume they can't because they're checking for children of the ref''ed node.

We can definitely do better with getSnapshotBeforeUpdate + store positions in context for multi-column + spring physics; would also be nice to add the standard spring API stuff in like "immediate" etc because there's going to be solid use-cases where people don't want layout animation in certain cases.

EDIT: Might throw together a quick implementation with getSnapshot, context, multi column DnD etc over christmas break

EDIT2: Didn't want to create a new post. Finished building the multi column transitions in a few lines of code but just thinking of building in a drag-and-drop api where position is handled completely for you.

EDIT3: https://giphy.com/gifs/h8gGCYKXgslqwnqvCw

aleclarson commented 4 years ago

There's been more discussion of the possible API over here: https://gist.github.com/aholachek/32d0aef7fce6a5523dec238714e0d23b

No estimate on when any code will be written, though.

jlkiri commented 4 years ago

react-easy-flip author here. Since 4.0 (which is WIP at the moment) instead of dirty DOM manipulations (= setting transforms directly) the library will use WAAPI. The refs are not used anymore and data-flip-id is used instead. Multi-column (shared layout) transitions are possible because the useFlip hook queries the data-flip-id, so even if an element with a certain data-flip-id is unmounted from one position but mounted at some other position, it can be animated.

It will be around 1.7KB however and still no spring physics. Actually I would love to use spring physics but really want to keep the size low. If anyone has good ideas - please share :)

morgs32 commented 3 years ago

I ran into issues with the other contenders react-easy-flip and react-flip-toolkit (such as FLIP animations inside components with perspective 3d transforms). We went with this solution: react-spring-flip. It's really not a lot of code and you could copy/fork it to do something custom. Or make an enhancement request - we're waiting for feature requests. If you're already using react-flip this library is tiny.