DEV Community

Rares Mardare
Rares Mardare

Posted on

Introducing react-tweenful: Animation engine for React

Hello, everyone,

I'm very glad to have released an opensource project I've been working on lately - react-tweenful. Hence I'm sharing this with you and looking for your feedback!

Repository

https://github.com/teodosii/react-tweenful

Demo

https://teodosii.github.io/react-tweenful/

What is react-tweenful?

react-tweenful it's an animation engine written from scratch to help you animate stuff easily. Initially I wanted a replacement for react-transition-group but with an animation engine behind, hence I've written the engine and then written individual components to handle multiple cases - Tweenful, SVG, Observer and ObserverGroup. It's inspired by anime.js.

Features

  • Loop support (infinite or up to a specific number)
  • Wide variety of easings (bezier, predefined and custom easing)
  • Delayed animations (before and after)
  • Events support
  • Negative delay support to mimic CSS animations
  • Percent based animations to mimic CSS animations (e.g. 0%, 50%, 100%)
  • Tweenful component for animating DOM nodes
  • SVG component to animate SVG nodes
  • Observer component for mount/unmount animations
  • ObserverGroup component to handle child transition (list removal/insertion, page transition etc)

Usage

react-tweenful exports the following:

  • Tweenful - component to animate DOM elements. It requires a DOM node to perform animation on.
  • SVG - component to animate SVG elements. It requires a SVG node to perform animation on.
  • Observer - component to animate mounting and unmounting of an element.
  • ObserverGroup - component to watch over a list of Observer elements such as list removal/insertion or route transition

A couple of utility functions are also exported to help you out animating:

  • percentage for percentage based animations
  • bezier for bezier easings
  • elastic for elastic easing

Import the needed component, for example Tweenful

import Tweenful, { elastic } from 'react-tweenful';
Enter fullscreen mode Exit fullscreen mode

Tweenful requires a node to render on which it will perform the animation. We've got most of the DOM nodes covered in the form of namespacing such as Tweenful.div, Tweenful.span and so on.

const Example = () => (
  <Tweenful.div 
    className="tween-box"   
    duration={2000} 
    easing={elastic(1, 0.1)}
    style={{ position: 'relative' }}    
    animate={{ left: ['0px', '250px'] }}
  ></Tweenful.div>
);
Enter fullscreen mode Exit fullscreen mode

Real world examples

Animate page route transition

react-tweenful can be used to animate children removal/insertion of a list or to animate betweeen routes with the help of Observer and ObserverGroup

Notifications

<ObserverGroup
  config={{
    duration: 800,
    style: { overflow: 'hidden' },
    mount: { opacity: [0, 1], height: ['0px', 'auto'] },
    unmount: { opacity: [1, 0], height: ['auto', '0px'] },
    easing: 'easeInOutCubic'
  }}
  skipInitial={true}
>
  {this.state.notifications.map(notification => (
    <Notification
      key={notification.id}
      notification={notification}
      onClick={this.removeNotification}
    />
  ))}
</ObserverGroup>
Enter fullscreen mode Exit fullscreen mode

Next example shows how easily we can animate when route changes. Watch the URL getting changed and see how the content is animated using a fade-in fade-out animation between previous and current page.

Routing

Prism

This example shows usage of negative delay, which was implemented into the library to mimic CSS animations.

Prism

import React from 'react';
import { SVG } from 'react-tweenful';

const WAVE_COUNT = 16;
const offset = 40;
const waveLength = 375;
const duration = 1500;

const waves = new Array(WAVE_COUNT).fill(0).map((wave, i) => ({
  key: i + 1,
  style: {
    transformOrigin: '500px 500px',
    opacity: 4 / WAVE_COUNT,
    mixBlendMode: 'screen',
    fill: `hsl(${(360 / WAVE_COUNT) * (i + 1)}, 100%, 50%)`,
    transform: `rotate(${(360 / WAVE_COUNT) * i}deg) translate(${waveLength}px, ${offset}px)`
  },
  rotate: `${(360 / WAVE_COUNT) * (i + 1)}deg`,
  translate: `${waveLength}px ${offset}px`,
  angle: `${(360 / WAVE_COUNT) * (i + 1)}deg`,
  delay: (duration / WAVE_COUNT) * (i + 1) * -3,
  path:
    'M-1000,1000V388c86-2,111-38,187-38s108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,109,38,188,38,110-38,187-38,108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,108,38,187,38,111-38,187-38,109,38,188,38c0,96,0,612,0,612Z'
}));

