Character Outlines for Lottie-web

Combining lottie-web with bodymovin, web developers can render After Effects animations on the web with ease. Lottie offers 3 ways of rendering animation on the web: SVG, canvas, and HTML.

Recently in one of my projects, I’m implementing a video editor based on canvas, which provides a feature that allows the user to edit text animation, as shown below.

dynamic text using lottie-web canvas renderer

The animation is exported from AE using bodymovin, and rendered with lottie’s canvas renderer. Now here’s the problem:

In this article, I’m writing down how I generate glyphs info while the user is typing and how to tell lottie-web that the characters info change.

BTW, if you have any questions or better ideas, please open an issue on my blog’s repo (since I havn’t found a new comment system to replace the defunct one).

The demo and code archive

The font that I’m using in the code archive is Bitstream Vera Fonts. It’s an open source font which can be redistributed. However, the font that I use to demonstrate the whole process in this article is Noto Sans CJK SC.

A really simple AE project

I created a very simple demo in After Effects. It was a composition containing only a text layer with font set to Bitstream Vera Sans Bold. There was nothing in the text area. Then I exported it using bodymovin and the data looks something like this.

{
  "v": "5.5.9", "fr": 25, "ip": 0, "op": 125,
  "w": 1200, "h": 700, "nm": "Comp 1", "ddd": 0,
  "assets": [],
  "fonts": {
    "list": [{
      "fName": "BitstreamVeraSans-Bold",
      "fFamily": "Bitstream Vera Sans",
      "fStyle": "Bold",
      "ascent": 75.9994506835938
    }]
  },
  "layers": [{...}],
  "markers": [],
  "chars": []
}

Notice that the chars array is empty.

You can download the code here. Run npm install and npm start. In the demo page, the canvas will update as you type text in the text input. The content in the canvas is rendered using lottie-web canvas renderer. Since the font only contains Latin characters, don’t type characters that are not included in the font.

Let’s see how it is done at the rest of this article.

Getting glyph data

The first step is to get a character’s glyph data from the font file in browser. Kudos to fontkit, its font.glyphForCodePoint method returns exactly what we want.

Webpack config for fontkit

In the demo mentioned before, I’m using webpack. In order to use fontkit in browser, the following webpack configuration is needed.

{
  node: {
    fs: "empty",
  },
  module: {
    rules: [
      {
        test: /fontkit[/\\]index.js$/,
        loader: 'transform-loader?brfs',
        enforce: 'post',
      },
      {
        test: /unicode-properties[/\\]index.js$/,
        loader: 'transform-loader?brfs',
        enforce: 'post',
      },
      {
        test: /linebreak[/\\]src[/\\]linebreaker.js/,
        loader: 'transform-loader?brfs',
        enforce: 'post',
      },
    ],
  },
}

Reading font as buffer

Fontkit provides 3 methods for loading a font. However, the fontkit.open and fontkit.openSync methods depend on fs module, which is only available in Node.js. In the browser, the only option left is fontkit.create, which expects a buffer.

Assume the font file is a webfont, and can be loaded using fetch API. The fetch API can parse what’s being fetched as a blob, not as a buffer though.

Silly browser. How do you convert it to a Buffer?

Something with a goofy FileReader thingy… Time to Google for it yet again… There must be a better way!

Yes. Let’s turn to blob-to-buffer module. And here’s all the code that you need.

import fontkit from 'fontkit';
import blobToBuffer from 'blob-to-buffer';

fetch('your_webfont_url')
.then(res => res.blob())
.then(blob => {
  blobToBuffer(blob, (err, buffer) => {
    if (err) {throw err;}
    const font = fontkit.create(buffer);
    const glyph = font.glyphForCodePoint('D'.charCodeAt(0));
    const d = glyph.path.toSVG();
    // do something with the glyph or its SVG path string
  });
});

Converting glyph data

The next step is to convert the glyph data returned by fonkit to the format used by lottie-web.

Time to investigate the glyph data format exported by bodymovin from After Effects.

chars array in a lottie file

There’s a setting named “Glyphs” in bodymovin extension. Tick that and hit render, the generated lottie file will have an array of glyphs converted from all the characters used in the After Effects composition.

I’ve made a very simple lottie file, with only 1 character and nothing else. The character is “D” using Noto Sans CJK SC Bold font. Here’s an excerpt.

{
  "chars": [
    {
      "ch": "D",
      "size": 60,
      "fFamily": "Noto Sans CJK SC",
      "style": "Bold",
      "w": 71.4,
      "data": {
        "shapes": [
          {
            "ty": "gr",
            "it": [],
            "nm": "D",
            "np": 5,
            "cix": 2,
            "bm": 0,
            "ix": 1,
            "mn": "ADBE Vector Group",
            "hd": false
          }
        ]
      }
    }
  ]
}

