Software DevelopmentUncategorized

Creating Simple Character Animations in React Native

By January 26, 2018 No Comments

Recently Radial built a small iPad application aimed at children. We worked with an illustrator to create a fun character for the app named Pete the Carrier Pigeon. We wanted Pete to come alive in a few of the views with some simple animations. Here’s how we did it.

Pete the Carrier Pigeon

This is Pete the Carrier Pigeon. Isn’t he great-looking?

Step One: Create layers

First we had to decide which parts of Pete we wanted to animate. We decided on the eyes (we’d make them blink), the cap (we’d make it lift up and spin), and the beak (so he could talk).

To do this, first I exported a base image of the character. This does not contain the parts of his body that would be animated. Our app was being built in React Native, which currently does not support svgs. We decided to animate pngs instead. So I exported the below image from Adobe Illustrator as a png with a transparent background.

The base image of Pete, without eyes or a cap

This is the base image, without eyes or a cap, because those are the parts we are animating in this component.

We called this base_pete.png

Then I made a separate pngs of the parts to be animated. For winking and a cap animation, I made:

  • pete_eyes_open.png
  • pete_eyes_closed.png
  • pete_cap_up.png
  • pete_cap_down.png
We made separate pngs of Pete's eyes closed and open

We made separate pngs of Pete’s eyes open (left) and closed (right), and also his cap tilted up and tilted down. (not pictured)

Step 2: Create components and position the layers

We created a React component called PetePart. That component returns the part of Pete desired based on the value passed to the source attribute (eyes_open, eyes_closed, cap_up, cap_down.)

import React, { Component } from 'react'; 
import { View, Image, Animated } from 'react-native'; 
import styles from '../styles/Pete.style';

const PetePart = ({source, style={}}) => (
  <Animated.View style={[styles.petePart, style]}>
    <Image source={source}/>
  </Animated.View>
);

This component PetePart then becomes part of the parent component Pete. The Pete component renders the base image. We absolutely positioned all the PetePart images so that when they are visible they appear in the correct location on the base image. The code below now represents a reusable “Pete” component that can go anywhere in the app, ready to be animated.

Below is the code for the Pete component; click to expand.

View the code for Pete Component
import React, { Component } from 'react';
import { View, Image, Animated } from 'react-native';
import styles from '../styles/Pete.style';

const PetePart = ({source, style={}}) => (
  <Animated.View style={[styles.petePart, style]}>
    <Image source={source}/>
  </Animated.View>
);

const eyesOpen = require('../assets/images/pete_eyes_open.png');
const eyesClosed = require('../assets/images/pete_eyes_closed.png');

const capUp = require('../assets/images/pete_cap_up.png');
const capDown = require('../assets/images/pete_cap_down.png');

const capHeightStyle = (height=0) => ({transform: [{translateY: height}]})

export default function Pete({ eyes='open', cap='on', capHeight=0 }) {
  return (
    <View>
      <PetePart source={eyes === 'open' ? eyesOpen : eyesClosed}/>
      <PetePart source={cap === 'off' ? capUp : capDown} style={capHeightStyle(capHeight)}/>
      <Image source={require('../assets/images/pete_base.png')} ></Image>
    </View>
  );
}

Step 3: Create the animations

We created a component called AnimatedPete that handles the animation. This component is where all the animation logic lives. It then passes props to the Pete component which animates based on those props.

Gif of Pete blinking

Gif of blinking animation

The animations are all created using setTimeouts. A tricky part of this is working out the timing of the animations. 

In the original code I wrote, Pete’s eyes blinked on and off at a predictable rate, which looked unnatural.

We refactored to fix this. Essentially what needed to happen was the blinking timing had to be separated from the periods of time when Pete’s eyes are just open.

In the refactored code, the eyes open and blinking timing get kicked off with different setTimeouts. This helps the blinking look more natural.

The timeouts are then cleared using onComponentWillUnmount. This prevents events being called on a component that was unmounted.

To lift the cap we use the Animated API from ReactNative. This gives the ability to have a set of animations in sequence Animated.sequenceto lift and lower the cap.

Wiggling the cap is very similar to blinking the eyes. To wiggle the cap we simply switch between the pete_cap_up.png and pete_cap_down.png images using a setTimeout.

Below is the complete code for the Animated Pete component.

View the code for Animated Pete
import _ from 'lodash';
import React, { Component } from 'react';
import { View, Image, Animated } from 'react-native';
import Pete from '../components/Pete';

const BLINK_TIME = 120;
const CAP_WIGGLE_TIMER = 80;
const CAP_FLOAT_SPEED = 350;
const CAP_FLOAT_TIMER = 5000;
const CAP_FLOAT_HEIGHT = 30;

export default class AnimatedPete extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      eyes: true,
      capTip: true,
      capHeight: new Animated.Value(0)
    };
    if (props.blinking) {
      setTimeout(this.blink, 1000);
    }
    if (props.capToggle) {
      this._wiggleTimer = setTimeout(this.capWiggle, 100);
      this._capTimer = setTimeout(this.capFloat, 100);
    }
  }

  onComponentWillUnmount() {
    clearTimeout(this._blinkTimer);
    clearTimeout(this._wiggleTimer);
    clearTimeout(this._capTimer);
  }

  blink = () => {
    this.setState({eyes: false});
    setTimeout(() => this.setState({eyes: true}), BLINK_TIME);
    this._blinkTimer = setTimeout(this.blink, _.random(1000, 5000));
  }

  capWiggle = () => {
    const wiggles = 7;
    _.times(wiggles, (i) => {
      setTimeout(() => this.setState({capTip: false}), CAP_FLOAT_SPEED+CAP_WIGGLE_TIMER*(i*2));
      setTimeout(() => this.setState({capTip: true}), CAP_FLOAT_SPEED+CAP_WIGGLE_TIMER*(i*2+0.8));
    })
    this._wiggleTimer = setTimeout(this.capWiggle, CAP_FLOAT_TIMER);
  }
  capFloat = () => {
    Animated.sequence([
      Animated.spring(this.state.capHeight, {toValue: -CAP_FLOAT_HEIGHT, duration: CAP_FLOAT_SPEED}),
      Animated.spring(this.state.capHeight, {toValue: 0, duration: CAP_FLOAT_SPEED, delay: CAP_FLOAT_SPEED})
    ]).start();
    this._capTimer = setTimeout(this.capFloat, CAP_FLOAT_TIMER);
  }

  render() {
    const {eyes, capTip, capHeight} = this.state;
    return <Pete eyes={eyes ? 'open' : 'closed'} cap={capTip ? 'on' : 'off'} capHeight={capHeight}/>
  }
}

This is a fairly straightforward way to create simple animations for characters.

Here’s a look at the final hat animation:

Pete's hat animating

Animated gif of hat animation

If you were programming for a web browser you would probably prefer to use svgs. This would allow you to make even more custom animations, since SVG is a complete vector drawing language. But we were able to accomplish what we wanted for this straightforward case using pngs in React Native.

 

Leave a Reply