Nefe James Nefe is a frontend developer who enjoys learning new things and sharing his knowledge with others.

Creating physics-based animations in React with renature

4 min read 1389

Creating Physics-based Animations in React with renature

Introduction

renature is a physics-based animation library for React inspired by the natural world. It takes inspiration from the physics of our universe and models real-world forces like gravity, friction, air resistance, and fluid dynamics.

renature is influenced by other popular physics-based animation libraries like react-spring and elements of Framer Motion. In this article, we will learn how to create animations in renature by building several examples and applications.

Basic implementation

First things first: install the renature package by running either of the commands below:

npm install --save renature
# or
yarn add renature

renature provides six hooks we can use in creating animations. The first three are the useFriction, useGravity, and useFluidResistance hooks. These hooks animate the properties of a single element.

They all work in a similar manner, but as their names imply, the natural forces they are modeled after and the physics they depend on will vary. Also, the config object varies from hook to hook depending on the force you’re using.

There are also the useFrictionGroup, useGravityGroup, and useFluidResistanceGroup hooks, which we can use to animate the properties of a group of elements simultaneously.

Let’s take a look at a basic implementation using the useFriction hook.

import { useFriction } from "renature";
import "./styles.css";

export default function App() {
  const [props] = useFriction({
    from: {
      transform: "translateX(100px)"
    },
    to: {
      transform: "translateX(300px)"
    },
    config: {
      mu: 0.2,
      mass: 20,
      initialVelocity: 5
    },
    repeat: Infinity
  });

  return (
    <div className="App">
      <div {...props} className="box" />
    </div>
  );
}

If you’re familiar with react-spring, you’ll notice renature has a similar declarative API. Every hook in renature expects from and to properties wherein we describe the CSS states we want to animate to and from. If a value is set for a property in from but not in to, that property will be ignored, and it won’t be animated.

Sometimes we might want our animations to run infinitely without stopping. We can do this by applying repeat: Infinity to our animation configuration.

Since we used the useFriction hook, we pass in the corresponding physics parameters. As its name indicates, the useFriction hook is modeled after the frictional force. The parameters are:

We made a custom demo for .
No really. Click here to check it out.

  • mu, which is the coefficient of kinetic friction
  • mass, which is the mass of the moving body
  • initialVelocity, which is the initial velocity of the movement

The wonderful thing about renature is that you don’t need to be a science guru to make awesome animations.

Animating multiple properties with renature

So we’ve seen how the useFriction hook works and how to animate the transform property of an element. Where and when necessary, we can also animate multiple properties simultaneously:

import { useFriction } from "renature";
import "./styles.css";

export default function App() {
  const [props] = useFriction({
    from: {
      transform: "translateX(100px), rotate(0deg)",
      background: "red",
      borderRadius: "0%"
    },
    to: {
      transform: "translateX(300px), rotate(360deg)",
      background: "steelblue",
      borderRadius: "50%"
    },
    config: {
      mu: 0.2,
      mass: 20,
      initialVelocity: 5
    },
    repeat: Infinity
  });

  return (
    <div className="App">
      <div {...props} className="box" />
    </div>
  );
}

To animate multiple properties of an element, add more properties of the element you want to animate.

Creating animations in groups

We may want to animate multiple elements simultaneously. Grouped animations allow us to specify the number of elements to animate and to set the configuration for each of them independently.

import { useGravityGroup } from "renature";
import "./styles.css";
export default function App() {
  const [nodes] = useGravityGroup(3, (i) => ({
    from: {
      transform: "translate(0px, 0px) scale(1) skewY(0deg)",
      opacity: 0
    },
    to: {
      transform: "translate(20px, 20px) scale(1.2) skewY(5deg)",
      opacity: 1
    },
    config: {
      moverMass: 10000,
      attractorMass: 10000000000000,
      r: 10
    },
    repeat: Infinity,
    delay: i * 500
  }));
  return (
    <div className="App">
      <div className="container">
        {nodes.map((props, i) => {
          return <div className="box" key={i} {...props} />;
        })}
      </div>
    </div>
  );
}