const RotatingSvg = () => {
  return (
    <svg height="300" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 1000 1000">
      <defs>
        <clipPath id="world">
          <circle cx="500" cy="500" r="450" stroke="none" fill="none"></circle>
        </clipPath>
      </defs>
      <circle cx="500" cy="500" r="450" stroke="none" fill="#000"></circle>
      <SVG
        type="g"
        className="circle"
        loop={true}
        duration={(WAVE_COUNT / 2) * duration}
        style={{ transformOrigin: '500px 500px' }}
        easing="linear"
        animate={{ rotate: '360deg' }}
      >
        {waves.map(wave => (
          <SVG.path
            loop={true}
            id={wave.key}
            key={wave.key}
            easing="linear"
            duration={1500}
            d={wave.path}
            style={wave.style}
            delay={wave.delay}
            transform={{ rotate: wave.rotate, translate: wave.translate }}
            animate={{ rotate: `${wave.angle}`, translate: `0px ${offset}px` }}
          />
        ))}
      </SVG>
    </svg>
  );
};
Enter fullscreen mode Exit fullscreen mode

SVG

The following shows usage of SVG component to animate path from start to end and then fill the form.

SVG

const SvgDemo = () => {
  return (
    <svg
      height="300"
      xmlns="http://www.w3.org/2000/svg"
      x="0px"
      y="0px"
      viewBox="0 0 400 400"
    >
      <SVG.path
        duration={3000}
        easing="easeInQuad"
        style={{ fill: "#fff", transform: 'scale(4)' }}
        animate={[{ strokeDashoffset: [100, 0] }, { fill: '#b91e1e' }]}
        stroke="#b91e1e"
        strokeWidth="2"
        fill="none"
        d="M61.9,55.4c-2.3-3.5-3.6-7.7-3.6-12.2c0-4.7,1.5-9.1,4-12.7c2.1,3.1,3.4,6.7,3.7,10.7h13  C78.2,24,65,10.1,48.1,8.2l-3.8-6.6l-3.8,6.6C23.5,10.1,10.3,24,9.5,41.3h13c0.3-3.9,1.6-7.6,3.7-10.7c2.5,3.6,4,8,4,12.7  c0,4.5-1.4,8.7-3.7,12.2c-2.3-3.2-3.8-7-4-11.2h-13c0.8,18.5,16,33.3,34.7,33.3S78.2,62.7,79,44.3h-13  C65.7,48.4,64.2,52.2,61.9,55.4z M36,62.9c3.9-5.6,6.2-12.3,6.2-19.6c0-7.6-2.5-14.7-6.8-20.4c2.7-1.2,5.6-1.9,8.8-1.9  c3.1,0,6.1,0.7,8.8,1.9c-4.2,5.7-6.8,12.7-6.8,20.4c0,7.3,2.3,14.1,6.2,19.6c-2.5,1-5.3,1.6-8.2,1.6C41.3,64.5,38.6,63.9,36,62.9z"
      />
    </svg>
  );
};
Enter fullscreen mode Exit fullscreen mode

Bouncing Balls

The following example shows usage of percentage-based animation and negative delay support.

Bouncing Balls

import React from 'react';
import { SVG, percentage, elastic } from 'react-tweenful';

const circles = new Array(10).fill(0).map((_e, i) => ({
  loop: true,
  fill: `hsl(${(i + 1) * 20 - 20}, 70%, 70%)`,
  delay: ((i + 1) * 1500) / -10,
  duration: 1500,
  easing: elastic(2, 0.9),
  transform: {
    translate: '0 100px'
  },
  style: {
    transformOrigin: `${-200 + 120 * (i + 1)}px 250px`
  },
  animate: percentage({
    '0%': { translate: '0px 100px', scale: 1 },
    '50%': { translate: '0px -100px', scale: 0.3 },
    '100%': { translate: '0px 100px', scale: 1 }
  }),
  r: 35,
  cx: 100 * i + 50,
  cy: 250
}));

const BouncingBalls = () => {
  return (
    <div className="bouncing-balls">
      <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 1000 500">
        {circles.map((circle, i) => (
          <SVG.circle key={i} {...circle}></SVG.circle>
        ))}
      </svg>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Conclusions

The library is released under MIT license so feel free to use it in any commercial product.

I hope you liked it and I'm looking forward for your feedback and suggestions.

Top comments (0)