Creating custom objects in Fabric.js

Introduction

The Fabric.js library is a great tool for creating graphical editors in the web. It has many built in features like the controls editing, selection, rotation, scaling, filters and many more features that come handy when creating graphical editors. Moreover developers can create their own objects. In this article we will focus on creating custom objects using the Fabric.js API.

The basics of creating custom objects

Fabric.js uses a class system for all its objects. Every object is a child of the fabric.Object class. To create our own object we have to extend the fabric.Object class or one of already defined objects. We do it by using the fabric.util.createClass function. Take a look at the listing below to see how to use it.

fabric.CustomObject = fabric.util.createClass(fabric.Object, {
    type: 'customobject',
    
    initialize: function () {
    },
    
    _render: function (ctx) {
    }
});

As you can see, we pass the parent class (fabric.Object) object as a first argument of the fabric.util.createClass function. As a second argument, we pass our new object definition. Actually there is only one required field that has to be defined, it's the _render method. It draws a given object onto the canvas using a context object passed as a first argument. However we also want to pass the type property that gives a name to our class and the initialize function that is the class constructor. From every class function we can call the this.callSuper method that executes the parent class function. In the code below, you can see how to use this function.

fabric.CustomObject = fabric.util.createClass(fabric.Object, {
    type: 'customobject',
    
    initialize: function (options) {
        options = options || {};
        
        this.callSuper('initialize', options);
    }
});

We pass the parents' function name as a first argument of the callSuper function and any other paramaters, options as further arguments. In the code above, we've called the parent constructor and passed an options argument, which is initialized with an empty object if not defined.

Creating the BitmapText class

Let's create our own class for displaying text using bitmap fonts. We could create a FNT format reader and display text from such source but it's out of the scope of this article. We will create our own, very simple format of bitmap fonts without any extra options like kerning.

Bitmap fonts are a very nice replacement for standard font rendering. Sometimes it's hard to apply special effects to text, like gradients, glow effects etc. So instead adding such effects programmatically, we can just prepare a bitmap font where such effects are already assigned to the letters. Of course, such approach has some limitations, like the lack of the possibility to change the font color.

Here is the code of our BitmapClass class which we will discuss in details.

fabric.BitmapText = fabric.util.createClass(fabric.Image, {
  type: 'bitmaptext',

  chars: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
  text: '',
  charWidth: 0,
  charHeight: 0,

  initialize: function (element, options) {
    options = options || {};

    this.callSuper('initialize', element, options);

    this._setWidthHeight();
  },

  _setWidthHeight: function () {
    if (!this.getElement()) return;
    if (this.chars.length === 0) return;

    this.charWidth = this.getElement().width / this.chars.length;
    this.charHeight = this.getElement().height;

    this.width = this.charWidth * this.text.length;
    this.height = this.charHeight;
  },

  _renderChar: function (ctx, char, x, y) {
    var charIndex = this.chars.indexOf(char);

    ctx.drawImage(
      this.getElement(),
      charIndex * this.charWidth, 0, this.charWidth, this.charHeight,
      x, y, this.charWidth, this.charHeight);
  },

  _render: function (ctx) {
    this._setWidthHeight();

    var x = -this.width / 2;
    var y = -this.height / 2;

    Array.prototype.forEach.call(this.text, function (char, index) {
      this._renderChar(ctx, char, x + (index * this.charWidth), y);
    }, this);
  }
});

As you can see we don't extend the fabric.Object class but the fabric.Image. It's because we want some image features that are already defined there, and there is no point to rewrite them again in our class.

Properties

After defining our class type ("bitmaptext") we have the chars property. This property tells our object what chars we have defined in our font image. The font image should have each letter placed one next to another and each letter should take the same amount of space. You can see below an example of a font image.

The order of letters in the chars property should correspond to the letter position defined in the font image.

The next field named text is the actual text that will be displayed using the font.

Properties named charWidth and charHeight are automaticaly calculated and represent the width and height of one char in the font image. It's calculated inside the _setWidthHeight function where the image width is divided by length of the chars property.

Let's take a closer look at the class constructor (initialize function). We take two arguments, the first one is a image font object and the second one is the options object. We call the parent constructor by passing both options to the fabric.Image constructor. This constructor will set the image as an element property. The last thing we need to do in the constructor of the BitmapText class, is to call the this._setWidthHeight() function.

Methods

The _setWidthHeight method is responsible for setting width and height of the instance of a BitmapText class and setting the width and height char in the bitmap image.

_setWidthHeight: function () {
  if (!this.getElement()) return;
  if (this.chars.length === 0) return;

  this.charWidth = this.getElement().width / this.chars.length;
  this.charHeight = this.getElement().height;

  this.width = this.charWidth * this.text.length;
  this.height = this.charHeight;
},

At first we check if all the required properties are set. Then we terminate the execution if there is no image font or the chars property is empty.

Next we calculate the char width and height of the char inside the image font. The process of calculation had already been described above.

The last thing we do is setting width and height of our object. To do that, we just multiply the char width by number of letters in the text property. The object height is simpler, bacuse it's just the char height. One thing worth mentioning here is that we can't create multiline text using our class.

Let's move on to the _render function. At first, we run the _setWidthHeight function for one more time. We have to make sure that the char width and height are set.

A very important thing here to mention is that in the _render function, the canvas has already been translated to the center of the object which is being drawn. So anything we draw at the point 0, 0 will be drawn in the center of the object. If we want to start rendering from the left, top point we need to calculate that point first. Here is how we do it.

var x = -this.width / 2;
var y = -this.height / 2;

Next, we loop through each letter of our text and display each indivudial letter using the _renderChar function.

Array.prototype.forEach.call(this.text, function (char, index) {
  this._renderChar(ctx, char, x + (index * this.charWidth), y);
}, this);

The _renderChar function gets four arguments: context, char to render and x, y positon of the left, top corner of the text. At first we calculate the position of the char in the font image.

var charIndex = this.chars.indexOf(char);

Later, we draw part of the font image onto the canvas using ctx.drawImage function.

ctx.drawImage(
  this.getElement(),
  charIndex * this.charWidth, 0, this.charWidth, this.charHeight,
  x, y, this.charWidth, this.charHeight);

This is everything that is needed to display a bitmap font.

We have also created a utility function for loading an image from the url and creating a BitmapText object when the loading is finished.

fabric.BitmapText.fromURL = function (url, callback, options) {
  fabric.util.loadImage(url, function (img) {
    callback(new fabric.BitmapText(img, options));
  }, null, options && options.crossOrigin);
};

Usage

To create and display an object of the BitmapText class try the following code.

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

fabric.BitmapText.fromURL('images/font.png', function (text) {
  canvas.add(text);
}, {
  text: 'Example text'
});

We used our helper function to load a font and create a BitmapFont object automaticaly. We get this object as a first argument of a callback function (text argument) and add it to the canvas. As a third argument of the fromURL function, we pass an object containing options that will be set on the object. The only option we set is the text to display. In this example it is the "Example text" string.