We used the useGravityGroup to create the grouped animation above. All grouped animation hooks take a similar form:

const [props] = use<Force>Group(n: number, fn: (index: number) => Config);

You can delay animations in renature by specifying the delay property in your animation configuration. delay expects a number in milliseconds and will start the animation once the specified delay has elapsed. This is most commonly used for grouped animations where you want to stagger children animating at regular intervals.

Controlling animation states

Let’s look at how to control animation states in renature. We can start, stop, pause, delay, and run animations a specific number of times. We’ve already looked at how to repeat and delay animations.

To control animation states, renature provides a controller API. The controller is the second object returned by a renature hook, and it comes with three methods — start, pause, and stop — for interacting with your animation’s play state.

import { useFriction } from "renature";
import "./styles.css";
import Button from "./Button";

export default function App() {
  const [props, controller] = useFriction({
    from: {
      transform: "translateY(0px)",
      opacity: 1,
      borderRadius: "10%"
    },
    to: {
      transform: "translateY(50px)",
      opacity: 0,
      borderRadius: "50%"
    },
    repeat: Infinity,
    pause: true //Signal that the animation should not run on mount.
  });

  return (
    <div className="App">
      <div className="btn-box">
        <Button action={controller.start} text="start" />
        <Button action={controller.pause} text="pause" />
        <Button action={controller.stop} text="stop" />
      </div>
      <div className="box" {...props} />
    </div>
  );
}

We use the controller.start method available in the controller API to start animations. To do so, we need to include pause: true in our animation configuration to prevent the animation from immediately running on mount.

To pause a running animation, we use the controller.pause method. This method will stop the frame loop but preserve the animation state. This means we can resume an animation at any time using controller.start.

To stop a running animation, we use the controller.stop method. Unlike controller.pause, this will destroy the animation state, so we should only use it when we’re certain we want the animation to end.

renature demos

Now that we know how to create animations with renature, let’s look at some real-world applications.

Card hover demo

Have you ever hovered over a card and seen the image scale up or down along the width and height of its container? Let’s recreate that effect using renature.

Here’s a sandbox of what we will build:

While CSS is not the focus of this topic, we will need some CSS for the effect to work as it should.

.card-box {
  width: 200px;
  border: 1px solid grey;
  overflow: hidden;
  margin-bottom: 2rem;
}

We have to set an overflow: hidden to the card-box div to ensure that when the image scales, it remains restricted in its container. With that explained, let’s get back to renature.

Using what we learned about controlling animation states, we call the controller.start and the controller.pause methods whenever we hover in and out of the img-box div.

export default function Card({ imgUrl, props, controller }) {
  return (
    <div className="card-box">
      <div
        className="img-box"
        {...props}
        onMouseEnter={controller.start}
        onMouseLeave={controller.pause}
      >
        <img src={imgUrl} />
      </div>

      //more stuff below...

Notification toast demo

Notification toasts are among the common components used in applications. Let’s create one using renature.

Here’s a sandbox of what we will build:

For the CSS, we had to set the opacity of the toast to 0 so that it only shows when the button is clicked.

return (
    <div className="App">
      <button onClick={controller.start}>show toast</button>
      <Toast props={props} />
    </div>
);

We call the controller.start method when the button is clicked. This fires the animation, and the toast drops down into view.

Accessibility concerns

Not everyone enjoys decorative animations or transitions, and some users experience outright mental and physical difficulty when faced with parallax scrolling, zooming effects, and so on. As such, we must ensure that our animations don’t present accessibility concerns for application users.

In renature‘s config object, we can include an optional reducedMotion property. If this is left unspecified and the end user prefers reduced motion, the animating element will be set immediately to the to state to avoid potentially harmful animation.

Conclusion

While I’ve made some demos in this article showing how renature can be used in real-world applications, it would be awesome if the team included more practical demos that might be useful to developers. Regardless, renature is an awesome physics-based animation library.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Nefe James Nefe is a frontend developer who enjoys learning new things and sharing his knowledge with others.

Leave a Reply