Weaving Fabric.js With Anime.js For Exceptional Designs

Weaving Fabric.js With Anime.js For Exceptional Designs

A short module about integrating Fabric.js with anime.js for for excellent animation decisions

Most of us have worked with/come across Fabric.js when working on projects that involves fiddling around with the canvas. It could be dragging simple shapes around or defining animations on them. Here's how a simple Fabric object looks like:

var canvas = new fabric.Canvas('c');

var rect = new fabric.Rect({
  width: 50,
  height: 50,
  left: 100,
  top: 100,
  stroke: '#aaf',
  strokeWidth: 5,
  fill: '#faa',
  selectable: false
});
canvas.add(rect);

// Adding animation to the rect object

rect.animate('left', rect.left === 100 ? 400 : 100, {
      duration: 1000,
      onChange: canvas.renderAll.bind(canvas),
      onComplete: function() {
        animateBtn.disabled = false;
      },
      easing: fabric.util.ease[document.getElementById('easing').value]
    });
  • The above object is a Fabric rectangle type with different properties that define how the object would appear when rendered on the canvas.
  • On line #17 we are defining the animation with some values pertaining to the rectangle object. Here's how it would look like after completing the animation.

Let's look into how anime.js creates animation on different objects, specifically the timeline feature. With the help of this feature on the timeline, we can control the animations more comfortably like a video. Use the following code to change this:

// Create a timeline with default parameters
var tl = anime.timeline({
  easing: 'easeOutExpo',
  duration: 750
});

// Add children
tl
.add({
  targets: '.basic-timeline-demo .el.square',
  translateX: 250,
})
.add({
  targets: '.basic-timeline-demo .el.circle',
  translateX: 250,
})
.add({
  targets: '.basic-timeline-demo .el.triangle',
  translateX: 250,
});

In the above snippet we start with defining the type of animations as well as defining the duration of the entire animation. In this scenario, we would be working with a timeline animation:

  • Once we create an animation we can then add objects or children to them.
  • The variable tl is a timeline object with a bunch of different properties like pause, play, restart, current time etc on it. With the help of these properties, the user can control how the animation is played.
  • The above snippet shows a simple translate animation where the objects move from left to right.
  • More granular animations can be defined with the help of keyframes which is available from the anime.js library.
  • Keyframes are an array of objects that define how the object should behave at any instance of time.
  • Each object in a keyframe can have different properties relating to the position, opacity, scale and also the delay of the object animation. A sample keyframe would look similar to this:
anime({
  targets: '.animation-keyframes-demo .el',
  keyframes: [
    {translateY: -40},
    {translateX: 250},
    {translateY: 40},
    {translateX: 0},
    {translateY: 0}
  ],
  duration: 4000,
  easing: 'easeOutElastic(1, .8)',
  loop: true
});

Now, we have a basic understanding of integrating the animations using Fabric.js and anime.js. While Anime.js is more suitable for video like animations, Fabric.js works with the canvas to gain control over the behaviour of the objects.

Now the question is how can we combine these together to create animations on the timeline using anime.js?

