原文: https://www.sitepoint.com/fabric-js-advanced/
结果如下:
----------------------------------------
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.
Groups
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.)
Figure 1. A Selection Becomes a Group in Fabric
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:
var text = new fabric.Text('hello world', { fontSize: 30 }); var circle = new fabric.Circle({ radius: 100, fill: '#eef', scaleY: 0.5 }); var group = new fabric.Group([ text, circle ], { left: 150, top: 100, angle: -10 }); canvas.add(group);
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.
Figure 2. A Group Created Programmatically
And now that we have a group on our canvas, let’s change it up a little:
group.item(0).set({ text: 'trololo', fill: 'white' }); group.item(1).setFill('red');
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 3shows the results.
Figure 3. Squeezed Red Circle with New Text
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.
var circle1 = new fabric.Circle({ radius: 50, fill: 'red', left: 0 }); var circle2 = new fabric.Circle({ radius: 50, fill: 'green', left: 100 }); var circle3 = new fabric.Circle({ radius: 50, fill: 'blue', left: 200 }); var group = new fabric.Group([ circle1, circle2, circle3 ], { left: 200, top: 100 }); canvas.add(group);
Figure 4. A Group with Three Circles Aligned Horizontally
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.
fabric.Image.fromURL('/assets/pug.jpg', function(img) { var img1 = img.scale(0.1).set({ left: 100, top: 100 }); fabric.Image.fromURL('/assets/pug.jpg', function(img) { var img2 = img.scale(0.1).set({ left: 175, top: 175 }); fabric.Image.fromURL('/assets/pug.jpg', function(img) { var img3 = img.scale(0.1).set({ left: 250, top: 250 }); canvas.add(new fabric.Group([ img1, img2, img3], { left: 200, top: 200 })) }); }); });
Figure 5. A Group with Three Images
Several other methods are available for working with groups:
- getObjects works exactly like fabric.Canvas#getObjects() and returns an array of all objects in a group
- size represents the number of objects in a group
- contains allows you to check whether a particular object is in a group
- item (which you saw earlier) allows you to retrieve a specific object from a group
- forEachObject also mirrors fabric.Canvas#forEachObject, but in relation to group objects
- add and remove add and remove objects from a group, respectively
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:
group.add(new fabric.Rect({ ... }));
To add a rectangle 100 px from the center of the group, do this:
group.add(new fabric.Rect({ ... left: 100, top: 100 }));
To add a rectangle at the center of a group and update the group’s dimensions, use the following code:
group.addWithUpdate(new fabric.Rect({ ... left: group.getLeft(), top: group.getTop() }));
To add a rectangle at 100 px off from the center of a group and update the group’s dimensions, do this:
group.addWithUpdate(new fabric.Rect({ ... left: group.getLeft() + 100, top: group.getTop() + 100 }));
Finally, if you want to create a group with objects that are already present on the canvas, you need to clone them first:
// create a group with copies of existing (2) objects var group = new fabric.Group([ canvas.item(0).clone(), canvas.item(1).clone() ]); // remove all objects and re-render canvas.clear().renderAll(); // add group onto canvas canvas.add(group);
Serialization
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.
toObject, toJSON
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:
var canvas = new fabric.Canvas('c'); JSON.stringify(canvas); // '{"objects":[],"background":"rgba(0, 0, 0, 0)"}'
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:
canvas.backgroundColor = 'red'; JSON.stringify(canvas); // '{"objects":[],"background":"red"}'
As you would expect, the canvas representation reflects the new background color. Now let’s add some objects:
canvas.add(new fabric.Rect({ left: 50, top: 50, height: 20, width: 20, fill: 'green' })); console.log(JSON.stringify(canvas));
The logged output is as follows:
'{"objects":[{"type":"rect","left":50,"top":50,"width":20,"height":20,"fill":"green","overlayFill":null,
"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,
"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,
"perPixelTargetFind":false,"rx":0,"ry":0}],"background":"rgba(0, 0, 0, 0)"}'
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:
canvas.add(new fabric.Circle({ left: 100, top: 100, radius: 50, fill: 'red' })); console.log(JSON.stringify(canvas));
Here’s the logged output now:
'{"objects":[{"type":"rect","left":50,"top":50,"width":20,"height":20,"fill":"green","overlayFill":null,
"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,
"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,
"perPixelTargetFind":false,"rx":0,"ry":0},"type":"circle","left":100,"top":100,"width":100,"height":100,"fill":"red",
"overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,
"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,
"transparentCorners":true,"perPixelTargetFind":false,"radius":50}],"background":"rgba(0, 0, 0, 0)"}'
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')
:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAK8CAYAAAAXo9vkAAAgAElEQVR4Xu3dP4xtBbnG4WPAQOQ2YBCLK1qpoQE1
/m+NVlCDwUACicRCEuysrOwkwcJgAglEItRQaWz9HxEaolSKtxCJ0FwMRIj32zqFcjm8e868s2fNWoJygl+e397rWetk5xf5pyZd13wPwIEC
BAgQIAAAQIECBxI4F0H+hwfQ4AAAQIECBAgQIAAgQsCxENAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQIDAw
QQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQI
ECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABA
gQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBq
H0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABA
gLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQ
IDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECB
AgQIECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAg
AABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECyw+Qb134RU2fevC8q+5esGWESBAgAABAgQIEFiOwPLMC5AlvO0OBMCBAgQIECAAAECJxQ
QICcE9HYCBAgQIECAAAECBPYXECD7W3klAQIECBAgQIAAAQInFBAgJwT0dgIECBAgQIAAAQIE9hcQIPtbeSUBAgQIECBAgAABAicUECAnBPR
2AgQIECBAgAABAgT2FxAg+1t5JQECBAgQIECAAAECJxQQICcE9HYCBAgQIECAAAECBPYXECD7W3klAQIECBAgQIAAAQInFBAgJwTc9+3z49y
vmNd+dI7PzPHJOW6Y4wNzXD3HlXNc9pZdb85/vzbHK3P8aY7n5vj1HL+Y43dz417f97O9jgABAgQIECBAgMBSBATIKd2JCY5dWNwyx5fn+Pw
cV5U/6tXZ99M5fjjHk3Mjd6HifwQIECBAgAABAgQWLSBAirdnouP6WXfvHHfOcU1x9T6rXp4XPTLHA3NTX9jnDV5DgAABAgQIECBA4NACAuS
E4hMdl8+Kr83xzTmuO+G61ttfnEXfnuN7c4PfaC21hwABAgQIECBAgMBJBQTIJQpOeFw7b71/jtsvccWh3vbYfNB9c6NfOtQH+hwCBAgQIEC
AAAECFxMQIMd8No7C4+F5283HfOtZv/ypOYG7hMhZ3wafT4AAAQIECBDYtoAA2fP+H/1Vqwd3f4jf8y1Lfdkunu7xV7OWenucFwECBAgQIEB
g3QICZI/7O/Fxx7xs9wf3t36r3D3evciX7L7F7+6rIY8u8uycFAECBAgQIE
…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:
{ "background" : "rgba(0, 0, 0, 0)", "objects" : [ { "angle" : 0, "fill" : "green", "flipX" : false, "flipY" : false, "hasBorders" : true, "hasControls" : true, "hasRotatingPoint" : false, "height" : 20, "left" : 50, "opacity" : 1, "overlayFill" : null, "perPixelTargetFind" : false, "scaleX" : 1, "scaleY" : 1, "selectable" : true, "stroke" : null, "strokeDashArray" : null, "strokeWidth" : 1, "top" : 50, "transparentCorners" : true, "type" : "rect", "width" : 20 } ] }
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:
var rect = new fabric.Rect(); rect.toObject = function() { return { name: 'trololo' }; }; canvas.add(rect); console.log(JSON.stringify(canvas));
The logged output is:
'{"objects":[{"name":"trololo"}],"background":"rgba(0, 0, 0, 0)"}'
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:
var rect = new fabric.Rect(); rect.toObject = (function(toObject) { return function() { return fabric.util.object.extend(toObject.call(this), { name: this.name }); }; })(rect.toObject); canvas.add(rect); rect.name = 'trololo'; console.log(JSON.stringify(canvas));
And here’s the logged output:
'{"objects":[{"type":"rect","left":0,"top":0,"width":0,"height":0,"fill":"rgb(0,0,0)","overlayFill":null,
"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,
"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,
"transparentCorners":true,"perPixelTargetFind":false,"rx":0,"ry":0,"name":"trololo"}],
"background":"rgba(0, 0, 0, 0)"}'
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.
toSVG
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:
canvas.add(new fabric.Rect({ left: 50, top: 50, height: 20, width: 20, fill: 'green' })); console.log(canvas.toSVG());
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:
var canvas = new fabric.Canvas(); canvas.loadFromJSON('{"objects":[{"type":"rect","left":50,"top":50,"width":20,"height":20, fill":"green","overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1, "scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true, "hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false, "rx":0,"ry":0},"type":"circle","left":100,"top":100,"width":100,"height":100,"fill":"red", "overlayFill":null,"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1, "angle":0,"flipX":false,"flipY":false,"opacity":1,"selectable":true,"hasControls":true, "hasBorders":true,"hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false, "radius":50}],"background":"rgba(0, 0, 0, 0)"}');
Both objects magically appear on canvas, as shown in Figure 6.
Figure 6. A Circle and a Square Rendered on Canvas
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.
Figure 7. A Complex Shape Rendered on Canvas
JSON.stringify(canvas) output for the shape in Figure 7 is as follows:
{"objects":[{"type":"path","left":184,"top":177,"width":175,"height":151,"fill":"#231F20","overlayFill":null,
"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":-19,"flipX":false,
"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,
"transparentCorners":true,"perPixelTargetFind":false,"path":[["M",39.502,61.823],["c",-1.235,-0.902,-3.038,
-3.605,-3.038,-3.605],["s",0.702,0.4,3.907,1.203],["c",3.205,0.8,7.444,-0.668,10.114,-1.97],["c",2.671,-1.302,
7.11,-1.436,9.448,-1.336],["c",2.336,0.101,4.707,0.602,4.373,2.036],["c",-0.334,1.437,-5.742,3.94,-5.742,3.94],
["s",0.4,0.334,1.236,0.334],["c",0.833,0,6.075,-1.403,6.542,-4.173],["s",-1.802,-8.377,-3.272,-9.013],["c",-1.468,
-0.633,-4.172,0,-4.172,0],["c",4.039,1.438,4.941,6.176,4.941,6.176],["c",-2.604,-1.504,-9.279,-1.234,-12.619,
0.501],["c",-3.337,1.736,-8.379,2.67,-10.083,2.503],["c",-1.701,-0.167,-3.571,-1.036,-3.571,-1.036],["c",1.837,
0.034,3.239,-2.669,3.239,-2.669],["s",-2.068,2.269,-5.542,0.434],["c",-3.47,-1.837,-1.704,-8.18,-1.704,-8.18],
["s",-2.937,5.909,-1,9.816],["C",34.496,60.688,39.502,61.823,39.502,61.823],["z"],["M",77.002,40.772],["c",0,0,
-1.78,-5.03,-2.804,-8.546],["l",-1.557,8.411],["l",1.646,1.602],["c",0,0,0,-0.622,-0.668,-1.691],["C",72.952,
39.48,76.513,40.371,77.002,40.772],["z"],["M",102.989,86.943],["M",102.396,86.424],["c",0.25,0.22,0.447,0.391,
0.594,0.519],["C",102.796,86.774,102.571,86.578,102.396,86.424],["z"],["M",169.407,119.374],["c",-0.09,-5.429,
-3.917,-3.914,-3.917,-2.402],["c",0,0,-11.396,1.603,-13.086,-6.677],["c",0,0,3.56,-5.43,1.69,-12.461],["c",
-0.575,-2.163,-1.691,-5.337,-3.637,-8.605],["c",11.104,2.121,21.701,-5.08,19.038,-15.519],["c",-3.34,-13.087,
-19.63,-9.481,-24.437,-9.349],["c",-4.809,0.135,-13.486,-2.002,-8.011,-11.618],["c",5.473,-9.613,18.024,-5.874,
18.024,-5.874],["c",-2.136,0.668,-4.674,4.807,-4.674,4.807],["c",9.748,-6.811,22.301,4.541,22.301,4.541],["c",
-3.097,-13.678,-23.153,-14.636,-30.041,-12.635],["c",-4.286,-0.377,-5.241,-3.391,-3.073,-6.637],["c",2.314,
-3.473,10.503,-13.976,10.503,-13.976],["s",-2.048,2.046,-6.231,4.005],["c",-4.184,1.96,-6.321,-2.227,-4.362,
-6.854],["c",1.96,-4.627,8.191,-16.559,8.191,-16.559],["c",-1.96,3.207,-24.571,31.247,-21.723,26.707],["c",
2.85,-4.541,5.253,-11.93,5.253,-11.93],["c",-2.849,6.943,-22.434,25.283,-30.713,34.274],["s",-5.786,19.583,
-4.005,21.987],["c",0.43,0.58,0.601,0.972,0.62,1.232],["c",-4.868,-3.052,-3.884,-13.936,-0.264,-19.66],["c",
3.829,-6.053,18.427,-20.207,18.427,-20.207],["v",-1.336],["c",0,0,0.444,-1.513,-0.089,-0.444],["c",-0.535,
1.068,-3.65,1.245,-3.384,-0.889],["c",0.268,-2.137,-0.356,-8.549,-0.356,-8.549],["s",-1.157,5.789,-2.758,
5.61],["c",-1.603,-0.179,-2.493,-2.672,-2.405,-5.432],["c",0.089,-2.758,-1.157,-9.702,-1.157,-9.702],["c",
-0.8,11.75,-8.277,8.011,-8.277,3.74],["c",0,-4.274,-4.541,-12.82,-4.541,-12.82],["s",2.403,14.421,-1.336,
14.421],["c",-3.737,0,-6.944,-5.074,-9.879,-9.882],["C",78.161,5.874,68.279,0,68.279,0],["c",13.428,16.088,
17.656,32.111,18.397,44.512],["c",-1.793,0.422,-2.908,2.224,-2.908,2.224],["c",0.356,-2.847,-0.624,-7.745,
-1.245,-9.882],["c",-0.624,-2.137,-1.159,-9.168,-1.159,-9.168],["c",0,2.67,-0.979,5.253,-2.048,9.079],["c",
-1.068,3.828,-0.801,6.054,-0.801,6.054],["c",-1.068,-2.227,-4.271,-2.137,-4.271,-2.137],["c",1.336,1.783,
0.177,2.493,0.177,2.493],["s",0,0,-1.424,-1.601],["c",-1.424,-1.603,-3.473,-0.981,-3.384,0.265],["c",0.089,
1.247,0,1.959,-2.849,1.959],["c",-2.846,0,-5.874,-3.47,-9.078,-3.116],["c",-3.206,0.356,-5.521,2.137,-5.698,
6.678],["c",-0.179,4.541,1.869,5.251,1.869,5.251],["c",-0.801,-0.443,-0.891,-1.067,-0.891,-3.473],...
…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:
canvas.item(0).sourcePath = '/assets/dragon.svg'; console.log(JSON.stringify(canvas.toDatalessJSON()));
Here’s the logged output:
{"objects":[{"type":"path","left":143,"top":143,"width":175,"height":151,"fill":"#231F20","overlayFill":null,
"stroke":null,"strokeWidth":1,"strokeDashArray":null,"scaleX":1,"scaleY":1,"angle":-19,"flipX":false,
"flipY":false,"opacity":1,"selectable":true,"hasControls":true,"hasBorders":true,"hasRotatingPoint":false,
"transparentCorners":true,"perPixelTargetFind":false,"path":"/assets/dragon.svg"}],"background":"rgba(0, 0, 0, 0)"}
That’s certainly smaller, so what happened? Notice that before calling toDatalessJSO
N, I gave the path (dragon shape) object a sourcePat
h 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.
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:
fabric.loadSVGFromString('...', function(objects, options) { var obj = fabric.util.groupSVGElements(objects, options); canvas.add(obj).renderAll(); });
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.groupSVGElement
s 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.
Subclassing
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?
For this task you need the fabric.util.createClass
utility method. This method is nothing but a simple abstraction over JavaScript’s prototypal inheritance. Let’s first create a simple Point “class”:
var Point = fabric.util.createClass({ initialize: function(x, y) { this.x = x || 0; this.y = y || 0; }, toString: function() { return this.x + '/' + this.y; } });
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:
var point = new Point(10, 20); point.x; // 10 point.y; // 20 point.toString(); // "10/20"
If we wanted to create a child of “Point” class—say a colored point—we would use createClass
like so:
var ColoredPoint = fabric.util.createClass(Point, { initialize: function(x, y, color) { this.callSuper('initialize', x, y); this.color = color || '#000'; }, toString: function() { return this.callSuper('toString') + ' (color: ' + this.color + ')'; } });
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:
var redPoint = new ColoredPoint(15, 33, '#f55'); redPoint.x; // 15 redPoint.y; // 33 redPoint.color; // "#f55" redPoint.toString(); "15/35 (color: #f55)"
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.
var LabeledRect = fabric.util.createClass(fabric.Rect, { type: 'labeledRect', initialize: function(options) { options || (options = { }); this.callSuper('initialize', options); this.set('label', options.label || ''); }, toObject: function() { return fabric.util.object.extend(this.callSuper('toObject'), { label: this.get('label') }); }, _render: function(ctx) { this.callSuper('_render', ctx); ctx.font = '20px Helvetica'; ctx.fillStyle = '#333'; ctx.fillText(this.label, -this.width/2, -this.height/2 + 20); } });
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 toObjec
t 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.
var labeledRect = new LabeledRect({ width: 100, height: 50, left: 100, top: 100, label: 'test', fill: '#faa' }); canvas.add(labeledRect);
Figure 8. Rendering of labeledRect
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.
labeledRect.set({ label: 'trololo', fill: '#aaf', rx: 10, ry: 10 }
Figure 9. Modified labeledRect
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:
... initialize: function(options) { options || (options = { }); this.callSuper('initialize', options); // give all labeled rectangles fixed width/height of 100/50 this.set({ width: 100, height: 50 }); this.set('label', options.label || ''); } ... _render: function(ctx) { // make font and fill values of labels configurable ctx.font = this.labelFont; ctx.fillStyle = this.labelFill; ctx.fillText(this.label, -this.width/2, -this.height/2 + 20); } ...
Wrapping Up
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.