我们已经介绍了本系列的第一部分和第二部分的大部分基础知识。我们继续前进到更高级的技巧!
文章目录
- 组合(Groups)
- 序列化(Serialization)
- toObject, toJSON
- toSVG
- 反序列化,SVG解析器(Deserialization, SVG parser)
- 子类(Subclassing)
组合(Groups)
我们首先谈论的是组合。组合是Fabric最强大的功能之一。 将任何Fabric对象组合成一个单一实体的简单方法,为什么要这样做?当然是为了能够将这些对象作为一个单元来处理。
还记得画布上任意数量的Fabric对象如何用鼠标分组,形成一个选择吗?分组后,可以一起移动甚至修改所有对象。他们组成一个小组。我们可以缩放该组,旋转,甚至更改其表现属性-颜色、透明度、边框等。
这正是组合所要做的,每当您在画布上看到这样的选择时,Fabric将在内部创建一组对象。只有以编程方式提供使用组的访问才有意义。这就是fabric.Group。
让我们创建一个包含两个Fabric对象的组合,圆和文本:
var circle = new fabric.Circle({
radius: 100,
fill: '#eef',
scaleY: 0.5,
originX: 'center',
originY: 'center'
});
var text = new fabric.Text('hello world', {
fontSize: 30,
originX: 'center',
originY: 'center'
});
var group = new fabric.Group([ circle, text ], {
left: 150,
top: 100,
angle: -10
});
canvas.add(group);
首先,我们创建了一个“hello world”文本对象。将originX
和originY
设置为'center'
将使其在组内居中;默认情况下,组成员相对于组的左上角定位。然后,用100px半径圆,填充“#eef”颜色并垂直挤压(scaleY=0.5)。然后我们创建了一个fabric.Group
实例,包含这两个Fabric对象的数组传递给这个组合,并将其位置设置为150/100,角度为-10。最后,该组被添加到canvas中,就像任何其他对象一样(使用canvas.add()
)。
瞧!你在画布上看到一个物体,看起来像一个标记的椭圆。请注意,为了修改该对象,我们只需更改组的属性,为其提供自定义的left、top和angle值。现在可以将此对象作为单个实体使用。
现在我们在画布上有了一个Group,让我们稍微改变一下:
// in order to use setFill named setter, you need to add the optional named setter/getter
// code from src/util/named_accessors.mixins.js
group.item(0).set('fill', 'red');
group.item(1).set({
text: 'trololo',
fill: 'white'
});
这里发生了什么?我们通过item()方法访问组中的各个对象,并修改其属性。第一个对象是椭圆,第二个是文字。让我们看看发生了什么:
您现在可能注意到的一件重要事情是,组中的对象都是相对于组的中心定位的。当我们更改文本对象的文本时,即使在更改其宽度后,它仍保持居中。如果不希望出现这种行为,则需要指定对象的left/top坐标。在这种情况下,它们将根据这些坐标分组在一起。
让我们创建并组合3个圆,使它们一个接一个地水平放置:
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);
处理组时要记住的另一件事是对象的状态。例如,当使用图像组成Group时,需要确保这些图像已完全加载。由于Fabric已经提供了用于确保加载图像的助手方法,这变得相当简单:
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 }))
});
});
});
那么,在使用Group时可以使用哪些其他方法?有getObjects()
方法,其工作原理与fabric.Canvas#getObjects()
完全相同,返回一个包含组合中所有对象的数组。有size()
表示组中所有对象的数量。contains()
可以检查特定对象是否在组中。我们前面看到了item()
,它允许检索组中的特定对象。还有forEachObject()
,工作原理与fabric.Canvas#forEachObject
相同,可以遍历组合中的每个对象。最后,有add()
和remove()
方法来相应地添加和删除组中的对象。
您可以通过两种方式从组中添加/删除对象:
- 添加/删除的同时更新Group的尺寸/位置
- 添加/删除的同时不更新。
我们建议添加/删除的同时更新尺寸,除非您正在执行批处理操作,并且您在处理过程中没有遇到组宽度/高度错误的问题。
在组合中心添加一个长方形:
group.add(new fabric.Rect({
...
originX: 'center',
originY: 'center'
}));
在组合的中心附近100px添加矩形:
group.add(new fabric.Rect({
...
left: 100,
top: 100,
originX: 'center',
originY: 'center'
}));
在组合中心添加矩形并且更新组合的尺寸:
group.addWithUpdate(new fabric.Rect({
...
left: group.get('left'),
top: group.get('top'),
originX: 'center',
originY: 'center'
}));
在组合的中心附近100px添加矩形并且更新组合的尺寸:
group.addWithUpdate(new fabric.Rect({
...
left: group.get('left') + 100,
top: group.get('top') + 100,
originX: 'center',
originY: 'center'
}));
最后,如果您想创建一个已经存在于画布上的对象的组合,则需要首先克隆它们:
// 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)
一旦您开始构建某种类型的有状态应用程序,也许允许用户在服务器上保存画布内容的结果,或者将内容流式传输到不同的客户端,您就需要canvas序列化。您还可以如何发送画布内容?当然,总有办法可以将画布导出到图像,但将图像上载到服务器肯定会占用大量带宽。在大小方面,没有什么比文本更好,这正是Fabric为画布序列化/反序列化提供出色支持的原因。
toObject, toJSON
Fabric中的canvas序列化方法主要是toObject()和toJSON()方法。我们来看一个简单的例子,首先序列化一个空的画布:
var canvas = new fabric.Canvas('c');
JSON.stringify(canvas); // '{"objects":[],"background":"rgba(0, 0, 0, 0)"}'
我们使用ES5 JSON.stringify()
方法,如果参数的toJSON
方法存在,则会隐性调用。由于Fabric中的canvas实例已经具有toJSON
方法,就相当于我们调用了JSON.stringify(canvas.toJSON())
。
请注意返回的表示空画布的字符串。它是JSON格式的,基本上由“objects”和“background”属性组成。“objects”当前为空,因为画布上没有任何内容,背景具有默认的透明值(“rgba(0, 0, 0, 0)”)。
让我们给画布不同的背景,看看它有什么变化:
canvas.backgroundColor = 'red';
JSON.stringify(canvas); // '{"objects":[],"background":"red"}'
正如所料,画布表现现在反映出新的背景颜色。现在,我们来添加一些对象!
canvas.add(new fabric.Rect({
left: 50,
top: 50,
height: 20,
width: 20,
fill: 'green'
}));
console.log(JSON.stringify(canvas));
输出是:
'{
"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)"
}'
看起来改变了好多,在“objects”数组新增了一个对象,序列化为JSON。请注意它的表示方式是如何包含其所有视觉特征:left, top, width, height, fill, stroke等。
如果我们要添加另一个对象,比如,一个位于矩形旁边的红色圆圈,您会看到相应改变:
canvas.add(new fabric.Circle({
left: 100,
top: 100,
radius: 50,
fill: 'red'
}));
console.log(JSON.stringify(canvas));
输出是:
'{
"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)"
}'
注意看“type”:“rect”和“type”:“circle”部分,这样您可以更好地看到这些对象的位置。尽管乍看起来可能有很多输出,但与图像序列化相比,这根本算不上什么。为了比较,让我们看看使用canvas.toDataURL('png')
得到的字符串的大约1/10(!)的内容。
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAK8CAYAAAAXo9vkAAAgAElEQVR4Xu3dP4xtBbnG4WPAQOQ2YBCLK1qpoQE1/m+NVlCDwUACicRCEuysrOwkwcJgAglEItRQaWz9HxEaolSKtxCJ0FwMRIj32zqFcjm8e868s2fNWo/Jygl+e397rWetk5xf5pyZd13wPwIECBAgQIAAAQIECBxI4F0H+hwfQ4AAAQIECBAgQIAAgQsCxENAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECyw+Qb134R/U2fevC8q+5esGWESBAgAABAgQIEFiOwPL/MC5AlvO0OBMCBAgQIECAAAECJxQQICcE9HYCBAgQIECAAAECBPYXECD7W3klAQIECBAgQIAAAQInFBAgJwT0dgIECBAgQIAAAQIE9hcQIPtbeSUBAgQIECBAgAABAicUECAnBPR2AgQIECBAgAABAgT2FxAg+1t5JQECBAgQIECAAAECJxQQICcE9HYCBAgQIECAAAECBPYXECD7W3klAQIECBAgQIAAAQInFBAgJwTc9+3z49yvmNd+dI7PzPHJOW6Y4wNzXD3HlXNc9pZdb85/vzbHK3P8aY7n5vj1HL+Y43dz417f97O9jgABAgQIECBAgMBSBATIKd2JCY5dWNwyx5fn+PwcV5U/6tXZ99M5fjjHk3Mjd6HifwQIECBAgAABAgQWLSBAirdnouP6WXfvHHfOcU1x9T6rXp4XPTLHA3NTX9jnDV5DgAABAgQIECBA4NACAuSE4hMdl8+Kr83xzTmuO+G61ttfnEXfnuN7c4PfaC21hwABAgQIECBAgMBJBQTIJQpOeFw7b71/jtsvccWh3vbYfNB9c6NfOtQH+hwCBAgQIECAAAECFxMQIMd8No7C4+F5283HfOtZv/ypOYG7hMhZ3wafT4AAAQIECBDYtoAA2fP+H/1Vqwd3f4jf8y1Lfdkunu7xV7OWenucFwECBAgQIEBg3QICZI/7O/Fxx7xs9wf3t36r3D3evciX7L7F7+6rIY8u8uycFAECBAgQIE
…还有大约17000个字符。
你可能想知道为什么还有fabric.Canvas#toObject
。很简单,toObject
返回与toJSON
相同的表示,只是以实际对象的形式返回,没有字符串序列化。例如,以前面的画布为例,画布上只有一个绿色矩形canvas.toObject()
的输出如下:
{
"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
}
]
}
如您所见,toJSON
输出本质上是一个字符串化的toObject
输出。现在,有趣(而且有用!)问题是toObject
输出既聪明又懒惰。您在“objects”数组中看到的是迭代所有画布对象并委托给它们自己的toObject
方法的结果。fabric.Path
有自己的toObject
——它知道返回路径的“点”数组,fabric.Image
也有自己的toObject
-它知道返回图像的“src”属性。在真正的面向对象方式中,所有对象都能够序列化自己。
这意味着,当您创建自己的“类”,或者只需要自定义对象的序列化表示时,您所需要做的就是使用toObject方法——要么完全替换它,要么扩展它。让我们试试这个:
var rect = new fabric.Rect();
rect.toObject = function() {
return { name: 'trololo' };
};
canvas.add(rect);
console.log(JSON.stringify(canvas));
输出:
'{"objects":[{"name":"trololo"}],"background":"rgba(0, 0, 0, 0)"}'
您可以看到,objects数组现在有一个自定义的展示。这种覆盖可能不是很有用-尽管这一点很重要-所以不如我们用额外的属性扩展矩形的toObject方法。
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));
输出:
'{
"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)"
}'
我们用附加的属性“name”扩展了对象的现有toObject
方法,因此该属性现在是toObject
输出的一部分,结果显示在画布JSON表示中。还有一点值得一提的是,如果您像这样扩展对象,您还需要确保对象的“class”(在本例中为fabric.Rect
)在“stateProperties”数组中具有此属性,以便从字符串形式加载到画布时能正确解析并将其添加到对象中。
可以将对象标记为不可导出,将excludeFromExport设置为true。这样,您可以在画布上拥有的一些助手对象将不会在序列化期间保存。
toSVG
另一种有效的基于文本的画布表示是SVG格式。由于Fabric专门从事SVG解析和在画布上渲染,因此只有将其作为双向过程并提供画布到SVG的转换才有意义。让我们将相同的矩形添加到画布中,并查看从toSVG方法返回的表示形式:
canvas.add(new fabric.Rect({
left: 50,
top: 50,
height: 20,
width: 20,
fill: 'green'
}));
console.log(canvas.toSVG());
输出是:
'<?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>'
就像toJSON
和toObject
一样,当在画布上调用toSVG
时,toSVG
将其逻辑委托给每个单独的对象,并且每个单独对象都有自己的toSVG
方法,该方法对于对象类型是特殊的。如果您需要修改或扩展对象的SVG表示,您可以像前面扩展toObject
的示例一样,对toSVG
进行相同的操作。
与Fabric专有的toObject
/toJSON
相比,SVG表示的好处是,您可以将它放入任何支持SVG的渲染器(浏览器、应用程序、打印机、相机等)中,它应该可以正常工作。然而,对于toObject
/toJSON
,您首先需要将其加载到画布上。说到在画布上加载内容,现在我们可以将画布序列化为一个有效的文本块,我们将如何将其加载回画布上?
反序列化,SVG解析器(Deserialization, SVG parser)
与序列化一样,从字符串加载画布也有两种方式:从JSON表示或从SVG表示。当使用JSON表示时,有fabric.Canvas#loadFromJSON
和fabric.Canvas#loadFromDatalessJSON
方法。当使用SVG时,有fabric.loadSVGFromURL
和fabric.loadSVGFromString
。
请注意,前2个方法是实例方法,直接在canvas实例上调用,而后2个方法则是静态方法,在“fabric”对象上而不是在canva上调用。
关于这些方法没有什么好说的。它们的工作方式与您期望的完全一样。例如,让我们从画布中获取先前的JSON输出,并将其加载到干净的画布上:
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)"}');
可以看到两个对象出现在画布上:
所以从字符串加载到画布非常容易。但是看起来奇怪的loadFromDatalessJSON
方法呢?它与我们刚才使用的loadFromJSON
有什么区别?为了理解为什么我们需要这个方法,我们需要查看具有或多或少复杂的路径对象的序列化画布。像这个:
而这个形状的JSON.stringify(canvas)输出是:
{"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],...
…这只是整个输出的其中一部分!
这里发生了什么?事实证明,这个fabric.Path
实例(这个形状)包括数百条贝塞尔线,决定了它是如何被渲染的。JSON表示中的所有这些[“c”,0,2.67,-0.979,5.253,-2.048,9.079]
数据片段对应于这些曲线中的每一个。而当数百(甚至数千)的这些数据片段构成画布的表现最终是相当巨大的。
该怎么办?
这时使用toDatalessJSON会方便很多。我们来试试吧:
canvas.item(0).sourcePath = '/assets/dragon.svg';
console.log(JSON.stringify(canvas.toDatalessJSON()));
输出:
{"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)"}
这样看起来小多了,发生了什么?注意在调用toDatalessJSON
之前做了什么,我们给了“sourcePath”属性一个路径:“/assets/dragon.svg”,然后,当我们调用toDatalessJSON
时,从前一个输出(这几百个路径命令)的整个堆积如山的路径字符串被一个单独的“dragon.svg”字符串所取代。你可以看到它显示在上面。
当使用大量复杂的形状时,toDatalessJSON
可以让我们进一步减少画布表现,并用简单的SVG链接代替巨大的路径数据表示。
现在回到loadFromDatalessJSON
方法……您可能已经猜到,它只允许从dataless版本的canvas数据加载到画布。loadFromDatalessJSON
非常了解如何获取这些"path"字符串(如“/assets/dragon.svg”),加载它们,并将其用作相应路径对象的数据。
现在,我们来看看SVG的加载方法。我们可以使用字符串或URL:
fabric.loadSVGFromString('...', function(objects, options) {
var obj = fabric.util.groupSVGElements(objects, options);
canvas.add(obj).renderAll();
});
第一个参数是SVG字符串。第二个参数是回调函数,当SVG被解析和加载时调用,并且接收到2个参数 - objects
和options
。objects
包含从SVG解析的对象数组——路径、路径组(对于复杂对象)、图像、文本等等。为了将所有这些对象分组成一个连贯的集合,并使它们看起来与SVG文档中的相同,我们使用了fabric.util.groupSVGElements
,传递objects
和options
。在返回值中,我们可以获得fabric.Path
或fabric.Group
的一个实例,然后我们可以将其添加到画布上。
fabric.loadSVGFromURL
的工作方式相同,除了将SVG内容的字符串替换为URL,还要注意,Fabric将尝试通过XMLHttpRequest获取该URL,因此SVG需要符合通常的SOP(标准操作程序)规则。
子类(Subclassing)
由于Fabric是以真正面向对象的方式构建的,所以它的设计目的是使子类化和扩展变得简单自然。正如您在本系列的第一部分中所知道的,Fabric中有一个现有的对象层次结构。所有2D对象(路径、图像、文本等)都继承fabric.Object
,以及一些“classes”——fabric.IText
,甚至形成三级继承。
那么,我们将如何对Fabric中现有的一个“类”进行子类化呢?或者甚至创造我们自己的?
对于这个任务,我们需要fabric.util.createClass
实用程序方法。createClass
只是对Javascript原型继承的简单抽象。让我们首先创建一个简单的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;
}
});
createClass
接受一个对象,并使用该对象的属性创建具有实例级属性的 “class”。唯一经过特殊处理的属性是"initialize",它用作构造函数。因此,现在在初始化Point
时,我们将创建一个具有"x"和"y"属性以及"toString"方法的实例:
var point = new Point(10, 20);
point.x; // 10
point.y; // 20
point.toString(); // "10/20"
如果我们想创建一个“Point”类的子类,比如一个彩色点,我们可以这样使用createClass
:
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 + ')';
}
});
请注意,具有实例级属性的对象现在如何作为第二个参数传递。第一个参数接收Point
“class”,它告诉createClass
将其用作此参数的父“class”。为了避免重复,我们使用callSuper
方法,它调用父“class”的方法。这意味着,如果我们要更改Point
,更改也会传递到ColoredPoint 。要查看ColoredPoint的操作:
var redPoint = new ColoredPoint(15, 33, '#f55');
redPoint.x; // 15
redPoint.y; // 33
redPoint.color; // "#f55"
redPoint.toString(); "15/33 (color: #f55)"
现在,我们已经完成了创建自己的“类”和“子类”,让我们看看如何使用现有的Fabric类。例如,让我们创建一个LabeledRect“类”,它本质上是一个矩形,它有一些与之相关的标签。当在画布上渲染时,该标签将表示为矩形内的文本。类似于前一个带有圆圈和文本的分组示例。在使用Fabric时,您会注意到这样的组合抽象可以通过使用组或使用自定义类来实现。
var LabeledRect = fabric.util.createClass(fabric.Rect, {
type: 'labeledRect',
// initialize can be of type function(options) or function(property, options), like for text.
// no other signatures allowed.
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);
}
});
看起来这里发生了很多事情,但其实很简单。
首先,我们将父“类”指定为fabric.Rect
,以利用其渲染能力。接下来,我们定义“type”属性,将其设置为“labeledRect”。这只是为了保持一致性,因为所有Fabric对象都有类型属性(rect、circle、path、text等)。然后,我们又一次使用了熟悉的构造函数(initialize
)。此外,我们将对象的label属性设置为可以通过options传递的值。最后,我们剩下两个方法——toObject
和_render
。正如您在序列化一章中已经知道的,toObject
负责实例的对象(和JSON)表示。由于LabeledRect
具有与常规矩形相同的属性,但也有一个label,因此我们扩展了父对象的toObject
方法,只需在其中添加label即可。最后,_render
方法是负责实际绘制实例的方法。其中还有另一个callSuper
调用,它是渲染矩形的,另外还有3行是文本的渲染逻辑。
现在,如果我们要渲染这样的对象:
var labeledRect = new LabeledRect({
width: 100,
height: 50,
left: 100,
top: 100,
label: 'test',
fill: '#faa'
});
canvas.add(labeledRect);
我们会得到:
更改label值或任何其他常用矩形属性显然可以按预期工作:
labeledRect.set({
label: 'trololo',
fill: '#aaf',
rx: 10,
ry: 10
});
当然,此时,您可以随意修改这个“类”的行为。例如,将某些值设为默认值,以避免每次都将它们传递给构造函数。或者使实例上的某些可配置属性可用。如果您确实使其他属性可配置,您可能需要在toObject
和initialize
中考虑这些属性:
...
initialize: function(options) {
options || (options = { });
this.callSuper('initialize', options);
// give all labeled rectangles fixed width/heigh 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);
}
...
为了克隆和保存/恢复这个类,您需要添加一个“fromObject”静态方法,在此基础上,将子类添加到主fabricObject:
// standard options type:
fabric.labeledRect.fromObject = function(object, callback) {
return fabric.Object._fromObject('LabeledRect', object, callback);
}
...
// argument + options type:
// 在本例中,aProp是对象中包含值的属性
// that goes in someValue in `new fabric.MyClass(someValue, options)`
fabric.labeledRect.fromObject = function(object, callback) {
return fabric.Object._fromObject('LabeledRect', object, callback, 'aProp');
}
在这一点上,我将结束本系列的第三期,在这一期中,我们深入探讨了Fabric的一些更高级的方面。在组、类和(反)序列化的帮助下,您可以将应用程序提升到一个全新的水平。