We need the timeline in order to support all the basic video like controls. This can be achieved by defining custom properties on fabric objects. For example:

  1. From the first code snippet you can add your own properties on the React object using rect.setOptions({name:value}).
  2. We can utilise these properties to define the keyframes on the Fabric object itself.
  3. We can have something like an _anim property which has all the required keyframes that makes up the animation of that object. A sample object with animation keyframes would look like this:
{
          "rx": 0,
          "ry": 0,
          "top": 539.4963730569948,
          "fill": "#fff",
          "left": 539.4963730569947,
          "type": "rect",
          "_anim": {
            "config": {
              "keyframes": [
                {
                  "top": 539.4963730569948,
                  "left": 539.4963730569947,
                  "angle": 0,
                  "skewX": 0,
                  "skewY": 0,
                  "width": 1080,
                  "height": 1080,
                  "scaleX": 1,
                  "scaleY": 1,
                  "opacity": 1,
                  "_initial": {
                    "opacity": 0
                  },
                  "duration": 1,
                  "_presetId": 35,
                  "_presetIndex": 0
                },
                {
                  "top": 539.4963730569948,
                  "left": 539.4963730569947,
                  "angle": 0,
                  "skewX": 0,
                  "skewY": 0,
                  "width": 1080,
                  "height": 1080,
                  "scaleX": 1,
                  "scaleY": 1,
                  "opacity": 1,
                  "duration": 2434,
                  "_presetId": 35,
                  "_presetIndex": 0
                }
              ]
            },
            "offset": 0
          },
          "angle": 0,
          "flipX": false,
          "flipY": false,
          "skewX": 0,
          "skewY": 0,
          "width": 1080,
          "clipTo": null,
          "height": 1080,
          "scaleX": 1,
          "scaleY": 1,
          "shadow": null,
          "stroke": null,
          "_nodeId": "845eeaeb-491b-4db1-8e1e-22199e42a7f5",
          "_objKey": "01f114f2-cf8b-4ab5-92d9-0e2e41ab5b32",
          "opacity": 1,
          "originX": "center",
          "originY": "center",
          "version": "3.6.2",
          "visible": true,
          "fillRule": "nonzero",
          "paintFirst": "fill",
          "crossOrigin": "anonymous",
          "strokeWidth": 0,
          "strokeLineCap": "butt",
          "strokeLineJoin": "miter",
          "backgroundColor": "",
          "strokeDashArray": null,
          "transformMatrix": null,
          "strokeDashOffset": 0,
          "strokeMiterLimit": 4,
          "globalCompositeOperation": "source-over",
        }

You can even add videos as fabric objects not just images/shapes. This is possible with the help of fabric.util.createClass . You can look up more on this in the Fabric docs.

The next step is to create a visualJson using such different objects. Use the code given below to do this:

{"visualJson": {
  "objects": [
// Array of fabric objects
  ],
  "version": "",
  "backgroundImage": {
// A fabric image object
  },
  "_canvasDimensions": {
    "width":1280,
    "height":860
  }
 }
}

We can make use of this JSON to render objects on the canvas and play animations. So the question remaining is how to animate these properties using _anim?

We can start by creating a timeline object with added keyframes to play animations. Using this, we can iterate through the array of objects and add keyframes to the timeline object. A simple implementation of this would look like:

const timeline = anime.timeline({
          easing: 'easeInOutQuad',
          duration: 10000,
          autoplay: false,
          update: () => {
// operations to perform everytime the timeline object changes
          },
          complete: () => {
// executed when timeline completes            
        });


// Adding objects to the timeline, 
 const animConfig = obj._anim;
 const keyframes = obj._anim.config.keyframes;
 timeline.add(
      {
        ...animConfig,
        keyframes,
        targets: obj,
        update: () => canvas.requestRenderAll()
      },
      animOffset
    );

In the above code snippet the first part creates a timeline object and then we add object keyframes to it.

Every time there’s an update to the object, the canvas has to be re-rendered to account for those changes. With the use of this, we can play a visualJson as an animation on this canvas. The part relating to loading the visualJson on the canvas is quite simple and can be achieved with the fabric canvas loadFromJSON method.

Updating the animations:

So far we went through how to play a visualJson (an array of fabric objects) as an animation. But, what if we want to update/create animations or delete existing animations/objects? How, do we account for these changes? Let’s try to question what happens to the visualJson when we perform any of these operation and not think about the timeline object for the time-being.

  • When we make changes on the Fabric object itself, the timeline object has no knowledge of these changes as they have already been initialised.
  • To make any changes to the animation object to ensure it renders properly on the canvas, the timeline object has to be reinitialised. Which means to create a different timeline object with new Fabric objects and their _anim properties.
  • This can be quite pedantic if not handled properly. Careful design considerations has to be made to make sure that any changes with respect to Fabric objects is captured.

Combining with videos:

In the last part of this article let’s look into how we can add videos as Fabric objects and manage them along with other animations. What we need to do with videos is play them based on their delay and the duration, i.e. based on the currentTime of the timeline object. We should also make sure to sync that with videos on the pause/play/restart sections of the timeline .

Conclusion

I hope helps you get a basic understanding of Fabric.js and anime.js working in tandem with each other. This should a good starting point for you to start making design decisions for complex functionalities. I’ve worked on a pretty extensive project that involves working with different sorts of animation using Fabric.js, anime.js and React with Typescript. So, if you have any questions/queries feel free to reach out to me on my Twitter handle. Hope you've had a great read!