Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Web LA] Fill in missing keyframes in default animations #6259

Merged
merged 8 commits into from
Jul 11, 2024

Conversation

m-bert
Copy link
Contributor

@m-bert m-bert commented Jul 11, 2024

Summary

This PR adds missing keyframe steps definitions in default layout animations. While they are not necessary for animations to work, I've found some problems with it while implementing withInitialValues modifier.

It turns out if you do not specify a property in initial keyframe step and then you decide to change it in next keyframe steps, it may not work.

Note

Example below was written in pure HTML and CSS on W3Schools Tryit Editor. However, since this PR is about web layout animations, it applies to them in the same manner as to animations in HTML and CSS.

Initial state

Consider this example:

#box {
  width: 100px;
  height: 100px;
  background: red;
  animation: myAnimation 2s;
}

@keyframes myAnimation {
  100% {
    opacity: 0;
    transform: rotate(5rad) scale(0);
   }
}

In this case, div with id box will perform PinwheelOut animation:

Nagranie.z.ekranu.2024-07-11.o.14.16.50.mov

Adding initial values

Now, suppose that we want to use withInitialValues modifier and add scale: 2. This will result in the following keyframe:

@keyframes myAnimation {
  0% {
    transform: scale(2);
  }
  100% {
    opacity: 0;
    transform: rotate(5rad) scale(0);
   }
}

Now the animation looks like this:

Nagranie.z.ekranu.2024-07-11.o.14.17.45.mov

This animation is broken for two reasons:

  1. There is no rotation applied to the component
  2. There is a moment when component simply disappears, instead of smoothly changing opacity from 1 to 0

Filling remaining properties

Now, if we fill in remaining keyframe properties, it will look like this:

@keyframes myAnimation {
  0% {
    opacity: 1;
    transform: rotate(0rad) scale(2);
  }
  100% {
    opacity: 0;
    transform: rotate(5rad) scale(0);
   }
}

And the resulting animation looks like this:

Nagranie.z.ekranu.2024-07-11.o.14.15.34.mov

Order of properties

Caution

Order of properties between keyframe steps does matter!

It is obvious that order of operations inside transform matters. However, it is also important to keep order of those operations between keyframes. In the example above, if we switch rotation with scale like this:

@keyframes myAnimation {
  0% {
    opacity: 1;
    transform: scale(2) rotate(0rad);
  }
  100% {
    opacity: 0;
    transform: rotate(5rad) scale(0);
   }
}

The animation will look like this:

Nagranie.z.ekranu.2024-07-11.o.14.23.15.mov

So even though all properties were provided, we still do not get proper animation.

Potential issues

Problem described in the Order of properties paragraph may lead to some problems, since layout animations on web are created dynamically from objects that contain keyframe description. While mixing order of keyframe steps doesn't change anything (you can for example specify to before from and it will work correctly), order inside transform does matter. This may be problematic because we are iterating over objects using Object.entries method. However, up to this point, no issues were reported.

Test plan

Tested on Default Layout Animations example.

@m-bert m-bert added this pull request to the merge queue Jul 11, 2024
Merged via the queue into main with commit 78f2263 Jul 11, 2024
7 checks passed
@m-bert m-bert deleted the @mbert/add-initial-keyframes branch July 11, 2024 13:15
Copy link
Member

@piaskowyk piaskowyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

luks 👍

github-merge-queue bot pushed a commit that referenced this pull request Jul 15, 2024
## Summary

This PR adds `withInitialValues` into web layout animations.

> [!NOTE]
> This implementation required #6259. You can find more information in
mentioned PR.

## Implementation

Implementation is based on modifying first step in `keyframe`. First we
obtain default animation style and create its copy using
`structuredClone`. We also add `px` suffix into `translate` and
`perspective` in `initialValues`. Then we can modify styles of
animation.

### `transform`

Here situation is more complicated. First, if `transform` was not
specified in the predefined styles, we can simply assign to it the one
provided in `initialValues`. Otherwise we have to merge incoming
transform with predefined one.

To do this, we use `Map` that will contain final `transform`. First we
iterate through predefined transforms, adding each property with its
value. Then we perform second iteration, this time on `transform` from
`initialValues`. In that case we will either add new property, or
override existing one. Finally we convert `Map` back to array of objects
using `Array.from` method.

### Other props

Since layout animations on web do not change other props directly, we
can simply assign them into styles object.

> [!NOTE]
> Animation that has `withInitialValues` modifier is treated like a
custom animation and undergoes whole process of adding it into `DOM` and
removing it when it finishes.

## Example

<details>
<summary>Animation code</summary>

```tsx
<Animated.View
  entering={FadeIn.withInitialValues({
    transform: [{ scale: 0.5 }, { skewX: '-35deg' }],
  })}
  exiting={FadeOut.duration(500).withInitialValues({
    opacity: 0.75,
    transform: [{ scale: 2 }, { rotateY: '-7rad' }, { skewX: '35deg' }],
  })}
  style={styles.box}
/>
```

</details>


https://github.com/software-mansion/react-native-reanimated/assets/63123542/929bdf05-b3d0-45f6-92e5-4501b893bff7


## Test plan

<details>
<summary>Tested on the following code</summary>

```tsx
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { Button, StyleSheet, View } from 'react-native';

import React from 'react';

export default function BasicLayoutAnimation() {
  const [visible, setVisible] = React.useState(true);

  return (
    <View style={styles.container}>
      <Button onPress={() => setVisible(!visible)} title="Create/Remove" />
      {visible && (
        <Animated.View
          entering={FadeIn.withInitialValues({
            transform: [{ scale: 0.5 }, { skewX: '-35deg' }],
          })}
          exiting={FadeOut.duration(500).withInitialValues({
            opacity: 0.75,
            transform: [{ scale: 2 }, { rotateY: '-7rad' }, { skewX: '35deg' }],
          })}
          style={styles.box}
        />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    marginTop: 300,
  },
  box: {
    width: 100,
    height: 100,
    backgroundColor: '#b58df1',
    borderRadius: 10,

    marginTop: 20,
  },
});
```

</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants