Introduction to Fabric.js: Part 3
Juriy Zaytsev | December 17, 2012
We’ve covered most of the basics of Fabric in the first and second parts of this series. In this article, I’ll move on to more advanced features: groups, serialization (and deserialization) and classes.
The first topic I’ll talk about is groups, one of Fabric’s most powerful features. Groups are exactly what they sound like—a simple way to group Fabric objects into a single entity so that you can work with those objects as a unit. (See Figure 1.)
Remember that any number of Fabric objects on the canvas can be grouped with the mouse to form a single selection. Once grouped, the objects can be moved and even modified as one. You can scale the group, rotate it, and even change its presentational properties—its color, transparency, borders, and so on.
Every time you select objects like this on the canvas, Fabric creates a group implicitly, behind the scenes. Given this, it only makes sense to provide access to groups programmatically, which is where fabric.Group comes in.
Let’s create a group from two objects, a circle and text:
First, I created a "hello world" text object. Then, I created a circle with a 100 px radius, filled with the "#eef" color and squeezed vertically (scaleY=0.5). I next created a fabric.Group instance, passing it an array with these two objects and giving it a position of 150/100 at a -10 degree angle. Finally, I added the group to the canvas, as I would with any other object, by using canvas.add().
Voila! You see an object on the canvas as shown in Figure 2, a labeled ellipse, and can now work with this object as a single entity. To modify that object, you simply change properties of the group, here giving it custom left, top and angle values.
And now that we have a group on our canvas, let’s change it up a little:
Here, we access individual objects in a group via the item method and modify their properties. The first object is the text, and the second is the squeezed circle. Figure 3 shows the results.
One important idea you’ve probably noticed by now is that objects in a group are all positioned relative to the center of the group. When I changed the text property of the text object, it remained centered even when I changed its width. If you don’t want this behavior, you need to specify an object’s left/top coordinates, in which case they’ll be grouped according to those coordinates.
Here’s how to create and group three circles so that they’re positioned horizontally one after the other, such as those shown in Figure 4.
Another point to keep in mind when working with groups is the state of the objects. For example, when forming a group with images, you need to be sure those images are fully loaded. Since Fabric already provides helper methods for ensuring that an image is loaded, this operation becomes fairly easy, as you can see in this code and in Figure 5.
Several other methods are available for working with groups:
You can add or remove objects with or without updating group dimensions and position. Here are several examples:
To add a rectangle at the center of a group (left=0, top=0), use this code:
To add a rectangle 100 px from the center of the group, do this:
To add a rectangle at the center of a group and update the group’s dimensions, use the following code:
To add a rectangle at 100 px off from the center of a group and update the group’s dimensions, do this:
Finally, if you want to create a group with objects that are already present on the canvas, you need to clone them first:
As soon as you start building a stateful application of some sort—perhaps one that allows users to save results of canvas contents on a server or streaming contents to a different client—you need canvas serialization. There’s always an option to export the canvas to an image, but uploading a large image to a server requires a lot of bandwidth. Nothing beats text when it comes to size, and that’s exactly why Fabric provides an excellent support for canvas serialization and deserialization.
The backbone of canvas serialization in Fabric are the fabric.Canvas#toObject and fabric.Canvas#toJSON methods. Let’s take a look at a simple example, first serializing an empty canvas:
Here I’m using the ES5 JSON.stringify method, which implicitly calls the toJSON method on the passed object if that method exists. Because a canvas instance in Fabric has a toJSON method, it’s as though we called JSON.stringify(canvas.toJSON()) instead.
Notice the returned string that represents the empty canvas. It’s in JSON format, and essentially consists of "objects" and "background" properties. The "objects" property is currently empty because there’s nothing on the canvas, and "background" has a default transparent value ("rgba(0, 0, 0, 0)").
Let’s give our canvas a different background and see how things change:
As you would expect, the canvas representation reflects the new background color. Now let’s add some objects:
The logged output is as follows:
Wow! At first sight, quite a lot has changed, but looking more closely, you can see that the newly added object is now part of the "objects" array, serialized into JSON. Notice how its representation includes all its visual traits—left, top, width, height, fill, stroke and so on.
If we were to add another object—say, a red circle positioned next to the rectangle—you would see that the representation changed accordingly:
Here’s the logged output now:
Notice the "type":"rect" and "type":"circle" parts so that you can see better where those objects are. Even though it might seem like a lot of output at first, it is nothing compared to what you would get with image serialization. Just for fun, take a look at about one-tenth (!) of a string you would get with canvas.toDataURL('png'):
...and there’s approximately 17,000 characters more.
You might be wondering why there's also fabric.Canvas#toObject. Quite simply, toObject returns the same representation as toJSON, only in a form of the actual object, without string serialization. For example, using the earlier example of a canvas with just a green rectangle, the output for canvas.toObject is as follows:
As you can see, toJSON output is essentially stringified toObject output. Now, the interesting (and useful) thing is that toObject output is smart and lazy. What you see inside an "objects" array is the result of iterating over all canvas objects and delegating to each object’s own toObject method. For example, fabric.Path has its own toObject that knows to return path’s "points" array, and fabric.Image has a toObject that knows to return image’s "src" property. In true object-oriented fashion, all objects are capable of serializing themselves.
This means that when you create your own class, or simply need to customize an object's serialized representation, all you need to do is work with the toObject method, either completely replacing it or extending it. Here’s an example:
The logged output is:
As you can see, the objects array now has a custom representation of our rectangle. This kind of override brings the point across but is probably not very useful. Instead, here’s how to extend a rectangle's toObject method with an additional property:
And here’s the logged output:
I extended the object's existing toObject method with the additional property "name", which means that property is now part of the toObject output, and as a result it appears in the canvas JSON representation. One other item worth mentioning is that if you extend objects like this, you'll also want to be sure the object's "class" (fabric.Rect in this case) has this property in the "stateProperties" array so that loading a canvas from a string representation will parse and add it to an object correctly.
Another efficient text-based canvas representation is in SVG format. Since Fabric specializes in SVG parsing and rendering on canvas, it makes sense to make this a two-way process and provide canvas-to-SVG conversion. Let's add the same rectangle to our canvas and see what kind of representation is returned from the toSVG method:
The logged output is as follows:
Just like with toJSON and toObject, the toSVG method—when called on canvas—delegates its logic to each individual object, and each individual object has its own toSVG method that is special to the type of object. If you ever need to modify or extend an SVG representation of an object, you can do the same thing with toSVG as I did earlier with toObject.
The benefit of SVG representation, compared to Fabric's proprietary toObject/toJSON, is that you can throw it into any SVG-capable renderer (browser, application, printer, camera, and so on), and it should just work. With toObject/toJSON, however, you first need to load it onto a canvas.
And speaking of loading things onto a canvas, now that you know how to serialize a canvas into an efficient chunk of text, how do you go about loading this data back onto canvas?
Deserialization and the SVG Parser
As with serialization, there's two ways to load a canvas from a string: from JSON representation or from SVG. When using JSON representation, there are the fabric.Canvas#loadFromJSON and fabric.Canvas#loadFromDatalessJSON methods. When using SVG, there are fabric.loadSVGFromURL and fabric.loadSVGFromString.
Notice that the first two methods are instance methods and are called on a canvas instance directly, whereas the other two methods are static methods and are called on the "fabric" object rather than on canvas.
There's not much to say about most of these methods. They work exactly as you would expect them to. Let's take as an example the previous JSON output from our canvas and load it on a clean canvas:
Both objects magically appear on canvas, as shown in Figure 6.
So loading canvas from a string is pretty easy, but what about that strange-looking loadFromDatalessJSON method? How is it different from loadFromJSON, which we just used? To understand why you need this method, look at a serialized canvas that has a more or less complex path object, like the one shown in Figure 7.
JSON.stringify(canvas) output for the shape in Figure 7 is as follows:
...and that's only 20 percent of the entire output!
What's going on here? Well, it turns out that this fabric.Path instance—this shape—consists of literally hundreds of Bezier lines dictating how exactly it is to be rendered. All those ["c",0,2.67,-0.979,5.253,-2.048,9.079] chunks in JSON representation correspond to each one of those curves. And when there's hundreds (or even thousands) of them, the canvas representation ends up being quite enormous.
Situations like these are where fabric.Canvas#toDatalessJSON comes in handy. Let's try it:
Here’s the logged output:
That's certainly smaller, so what happened? Notice that before calling toDatalessJSON, I gave the path (dragon shape) object a sourcePath property of "/assets/dragon.svg". Then, when I called toDatalessJSON, the entire humongous path string from the previous output (those hundreds of path commands) is replaced with a single "dragon.svg" string. You can see it highlighted above.
When you’re working with lots of complex shapes, toDatalessJSON allows you to reduce canvas representation even further and replace huge path data representations with a simple link to SVG.
You can probably guess that the loadFromDatalessJSON method simply allows you to load a canvas from a data less version of a canvas representation. The loadFromDatalessJSON method pretty much knows how to take those "path" strings (like "/assets/dragon.svg"), load them, and use them as the data for corresponding path objects.
Now, let's take a look at SVG-loading methods. We can use either string or URL. Let’s look at the string example first:
The first argument is the SVG string, and the second is the callback function. The callback is invoked when SVG is parsed and loaded and receives two arguments—objects and options. The first, objects, contains an array of objects parsed from SVG—paths, path groups (for complex objects), images, text, and so on. To group those objects into a cohesive collection—and to make them look the way they do in an SVG document—we're using fabric.util.groupSVGElements and passing it both objects and options. In return, we get either an instance of fabric.Path or fabric.PathGroup, which we can then add onto our canvas.
The fabric.loadSVGFromURL method works the same way, except that you pass a string containing a URL rather than SVG contents. Note that Fabric will attempt to fetch that URL via XMLHttpRequest, so the SVG needs to conform to the usual SOP rules.
Since Fabric is built in a truly object-oriented fashion, it's designed to make subclassing and extension simple and natural. As described in the first article in this series, there's an existing hierarchy of objects in Fabric. All two-dimensional objects (paths, images, text, and so on) inherit from fabric.Object, and some "classes"—like fabric.PathGroup — even form a third-level inheritance.
So how do you go about subclassing one of the existing "classes" in Fabric, or maybe even creating a class of your own?
The createClass method takes an object and uses that object's properties to create a class with instance-level properties. The only specially treated property is initialize, which is used as a constructor. Now, when initializing Point, we'll create an instance with x and y properties and the toString method:
If we wanted to create a child of "Point" class—say a colored point—we would use createClass like so:
Notice how the object with instance-level properties is now passed as a second argument. And the first argument receives Point "class", which tells createClass to use it as a parent class of this one. To avoid duplication, we're using the callSuper method, which calls the method of a parent class. This means that if we were to change Point, the changes would also propagate to the ColoredPoint class.
Here’s ColoredPoint in action:
Now let’s see how to work with existing Fabric classes. For example, let’s create a LabeledRect class that will essentially be a rectangle that has some kind of label associated with it. When rendered on our canvas, that label will be represented as a text inside a rectangle (similar to the earlier group example with a circle and text). As you're working with Fabric, you'll notice that combined abstractions like this can be achieved either by using groups or by using custom classes.
It looks like there's quite a lot going on here, but it's actually pretty simple. First, we're specifying the parent class as fabric.Rect, to utilize its rendering abilities. Next, we define the type property, setting it to "labeledRect". This is just for consistency, because all Fabric objects have the type property (rect, circle, path, text, and so on.) Then there's the already-familiar constructor (initialize), in which we utilize callSuper once again. Additionally, we set the object's label to whichever value was passed via options. Finally, we're left with two methods—toObject and _render. The toObject method, as you already know from the serialization section, is responsible for object (and JSON) representation of an instance. Since LabeledRect has the same properties as regular rect but also a label, we're extending the parent's toObject method and simply adding a label into it. Last but not least, the _render method is what's responsible for the actually drawing of an instance. There's another callSuper call in it, which is what renders rectangle, and an additional three lines of text-rendering logic.
If you were to render such object, you do something like the following. Figure 8 shows the results.
Changing the label value or any of the other usual rectangle properties would obviously work as expected, as you can see here and in Figure 9.
Of course, at this point you’re free to modify the behavior of this class anyway you want. For example, you could make certain values the default values to avoid passing them every time to the constructor, or you could make certain configurable properties available on the instance. If you do make additional properties configurable, you might want to account for them in toObject and initialize, as I’ve shown here:
That closes the third installment of this series, in which I’ve dived into some of the more advanced aspects of Fabric. With help from groups, serialization and deserialization and classes, you can take your app to a whole new level.
About the Author
Find Juriy on: