by

If you ever wondered how to animate snow, follow this tutorial to know how.

What We Will Be Building

We’re going to build an app that has an image of a Christmas Tree and snowfall. That’s how it’s going to look like.

For your reference, the final code for the app we’re building can be found in this GitHub repo.

Outlining the App Structure

It’s always a good idea to plan you app code structure in advance. Let’s list the files that we’re going to need to build all of the components for the app.

  • index.ios.js or index.android.js. Entry points into the app for iOS or Android platforms respectively. Both are going to render just one component called Tree.
  • Tree.js. A component that renders a background image and flakes using multiple instances of Flake component.
  • Flake.js. A component that renders a flake of given size and position.

Let’s Get Started

Let’s start off by creating a new app. Open Terminal App and run these commands to initialize a new project and run it in the simulator.

react-native init ChristmasTree;
cd ChristmasTree;
react-native run-ios;

Enable Hot Reloading

Once your app is up and running, press D and select Enable Hot Reloading. This will save you some time having to reload the app manually every time you make a change.

Flake Component

Let’s start off by creating Flake component, which is a pure render component. It doesn’t have state and just renders a flake taking in radius, density and x, y coordinates as props.

Create a new file with the following code and call it Flake.js.

import React, { Component } from 'react';
import {
  Animated,
  Dimensions,
  Easing
} from 'react-native';

// Detect screen size
const { width, height } = Dimensions.get('window');

export default class Flake extends Component {

  // Angle of falling flakes
  angle = 0

  componentWillMount() {
    // Pull x and y out of props
    const { x, y } = this.props;
    // Initialize Animated.ValueXY with passed x and y coordinates for animation
    this.setState({
      position: new Animated.ValueXY({ x, y })
    });
  }

  getStyle = ({ radius, x, y } = this.props) => ({
    position: 'absolute',
    backgroundColor: 'rgba(255,255,255,0.8)',
    borderRadius: radius,
    width: radius * 2,
    height: radius * 2,
  })

  render() {
    return <Animated.View style={[this.getStyle(), this.state.position.getLayout()]} />;
  }

}

Index Files

Next, let’s update our index files. Since we’re going to re-use the same code for both, iOS and Android, so we don’t need two different index files. We’ll be using the same Tree component in both index files.

Open index.ios.js file and scrap all of the React Native boilerplate code to start from scratch. Do the same for index.android.js. And add the following code to both of index files.

import { AppRegistry } from 'react-native';
import Tree from './Tree';

AppRegistry.registerComponent('ChristmasTree', () => Tree);

This code imports Tree component from Tree.js file and registers it as main app container. If you took at look at the simulator at this point, you would see an error screen. That’s because Tree.js doesn’t exist yet, and therefore can’t be imported. So, let’s fix it.

Tree Component

Next, let’s build Tree component. It takes in flakesCount prop with the default value of 50 flakes on the screen and renders that amount of flakes distributed randomly on the screen. To render each flake, it uses Flake component we built in the previous step. It also renders a background image we’re about to download.

Download Background Image

Create a new folder called assets inside your project and download this image into that folder.

Create Tree.js

Create a new file with the following code and call it Tree.js.

import React, { Component } from 'react';
import {
  Dimensions,
  Image,
  StyleSheet,
  View
} from 'react-native';
import Flake from './Flake';

// Detect screen size
const { width, height } = Dimensions.get('window');

export default class Tree extends Component {

  static defaultProps = {
    flakesCount: 50, // total number of flakes on the screen
  }

  render({ flakesCount } = this.props) {
    return <View style={styles.container}>
      {/* Christmas Tree background image */}
      <Image
        style={styles.image}
        source={require('./assets/tree.jpg')}
      >
        {/* Render flakesCount number of flakes */}
        {[...Array(flakesCount)].map((_, index) => <Flake
            x={Math.random() * width}               // x-coordinate
            y={Math.random() * height}              // y-coordinate
            radius={Math.random() * 4 + 1}          // radius
            density={Math.random() * flakesCount}   // density
            key={index}
          />)}
      </Image>
    </View>;
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  image: {
    flex: 1,
    resizeMode: 'cover',
    width: width,
    position: 'relative',
  },
});

Check Out the Progress

Bring up the simulator window and see what we’ve got so far. We have our Christmas Tree and a bunch of snowflakes. If you press R you can see that they’re distributed randomly every time. That’s great, but snowflakes are static and do not move. Let’s make them fall in the next step.

Make Them Fall

To make the snowflakes fall, we’re going to need to add animate() method that will update flake coordinates and use React Native core module Animated to animate their falling.

So, open up Flake.js file and add the following code inside Flake class definition between export default class Flake extends Component { and }.

export default class Flake extends Component {
  
  // ...exisiting code

  componentDidMount() {
    this.animate();
  }

  animate = () => {
    // Animation duration
    let duration = 500;
    // Pull x and y out of Animated.ValueXY object in this.state.position
    const { x: { _value: x }, y: { _value: y } } = this.state.position;
    // Pull radius and density out of props
    const { radius, density } = this.props;

    this.angle += 0.02;
    let newX = x + Math.sin(this.angle) * 10;
    let newY = y + Math.cos(this.angle + density) + 10 + radius / 2;

    // Send flakes back from the top once they exit the screen
    if (x > width + radius * 2 || x < -(radius * 2) || y > height)
    {
      duration = 0;                   // no animation
      newX = Math.random() * width;   // random x
      newY = -10;                     // above the screen top

      // Send 2/3 of flakes back to the top
      if (Math.floor(Math.random() * 3) + 1 > 1) {
        newX = Math.random() * width;
        newY = -10;
      // Send the rest to either left or right
      } else {
        // If the flake is exiting from the right
        if (Math.sin(this.angle) > 0) {
          // Enter from the left
          newX = -5;
          newY = Math.random() * height;
        } else {
          // Enter from the right
          newX = width + 5;
          newY = Math.random() * height;
        }
      }
    }

    // Animate the movement
    Animated.timing(this.state.position, {
      duration,
      easing: Easing.linear,
      toValue: {
        x: newX,
        y: newY,
      }
    }).start(() => {
      // Animate again after current animation finished
      this.animate();
    });
  }

  // ...exisiting code
}

And now they’re falling.

Wrapping Up

I hope you enjoyed the tutorial. If you have any questions or ideas for next tutorials, just leave a comment.

Recommended Reading

Spread the Word