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

Animations #11

Closed
dariocravero opened this issue Nov 10, 2017 · 7 comments
Closed

Animations #11

dariocravero opened this issue Nov 10, 2017 · 7 comments

Comments

@dariocravero
Copy link

dariocravero commented Nov 10, 2017

Here's a summary of the first session on animations we did with @tomatuxtemple:

We want animations in Views to be easy to compose. As with any other feature of the language, the morpher should take care of doing the best animation possible for web and native while using the same code to produce it. It's a big challenge, in particular because springs don't exist on CSS transitions but we need to start somewhere, so let's iterate on a first version that gets animations in and we can improve the morphing later.

As a first step, we want to introduce animations in block scopes.

Animations will be added to a prop at the end of the value like:
color red linear duration 150

We'd like to use named values so that you don't need to mind the order. These are the values we want to support:

  • curve (required), possible values spring, linear, ease-out, ease-in, etc. We don't need a specific key for the curve because it's already a word.
  • order (defaults to 0, meaning all values animate at the same time). Takes a number specifying the order in which the animation is supposed to trigger. The rest of the values in the same scope should wait for it to be done before starting.
  • delay (defaults to 0, meaning all values animate at the same time). Takes a number in milliseconds specifying how much time the animation should wait to be triggered. It applies after the order, ie, if you set order 1, it will wait for values in order 0 to be done and its own delay.
  • the curve's configuration:
    • for spring (taken from React Native's Animated docs):
      • stiffness (defaults to 100).
      • damping *defaults to 10) defines how the spring’s motion should be damped due to the forces of friction.
      • bounciness (defaults to 8)
      • speed (defaults to 12).
    • for timing curves like linear, ease-out, etc:
      • duration defaults to 150 milliseconds.

The parser would have to clear out any animation values off the prop and attach those as metadata to it. This is a good point to get started with. We do all that parsing in the helpers.

We could make the curve the first parameter that tells the beginning of the animation. So if you have a complex prop like boxShadow there's no issue to parse it, eg: boxShadow 0 10px 10px red spring friction 10. Even in that complex case we know where the animation starts.

Then the morpher would generate the code we see below.
For React Native, see:

Here's the example view we use on the video (we replaced timing for duration after recording the video).

Web example:

Hey Text
color #323232
fontFamily Montserrat
fontSize 18
fontWeight 400
text Type something...
when props.isOn
color red ease-out
fontSize 28 ease-out

FakeProps
isOn true
import React from 'react';
import styled from 'react-emotion';
import PropTypes from 'prop-types';

const Hey1 = styled('span')(
  {
    fontFamily: 'Montserrat, sans-serif',
    fontWeight: 400,
    willChange: 'color, font-size',
    transition: 'color 150ms ease-out, font-size 150ms ease-out'
  },
  ({ props }) => ({
    color: props.isOn ? 'red' : '#323232',
    fontSize: props.isOn ? 28 : 18,
  })
);

const Hey = props => {
  return (
    <Hey1 data-test-id={props['data-test-id'] || 'Hey'} props={props}>
      Type something...
    </Hey1>
  );
};

Hey.propTypes = { isOn: PropTypes.string.isRequired };
export default Hey;

View with Emotion CSS in Codesandbox.

We could potentially support springs in web with keyframes and css-spring. The thing is that animation and transition in CSS are conceptually different for the browser and the starting state of animation would require more messing with the code, but it's definitely something worth looking at.
Here's a sandbox with an example - the result isn't all that great for this spring, it might be my choice of props to animate though :). It should be possible to use only one animation but it needs some extra stuff.

import React from "react";
import styled, { keyframes } from "react-emotion";
import PropTypes from "prop-types";
import spring, { toString } from "css-spring";

const on = keyframes(
  toString(
    spring(
      {
        color: "#323232",
        'font-size': '18px'
      },
      {
        color: "#ff0000",
        'font-size': '28px'
      },
      {
        stiffness: 100,
        damping: 10
      }
    )
  )
);

const off = keyframes(
  toString(
    spring(
      {
        color: "#ff0000",
        'font-size': '28px'
      },
      {
        color: "#323232",
        'font-size': '18px'
      },
      {
        stiffness: 100,
        damping: 10
      }
    )
  )
);
const Hey1 = styled("span")(
  {
    fontFamily: "Montserrat, sans-serif",
    fontWeight: 400,
    willChange: "color, font-size",
    animationTimingFunction: "linear",
    animationDuration: "1000ms"
  },
  ({ props }) => ({
    animationName: props.isOn ? on : off
  })
);

const Hey = props => {
  return (
    <Hey1 data-test-id={props["data-test-id"] || "Hey"} props={props}>
      Type something... I'll spring...
    </Hey1>
  );
};
export default Hey;

It should be possible to use https://github.com/animatedjs/animated too but I think that the react-emotion component would have to be tweaked for that to be the case. Also, JS animations aren't as performant as CSS ones yet, so they may feel a bit janky. However, it could be a good starting point since it makes the animation layer common. Here's an example using almost the same code that we used in RN below with in Web https://codesandbox.io/s/n0oyk8kzv0.

import React from "react";
import styled, { keyframes } from "react-emotion";
import PropTypes from "prop-types";
import Animated from "animated/lib/targets/react-dom";

const Hey1 = styled(Animated.span)(
  {
    fontFamily: "Montserrat, sans-serif",
    fontWeight: 400,
    willChange: "color, font-size"
  },
);

const getAnimatedValue = (animatedValue, from, to) =>
  animatedValue.interpolate({
    inputRange: [0, 1],
    outputRange: [from, to]
  });

class Hey extends React.Component {
  animatedValue = new Animated.Value(this.props.isOn ? 1 : 0);

  componentWillReceiveProps(next) {
    const { props } = this;
    if (props.isOn !== next.isOn) {
      Animated.spring(this.animatedValue, {
        toValue: next.isOn ? 1 : 0,
        stiffness: 100,
        damping: 10
      }).start();
    }
  }

  render() {
    const { props } = this;
    return (
      <Hey1
        data-test-id={props["data-test-id"] || "Hey"}
        style={{
          color: getAnimatedValue(this.animatedValue, "#323232", "#ff0000"),
          fontSize: getAnimatedValue(this.animatedValue, 18, 28)
        }}
      >
        Type something... I'll spring...
      </Hey1>
    );
  }
}

export default Hey;

I don't know how we'd go about hover effects for instance in that scenario since the styles are inline. Here's an alternative approach using css variables instead https://codesandbox.io/s/2p5qrr584n! It won't work on IE11 but it's probably worth the tradeoff since it works with hovers!

import React from "react";
import styled from "react-emotion";
import PropTypes from "prop-types";
import Animated from "animated/lib/targets/react-dom";

const Hey1 = styled(Animated.span)({
  fontFamily: "Montserrat, sans-serif",
  fontWeight: 400,
  willChange: "color, font-size",
  color: "var(--color)",
  fontSize: "var(--fontSize)",
  ':hover': {
    fontSize: "var(--fontSizeHover)",
  }
});

const getAnimatedValue = (animatedValue, from, to) =>
  animatedValue.interpolate({
    inputRange: [0, 1],
    outputRange: [from, to]
  });

class Hey extends React.Component {
  animatedValue = new Animated.Value(this.props.isOn ? 1 : 0);

  componentWillReceiveProps(next) {
    const { props } = this;
    if (props.isOn !== next.isOn) {
      Animated.spring(this.animatedValue, {
        toValue: next.isOn ? 1 : 0,
        stiffness: 100,
        damping: 10
      }).start();
    }
  }

  render() {
    const { props } = this;
    return (
      <Hey1
        data-test-id={props["data-test-id"] || "Hey"}
        style={{
          "--color": getAnimatedValue(this.animatedValue, "#323232", "#ff0000"),
          "--fontSize": getAnimatedValue(this.animatedValue, '18px', '28px'),
          "--fontSizeHover": getAnimatedValue(this.animatedValue, '28px', '38px')
        }}
      >
        Type something... I'll spring...
      </Hey1>
    );
  }
}

export default Hey;

Native example (same but with spring because that already works in Animated as is):

Hey Text
color #323232
fontFamily Montserrat
fontSize 18
fontWeight 400
text Type something...
when props.isOn
color red spring
fontSize 28 spring

FakeProps
isOn true
import React from 'react'
import { Animated, StyleSheet, Text } from 'react-native'

const getAnimatedValue = (animatedValue, from, to) =>
  animatedValue.interpolate({
    inputRange: [0, 1],
    outputRange: [from, to],
  })

class Ho extends React.Component {
  animatedValue = new Animated.Value(this.props.isOn ? 1 : 0)

  componentWillReceiveProps(next) {
    const { props } = this
    if (props.isOn !== next.isOn) {
      Animated.spring(this.animatedValue, {
        toValue: next.isOn ? 1 : 0,
        stiffness: 100,
        damping: 10,
      }).start()
    }
  }

  render() {
    const { props } = this
    return (
      <Animated.Text
        testID={props['testID'] || 'Ho'}
        style={{
          color: getAnimatedValue(this.animatedValue, '#323232', '#ff0000'),
          margin: 50,
          fontSize: getAnimatedValue(this.animatedValue, 18, 28),
        }}
      >
        Type something...
      </Animated.Text>
    )
  }
}

export default Ho

Expo Snack to try it out with the generated code.

We still need to cover block, list and view animations in further sessions but this should be enough to get something started.

@amymc
Copy link
Contributor

amymc commented Mar 1, 2018

@dariocravero Just FYI, after discussing with @tomatuxtemple we've decided to remove order from the supported values as the same effect can be achieved with delay.

@amymc
Copy link
Contributor

amymc commented Mar 16, 2018

Hello, I've got a bit of a problem. 😬

So on native (well on both really, but lets look at one at a time 🙈)
I'm not sure how to handle animations across multiple scopes that are animating the same properties

For example with this view

Hey Text
color blue
fontSize 18
text hey! 😜
when <isOn
color red spring delay 50
fontSize 30 linear
when <isClicked
color grey spring delay 90
fontSize 60 spring

This is what i have so far ...

import React from 'react';
import {
  Animated,
  StyleSheet,
  Text,
  TouchableWithoutFeedback,
  View,
} from 'react-native';
const getAnimatedValue = (animatedValue, from, to) =>
  animatedValue.interpolate({
    inputRange: [0, 1],
    outputRange: [from, to],
  });
const styles = StyleSheet.create({
  hspzgue: { alignItems: 'center', flex: 1, justifyContent: 'center' },
  hp2cr4d: { flexDirection: 'row' },
  hwcaxxv: { height: 100, width: 100 },
});

class App extends React.Component {
  animatedValue0 = new Animated.Value(this.props.isOn ? 1 : 0);
  animatedValue1 = new Animated.Value(this.props.isClicked ? 1 : 0);

  componentWillReceiveProps(next) {
    const { props } = this;
    if (props.isOn !== next.isOn) {
      Animated.spring(this.animatedValue0, {
        toValue: next.isOn ? 1 : 0,
        stiffness: 100,
        damping: 10,
        delay: 50,
      }).start();
    }
    if (props.isOn !== next.isOn) {
      Animated.timing(this.animatedValue0, {
        toValue: next.isOn ? 1 : 0,
        duration: 150,
        delay: 0,
      }).start();
    }
    if (props.isClicked !== next.isClicked) {
      Animated.spring(this.animatedValue1, {
        toValue: next.isClicked ? 1 : 0,
        stiffness: 100,
        damping: 10,
        delay: 90,
      }).start();
    }
  }

  render() {
    const { props } = this;

    return (
      <View testID={`${props['testID'] || 'App'}|`} style={styles.hspzgue}>
        <Animated.Text
          testID={`App.Hey|${
            props.isOn ? 'isOn|' : props.isClicked ? 'isClicked|' : ''
          }`}
          style={{
            color: getAnimatedValue(this.animatedValue0, 'blue', 'red'),
            fontSize: getAnimatedValue(this.animatedValue0, 18, 30),
            color: getAnimatedValue(this.animatedValue1, 'blue', 'grey'),
            fontSize: getAnimatedValue(this.animatedValue1, 18, 60),
          }}
        >
          hey! 😜
        </Animated.Text>
        <TouchableWithoutFeedback
          activeOpacity={0.7}
          onPress={props.onClick}
          underlayColor="transparent"
        >
          <View testID={`App.ButtonOne|`} style={styles.hp2cr4d}>
            <Text testID={`App.ButtonText|`} style={styles.hwcaxxv}>
              ButtonOne
            </Text>
          </View>
        </TouchableWithoutFeedback>
        <TouchableWithoutFeedback
          activeOpacity={0.7}
          onPress={props.onAnotherClick}
          underlayColor="transparent"
        >
          <View testID={`App.ButtonTwo|`} style={styles.hp2cr4d}>
            <Text testID={`App.ButtonTwoText|`} style={styles.hwcaxxv}>
              ButtonTwo
            </Text>
          </View>
        </TouchableWithoutFeedback>
        {props.children}
      </View>
    );
  }
}

export default App;

So its generating unique animated values for the different scopes, it's assigning them to the correct attributes in the style property and it's using the correct animated values in timing or spring but I have this...

 style={[
  styles.h3ix1vd,
    {
      color: getAnimatedValue(this.animatedValue0, 'blue', 'red'),
      fontSize: getAnimatedValue(this.animatedValue0, 18, 30),
      color: getAnimatedValue(this.animatedValue1, 'blue', 'grey'),
      fontSize: getAnimatedValue(this.animatedValue1, 18, 60),
    },

Which obviously can't work because I have duplicates of the same attributes 😭

So I tried this instead...

style={[
  styles.h3ix1vd,
  props.isOn
    ? {
        color: getAnimatedValue(this.animatedValue0, 'blue', 'red'),
        fontSize: getAnimatedValue(this.animatedValue0, 18, 30),
      }
    : props.isClicked
      ? {
          color: getAnimatedValue(
            this.animatedValue1,
            'blue',
            'grey'
          ),
          fontSize: getAnimatedValue(this.animatedValue1, 18, 60),
        }
      : styles.amymc,
]}

where styles.amymc is this..

const styles = StyleSheet.create({
  amymc: { color: 'blue', fontSize: 18 },
});

But then it's only animated the first time I change to props.isClicked or props.isOn
because when I switch back to the default styles in styles.amymc they're just regular styles with no animation...

I feel like I'm going to have the same problem in implementing CSS vars on DOM...

At the moment, I'm generating unique variable names like so...

 style={{
     '--color0': getAnimatedValue(this.animatedValue0, 'blue', 'red'),
      '--color1': getAnimatedValue(this.animatedValue1, 'blue', 'grey'),
      '--fontSize0': getAnimatedValue(this.animatedValue1, 18, 60),
   }}

Which is grand but I don't know how I'm going to handle it in the styled component...
I mean this obviously isn't going to work because I have duplicate attributes again 😬

const Hey1 = styled(Animated.span)({
  fontFamily: "Montserrat, sans-serif",
  fontWeight: 400,
  willChange: "color, font-size",
  color: "var(--color0)",
  color: "var(--color1)",
  fontSize: "var(--fontSize0))",
});

Here's a sandbox of the issue on native.

@dariocravero
Copy link
Author

That's some tricky stuff :). I tinkered here with a few variations of it but it doesn't quite do what you wanted to do there https://snack.expo.io/@dariocravero/multiple-scopes. I stored the from/to colour in the state in that example. It helps a bit but it doesn't do exactly what you're after. Maybe we can try using the same animated value for the same prop? Let me try a few alternatives and come back to you on that. I'll cc @tomatuxtemple because maybe we don't even want to go this far, at least not now :).

@amymc amymc mentioned this issue Mar 28, 2018
@dariocravero
Copy link
Author

Reference implementation of the comment above https://snack.expo.io/H1SgJMd9f

@dariocravero
Copy link
Author

dariocravero commented Jun 18, 2018

Until we get a chance to write up on the next iteration of animations to enable animations on mount and unmount, here is a sample project that may come in handy for it before I loose it again hehe.
animations-v2.zip. We'll also want to revisit react-spring or maybe package our little helpers (Stagger)[https://github.com/viewstools/demo/blob/master/web/src/Custom/Stagger.js] or Enter.

The upcoming syntax would deal with system scopes like before and after (or enter / leave) and it would take into account onWhen for context:

Prop Vertical
opacity 1
translateY 0
when <before
opacity 0 spring
translateY -10 spring
Text
onWhen <showMe
color red
when <before
color black spring

Text will transition from black to red when showMe is true. In this case before isn't the same as the one on the Vertical since it is scoped to showMe and not to the view.
We may also want to introduce some kind of "don't animate on mount" scope or keyword too.

@dariocravero
Copy link
Author

dariocravero commented Jul 5, 2018

react-spring example, mixed with hover on spring too

const Button1 = css({
  label: 'Button1',
  transformOrigin: 'left top',
  transition: 'background-color 150ms linear 0ms',
  willChange: 'transform, background-color',
  transform: 'scale(var(--scale))',
  backgroundColor: 'var(--backgroundColor)',
})

class Button extends React.Component {
  state = {
    hoverButton: false,
  }

  render() {
    const { props, state } = this

    return (
      <Spring
        native
        from={{ scale: 1 }}
        to={{ scale: state.hoverButton ? 2.5 : props.isActive ? 1.75 : 1 }}
        config={{
          speed: 12,
          bounciness: 12,
        }}
        delay={0}
        onRest={() => {
          props.onAnimationDone({
            scope: 'isActive',
            props: ['scale'],
          })
        }}
      >
        {spring => (
          <animated.button
            data-test-id={`${props['data-test-id'] || 'Button'}|${
              props.isActive ? 'isActive|' : ''
            }`}
            onMouseOver={() => this.setState({ hoverButton: true })}
            onMouseOut={() => this.setState({ hoverButton: false })}
            onClick={props.onClick}
            onTransitionEnd={() => {
              if (props.onAnimationDone) {
                props.onAnimationDone({
                  scope: 'isActive',
                  props: ['backgroundColor'],
                })
              }
            }}
            style={{
              '--scale': spring.scale,
              '--backgroundColor': `${
                props.isActive ? 'deepskyblue' : 'purple'
              }`,
            }}
            className={`views-block ${Button1}`}
          >
            <span data-test-id={`Button.Text|`} className="views-block">
              {props.text}
            </span>
            {props.children}
          </animated.button>
        )}
      </Spring>
    )
  }
}

@dariocravero
Copy link
Author

Closing for now as we have the initial phase of animations done :) and we have other issues to track specific bits and pieces that we want to do later!

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

No branches or pull requests

2 participants