fabricjs 高级篇(自定义类型)
原文:https://www.sitepoint.com/fabric-js-advanced/
<html>
<head>
<script src='./js/fabric.min.js'></script>
</head>
<body>
<canvas id="c"></canvas>
<script>
var canvas = new fabric.Canvas('c',{backgroundColor : "#0ff", '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({
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 thefirstandsecondparts 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. (SeeFigure 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 wherefabric.Groupcomes 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 afabric.Groupinstance, 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 usingcanvas.add().
Voila! You see an object on the canvas as shown inFigure 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 inFigure 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(newfabric.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(newfabric.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(newfabric.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 thefabric.Canvas#toObjectandfabric.Canvas#toJSONmethods. Let’s take a look at a simple example, first serializing an empty canvas:
varcanvas=newfabric.Canvas('c');
JSON.stringify(canvas);//'{"objects":[],"background":"rgba(0,0,0,0)"}'
Here I’m using the ES5JSON.stringifymethod, 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 calledJSON.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,
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 withcanvas.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 alsofabric.Canvas#toObject.Quite simply,toObjectreturns 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 forcanvas.toObjectis 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 stringifiedtoObjectoutput. Now, the interesting (and useful) thing is thattoObjectoutput 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 owntoObjectmethod. For example,fabric.Pathhas its owntoObjectthat knows to return path’s “points” array, andfabric.Imagehas atoObjectthat 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 thetoObjectmethod, 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’stoObjectmethod 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 existingtoObjectmethod with the additional property “name”, which means that property is now part of thetoObjectoutput, 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.Rectin 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 thetoSVGmethod:
canvas.add(new fabric.Rect({
left: 50,
top: 50,
height: 20,
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" transform="translate(50 50)" /></svg>'
Just like withtoJSONandtoObject, thetoSVGmethod—when called on canvas—delegates its logic to each individual object, and each individual object has its owntoSVGmethod 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 withtoSVGas I did earlier withtoObject.
The benefit of SVG representation, compared to Fabric’s proprietarytoObject/toJSON, is that you can throw it into any SVG-capable renderer (browser, application, printer, camera, and so on), and it should just work. WithtoObject/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 thefabric.Canvas#loadFromJSONandfabric.Canvas#loadFromDatalessJSONmethods. When using SVG, there arefabric.loadSVGFromURLandfabric.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:
varcanvas=newfabric.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 inFigure 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-lookingloadFromDatalessJSONmethod? How is it different fromloadFromJSON, 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 inFigure 7.
Figure 7. A Complex Shape Rendered on Canvas
JSON.stringify(canvas) output for the shape inFigure 7is 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 thisfabric.Pathinstance—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 wherefabric.Canvas#toDatalessJSONcomes 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 callingtoDatalessJSON, I gave the path (dragon shape) object asourcePath property of “/assets/dragon.svg”. Then, when I calledtoDatalessJSON, 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,toDatalessJSONallows you to reduce canvas representation even further and replace huge path data representations with a simple link to SVG.
You can probably guess that theloadFromDatalessJSONmethod simply allows you to load a canvas from a data less version of a canvas representation. TheloadFromDatalessJSONmethod 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){
varobj=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 usingfabric.util.groupSVGElements and passing it both objects and options. In return, we get either an instance offabric.Pathorfabric.PathGroup, which we can then add onto our canvas.
Thefabric.loadSVGFromURLmethod 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 fromfabric.Object, and some “classes”—likefabric.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 thefabric.util.createClassutility 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;
}
});
ThecreateClassmethod 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 thetoStringmethod:
varpoint=newPoint(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 usecreateClasslike 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 tellscreateClassto use it as a parent class of this one. To avoid duplication, we’re using thecallSupermethod, which calls the method of a parent class. This means that if we were to changePoint, the changes would also propagate to theColoredPointclass.
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 aLabeledRectclass 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 asfabric.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 utilizecallSuperonce again. Additionally, we set the object’s label to whichever value was passed via options. Finally, we’re left with two methods—toObjectand_render. ThetoObject method, as you already know from the serialization section, is responsible for object (and JSON) representation of an instance. SinceLabeledRecthas the same properties as regularrectbut also a label, we’re extending the parent’stoObjectmethod and simply adding a label into it. Last but not least, the_rendermethod is what’s responsible for the actually drawing of an instance. There’s anothercallSupercall 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 8shows the results.
var labeledRect = new LabeledRect({
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 inFigure 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 intoObjectandinitialize, 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({ 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 高级篇(自定义类型)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 椰子肉为什么那么硬?
- 下一篇: 爱因斯坦照片(爱因斯坦发明的)