The it array of a character

The it array contains all the path data of a character.

path items

If the character is very simple, like Latin letter C, it array will only have 1 path item; if the character is a bit more complicated, like Chinese character , it array will contain 3 path items.

In my demo, character D has 2 path items.

{
  "it": [
    {
      "ind": 0,
      "ix": 1,
      "ks": { "a": 0, "k": {}, "ix": 2 },
      "ty": "sh",
      "nm": "D",
      "mn": "ADBE Vector Shape - Group",
      "hd": false,
    },
    {
      "ind": 1,
      "ix": 2,
      "ks": { "a": 0, "k": {}, "ix": 2 },
      "ty": "sh",
      "nm": "D",
      "mn": "ADBE Vector Shape - Group",
      "hd": false,
    }
  ]
}

For each item,

The vertices of the path are stored in ks object.

Cubic Bézier curve in lottie files

As I mentioned before, there’s a w property describing the character’s width when rendered at 100px. All points in it[i].ks.k property are coordinates of the glyph’s vertices relative to 100px, too. I’d like to point out that what we are going to discuss not only apply to characters’ data, but also applicable to all {"ty": "sh"} shapes.

vertices of shape explained in lottie files

It’s too complicated to express in words. So I made this image, which shows how the data are used to render character “D” of font “Noto Sans CJK SC Bold”. The data in the image are it[0].ks.k and it[1].ks.k. Please pay attention to the coordinate system, positive x to the right, positive y to the bottom. The coordinates of control points start with @, which are relative to their corresponding anchor points.

vertices of a character

When you draw cubic Bézier curve in SVG, using C or c command of <path /> tag’s d property, the relativity of the coordinates is either to the SVG document’s origin or to the previous anchor point. Apparently, lottie file takes neither way. The image above shows how the coordinate transformation is done between vertices from lottie file and vertices in a C command of SVG <path />.

BTW, if you need a refresher on SVG <path /> tag’s drawing commands, I recommend this article from MDN.

The final glue

Until now, we can get a glyph’s SVG path data using fontkit, and also know the relations between an SVG path and the glyph’s vertices in lottie file. There’re two more problems we need to tackle. Let’s juxtapose both path data of the character “D”.

cubic and quadratic Bézier curves

  1. The SVG path string returned by fontkit is a quadratic Bézier curve, whereas the SVG path we converted from lottie file is a cubic Bézier curve;
  2. The coordinate systems are somewhat different.

Luckily, a quadratic Bézier curve can be converted to a cubic Bézier curve without being noticed any loss in details.

I’m not going to dive into this process here, you can read this article from codepen. In my code archive, I’m using Adobe’s snap.svg library to do this.

vertices in 2 forms

As for the coordinate system, let me draw the two paths in one graph as shown above. The vertices of the SVG path got from fontkit is based on the font’s unitsPerEm property. And it is vertically flipped comparing to the one from lottie file. After invoking const font = fontkit.create(fontBuffer), you can access the font’s unitsPerEm using font.unitsPerEm, which is a number. Lots of fonts are designed at 1000px, some at 1024px, and there may also be other values. Let s = 100 / font.unitsPerEm, we can convert points of fontkit’s SVG path to lottie files’ using the following equation.

transformation from fontkit's coordinate system to lottie file's

Putting it together

Now we know everything about the relations between a glyph’s SVG string got from fontkit and the glyph’s path items in a lottie file. We can generate lottie glyph data while the user is typing in the browser.

  1. Load the font using fontkit;
  2. Listen for the keydown event;
  3. When user types a character, get the glyph’s SVG path string using fontkitGlyph.path.toSVG() method;
  4. Convert the quadratic Bézier curve from Step 3 to a cuvic Bézier curve;
  5. Transform the coordinate system using the equation from the previous section;
  6. Turn the SVG string to lottie shape’s data structure;
  7. Make a new character object and push it to lottieFileData.chars array;
  8. Add the character data to lottie canvas renderer using renderer.globalData.fontManager.addChars([newCharacterObject]);
  9. Rerender the current frame.

There’s a gotcha in Step 8 though. The coordinates of all anchor points of the curve in a lottie file are relative to the corresponding anchor points. But the method I mentioned in this step expects all coordinates relative to the origin. Recall the second image in “Cubic Bézier curve in lottie files” section, it expects the data on the rightmost side.

Wrapping up

I hope this article helps you solve the "Missing character from exported characters list" problem if you are using lottie canvas renderer and can’t export all glyphs from AE. Feel free to download the code and play with it. Let me know if you have any questions or better ideas. Have fun with the amazing lottie-web library.