原文:https://www.sitepoint.com/fabric-js-advanced/

<html>
<head>
<script src='./js/fabric.min.js'></script>
</head>
<body>
<canvas id="c" style="border:1px solid red;"></canvas>
<script>
var canvas = new fabric.Canvas('c',{backgroundColor : "#0ff",width: '600',height: '600'});
var LabeledRect = fabric.util.createClass(fabric.Rect, {
type: 'labeledRect',
initialize: function(options) {
options || (options = { });
this.callSuper('initialize', options);
this.set('label', options.label || '');
this.set('labelFont', options.labelFont || '');
this.set('labelFill', options.labelFill || '');
},
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'), {
label: this.get('label'),
labelFont: this.get('labelFont'),
labelFill: this.get('labelFill')
});
},
_render: function(ctx) {
this.callSuper('_render', ctx);
// ctx.font = '20px Helvetica';
// ctx.fillStyle = '#333';
console.log('this', this);
ctx.font = this.labelFont;
ctx.fillStyle = this.labelFill;
// ctx.fillText(this.label, -this.width/2, -this.height/2 + 20);
ctx.fillText(this.label, 0, 0+10);
}
}); var labeledRect = new LabeledRect({
width: 100,
height: 50,
left: 100,
top: 100,
label: 'test',
fill: '#faa',
labelFont: '30px Helvetica',
labelFill: '#00ff00'
});
canvas.add(labeledRect);
setTimeout(function(){
labeledRect.set({
label: 'trololo',
fill: '#aaf',
rx: 10,
ry: 10,
labelFill: '#0000ff'
});
canvas.renderAll();
}, 3000)
</script>
</body>
</html>

  结果如下:

----------------------------------------

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 2A 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#toObjectand 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 toObjectmethod, 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 toSVGmethod:

       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:

'<?xml version="1.0" standalone="no" ?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800" height="700"
xml:space="preserve"><desc>Created with Fabric.js 0.9.21</desc><rect x="-10" y="-10"
rx="0" ry="0" width="20" height="20" style="stroke: none; stroke-width: 1; stroke-dasharray: ; fill: green;
opacity: 1;" transform="translate(50 50)" /></svg>'

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#loadFromDatalessJSONmethods. 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 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.

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.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.

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 callSupermethod, 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 callSuperonce 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 toObjectmethod 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 callSupercall 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.

fabricjs 高级篇(自定义类型)的更多相关文章

  1. 【.net深呼吸】动态类型(高级篇)

    前面老周给大家介绍了动态类型使用的娱乐级别用法,其实,在很多情景下,娱乐级别的用法已经满足需求了. 如果,你想自己来控制动态类型的行为和数据的存取,那么,就可以考虑用今天所说的高大上技术了.比如,你希 ...

  2. 阶段1 语言基础+高级_1-3-Java语言高级_04-集合_08 Map集合_7_HashMap存储自定义类型键值

    自定义类型做key值.必须要重写hashCode和equals方法 创建pserson类 有name个age两个成员变量.重写toString方法 key有重复,会被新的value值替换掉. key值 ...

  3. C#简单问题,不简单的原理:不能局部定义自定义类型(不含匿名类型)

    今天在进行代码测试时发现,尝试在一个方法中定义一个委托,注意是定义一个委托,而不是声明一个委托变量,在编写的时候没有报错,VS也能智能提示,但在编译时却报语法不完整,缺少方括号,但实际查询并没有缺少, ...

  4. PHP笔记(PHP高级篇)

    高级篇中将涉及数据库的使用以及Cookie和Session会话,提高PHP的开发效率和运行效率 PHP程序员需要掌握的MySQL操作 为项目设计表 使用SQL语句 MySQL的目录结构 data目录中 ...

  5. Kotlin——从无到有系列之高级篇(一):Lambda表达式

    如果您对Kotlin很有兴趣,或者很想学好这门语言,可以关注我的掘金,或者进入我的QQ群大家一起学习.进步. 欢迎各位大佬进群共同研究.探索 QQ群号:497071402 进入正题 经过前面一系列对K ...

  6. Kotlin——高级篇(二):高阶函数详解与标准的高阶函数使用

    在上面一个章节中,详细的讲解了Kotlin中关于Lambda表达式的语法以及运用,如果还您对其还不甚理解,请参见Kotlin--高级篇(一):Lambda表达式详解.在这篇文章中,多次提到了Kotli ...

  7. 跟我学SpringCloud | 第十篇:服务网关Zuul高级篇

    SpringCloud系列教程 | 第十篇:服务网关Zuul高级篇 Springboot: 2.1.6.RELEASE SpringCloud: Greenwich.SR1 如无特殊说明,本系列教程全 ...

  8. 面试题_Spring高级篇

    Spring高级篇 1.什么是 Spring 框架? Spring 框架有哪些主要模块?  Spring 框架是一个为 Java 应用程序的开发提供了综合.广泛的基础性支持的 Java 平台. Spr ...

  9. VFP的数据策略:高级篇

    VFP的数据策略:高级篇 引语 在“VFP中的数据策略:基础篇”一文中,我们研究了VFP应用程序中访问非VFP数据(如SQL Server)的不同机制:远程视图.SQL Passthrough.ADO ...

随机推荐

  1. vue-cli安装sass

    npm install node-sass --save npm install sass-loader --save 也可以使用淘宝镜像 npm install -g cnpm --registry ...

  2. java实现短连接

    现在比较流行短连接,例如新浪微博,腾讯等等公司都开始使用短连接. ① 将长网址用md5算法生成32位签名串,分为4段,,每段8个字符: ② 对这4段循环处理,取每段的8个字符, 将他看成16进制字符串 ...

  3. C++中的 Round(),floor(),ceil()

     2.1             2.6               -2.1               -2.6floor : 不大于自变量的最大整数             2          ...

  4. [bzoj1433][ZJOI2009]假期的宿舍——二分图

    题目大意 传送门 题解 显然是二分图匹配. 用一些方法建图就好了. 要注意的是: 本题有多组数据!!! 初始化一定要注意!!! 代码 #include <bits/stdc++.h> us ...

  5. [LA_3938]最大连续动态和

    Sample Input 3 1 1 2 3 1 1 Sample Output Case 1: 1 1 线段树 L,R表示该区间的左右端点,sum表示该区间值的总和 l,r表示该区间连续的最大和的左 ...

  6. eclipse easy shell plugin

    svn checkout svn://svn.code.sf.net/p/pluginbox/code/trunk pluginbox-code   Eclipse中Easy Shell插件配置Pow ...

  7. Linux的日志错误级别

    讯息等级 系统将讯息分为七个主要的等级,依序是由不重要排列到重要讯息等级: info:仅是一些基本的讯息说明而已: notice:比 info 还需要被注意到的一些信息内容: warning 或 wa ...

  8. Selenium2+python自动化3-解决pip使用异常【转载】

    一.pip出现异常 有一小部分童鞋在打开cmd输入pip后出现下面情况:Did not provide a commandDid not provide a command?这是什么鬼?正常情况应该是 ...

  9. Java程序执行时间

    第一种是以毫秒为单位计算的.  Java代码  //伪代码 long startTime=System.currentTimeMillis();   //获取开始时间 doSomeThing();  ...

  10. C# 获取农历日期

    //C# 获取农历日期 ///<summary> /// 实例化一个 ChineseLunisolarCalendar ///</summary> private static ...