motiondivision / motion

A modern animation library for React and JavaScript
https://motion.dev
MIT License
25.87k stars 851 forks source link

[FEATURE] Defining transitions between props #1725

Open mattgperry opened 2 years ago

mattgperry commented 2 years ago

There's a bit of a blind spot in the API at the moment where if you take

<motion.div
  initial={{ scale: 0 }}
  animate={{ scale: 1, transition: { delay: 1 } }}
  whileHover={{ scale: 2 }}
/>

For instance you can see that when we re-enter the animate state there'll be a delay applied. When really the delay naturally applies to the initial animation.

It'd be good if we could figure out a way to define a transition that applies just on the initial animation. Perhaps there's also value in being able to define specific transitions more generally, like whileHover -> animate?

<motion.div
  initial={{ scale: 0 }}
  animate={{ scale: 1, transition: { delay: 1 } }}
  whileHover={{ scale: 2 }}
  transition={{
    initial -> animate: {},
    whileHover -> animate: {}
  }}
/>

Or

<motion.div
  initial={{ scale: 0 }}
  animate={{
    scale: 1, 
    transitionFrom: {
      initial: { delay: 1 },
      whileHover: { type: "spring" }
    }
  }
  whileHover={{
    scale: 2
  }}
/>
akd-io commented 2 years ago

I've been thinking about this a bit today and wanted to share in case my thoughts aren't obvious.

Please take this with a grain of salt! I'm by no means an expert here, I'm certain you, Matt, know better than anyone. I also know maintaining OS is hard work. I can't even keep a single project afloat.

I just thought it might be fun to chime in and be a little part of some API design which I find fascinating.

Transitions as edges in a graph

To me, transitions are a phenomenon that inherently appears between states (variants), and therefore I believe it might make sense to model it likewise and declare transitions outside of variant declarations. Think of it like a finite state machine; a graph where states (variants) are nodes and transitions are the edges. In this sense it is arbitrary that the current implementation of transition implies an any variant -> current variant relationship.

In your first example, you provided pseudo code for such syntax, specifically:

...
transition={{
  initial -> animate: {},
  whileHover -> animate: {}
}}
...

Syntactic sugar for variants

Because I'm guessing it can become too verbose to declare the transitions in the previously described manner, I think it makes sense to add syntactic sugar to variants to handle the most frequent transition use cases.

You proposed such syntactic sugar in your second example, specifically:

...
animate={{
  scale: 1, 
  transitionFrom: {
    initial: { delay: 1 },
    whileHover: { type: "spring" }
  }
}
...

With transitionFrom, though, you might also want to add transitionTo. This would provide the following APIs:

But by the symmetry of these relationships, you might notice that there is no corresponding inverse of transition with the relation current variation -> any variant. As such you might introduce transitionToAny, whose name highlights the fact that transition has really been transitionFromAny in disguise all along.

That would leave us with:

I would personally just implement a * wildcard to represent any variant, though, and stick to:

transitions syntax

Here I would like to note the few obvious syntaxes I've thought of declaring transitions separately from variants.

Verbose object notation

<motion.div
  ...
  transitions={
    [
      {
        from: 'variant1',
        to: 'variant2',
        transition: {
          delay: 0.5,
        },
      },
      {
        from: 'variant2',
        to: '*', // `*` acts as a wildcard for any variant.
        transition: {
          delay: 1,
        },
      },
      {
        // Not specifying `from` implicitly sets it to `*`
        to: 'variant3',
        transition: {
          delay: 2,
        },
      },
      {
        from: 'variant4',
        // Not specifying `to` implicitly sets it to `*`
        transition: {
          delay: 4,
        },
      },
    ],
  }
/>

Easy to read and understand, but verbose.

As shown, specifying only one of from and to implies the other is a wildcard.

Concise object notation using arrays

<motion.div
  ...
  transitions={[
    ['variant1', 'variant2', {
      delay: 0.5,
    }],
    ['variant2', '*', {
      delay: 1,
    }],
    ['*', 'variant3', {
      delay: 2,
    }],
  ]}
/>;

Short and fairly easy to read. Unfortunately, Prettier will mess this one up big time if variations aren't written as one-liners. 🙁

You could choose to allow specifying one variant only, and imply it as from or to, but I'd vote against it for readability reasons.

A useful helper function

No matter the syntax, a simple helper function would clean it up a lot:

const makeTransition = (from, to, transition) => ({ from, to, transition });

const MyComponent = () => {
  return (
    <motion.div
      ...
      transitions={[
        makeTransition("hidden", "visible", { duration: 1 }),
        makeTransition("visible", "hidden", {
          duration: 2,
          delay: 10,
          ease: "easeInOut",
        }),
      ]}
    />
  );
};

This helper function plays nicely with Prettier and also makes the code very readable if you are using VS Code's Inlay Hints that show the names of parameters. In any case it will show in the verbosity the user has set in their preferences.

Screenshot 2022-10-04 at 23 10 59

I assume you wouldn't want to surface such a helper function from the API, though. Maybe it could be added to the docs.

Precedence

It would be necessary to take a look at precedence in these syntaxes.

Wildcard vs. non-wildcard

Non-wildcard relations should likely always override wildcard relations.

Eg.: variant1 -> variant2 would take precedence over variant1 -> * and * -> variant2

Wildcard vs. wildcard

A transition that is specified both via a wildcard at the source and via a wildcard at the destination could take into account both the position of the transition in a transitions array and whether the wildcard is on the source or the destination.

Eg.:

Finally

Again, please take this with a grain of salt and have a great day 😊

mattgperry commented 2 years ago

Amazing post! Thanks for help thinking this through.

On transition, transitionFrom etc... I think just these two would be enough. transitionTo and transitionFrom creates a potential conflict and the current paradigm is that the variant being entered is the one that defines the transition used. This is similar to CSS. transitionFrom would be enough to then further designate a specific transition to use for this variant combination.

Ideally we'd add as little API as possible to achieve our aims and I think just this would be enough, vs transitions which could itself get quite verbose.

There's also the added wrinkle of prop names/gestures vs variants.

Prop names, useful with or without variants

<motion.div
  animate={{
    scale: 1,
    transitionFrom: {
      whileHover: { duration: 1 }
    }
  }}
  whileHover={{
    scale: 2
  }}
/>

Or from variant names, only useful with variants

<motion.div
  variants={{
    enter: /** **/,
    exit: {
      transitionFrom: {
        enter: { duration: 1 }
      }
    }
  }}
  whileHover={{
    scale: 2
  }}
/>

Supporting both might be quite difficult though I'll take a look into this as I can see the use of both.

akd-io commented 2 years ago

Makes total sense to try to add as small changes to the API as possible, and you're totally correct it gets more complicated than I outlined when opening the whole transitions can of worms.

transitionFrom would be enough to cover our needs, and I assume other people's.

Would be wonderful to see transitionFrom implemented 👏

akd-io commented 6 months ago

Hey @mattgperry, just hit this problem again, and saw you have been working on this, but closed #2332 a couple weeks ago.

Did you find a blocker that prevents us from implementing transitionForm?

Let me know if not and you think I should give it a shot.

Edit: Is it revived here? #2643