在本系列的第一部分和第二部分中,我们已经介绍了Fabric的大多数基础知识。 在本文中,我将继续介绍更高级的功能:组,序列化(和反序列化)和类。
团体
我要谈论的第一个主题是组,这是Fabric最强大的功能之一。 分组的确切含义是一种将Fabric对象分组为单个实体的简单方法,以便您可以将这些对象作为一个单元来使用。 (参见图1。 )
图1.一个选择成为Fabric中的一个组
请记住,可以用鼠标将画布上任意数量的Fabric对象组合在一起以形成一个选择。 分组后,可以将对象移动甚至修改为一个对象。 您可以缩放组,旋转组,甚至更改其外观属性-颜色,透明度,边框等。
每次在画布上选择这样的对象时,Fabric都会在幕后隐式创建一个组。 鉴于此,只有以编程方式提供对组的访问才有意义,这是fabric.Group
所在。
让我们从两个对象(一个圆和一个文本)创建一个组:
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);
首先,我创建了一个“ hello world”文本对象。 然后,我创建了一个半径为100 px的圆,并填充了“ #eef”颜色并垂直挤压(scaleY = 0.5)。 接下来,我创建了一个fabric.Group
实例, fabric.Group
传递包含这两个对象的数组,并以-10度角将其赋予150/100的位置。 最后,通过使用canvas.add()
,将组与其他对象一样添加到画布中。
瞧! 您将在画布上看到一个如图2所示的对象,该对象被标记为椭圆,现在可以将该对象作为单个实体使用。 要修改该对象,只需更改组的属性,即可为其自定义左,上和角度值。
图2 。 以编程方式创建的组
现在我们在画布上有了一个组,让我们对其进行一些更改:
group.item(0).set({ text: 'trololo', fill: 'white' }); group.item(1).setFill('red');
在这里,我们通过item方法访问组中的单个对象并修改其属性。 第一个对象是文本,第二个对象是压缩的圆圈。 图3显示了结果。
图3.带有新文本的压缩红色圆圈
您现在可能已经注意到的一个重要想法是,组中的所有对象都相对于组的中心定位。 当我更改文本对象的text属性时,即使更改了宽度,它也保持居中。 如果您不希望出现这种情况,则需要指定对象的左/上坐标,在这种情况下,将根据这些坐标对它们进行分组。
这是创建和分组三个圆圈的方法,以使它们一个接一个地水平放置, 如图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);
图4.具有三个水平对齐的圆的组
使用组时要记住的另一点是对象的状态。 例如,当与图像组成一个组时,您需要确保这些图像已完全加载。 由于Fabric已经提供了用于确保加载图像的辅助方法,因此此代码和图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 })) }); }); });
图5.具有三个图像的组
可以使用其他几种方法来处理组:
- getObjects的工作原理与fabric.Canvas#getObjects()完全相同,并返回一组中所有对象的数组
- size表示组中的对象数
- 包含可让您检查特定对象是否在组中
- 项目(如前所述)允许您从组中检索特定对象
- forEachObject也镜像fabric.Canvas#forEachObject,但与组对象有关
- 添加和删除分别从组中添加和删除对象
您可以添加或删除对象,无论是否更新组的尺寸和位置。 以下是几个示例:
要在组的中心添加一个矩形(left = 0,top = 0),请使用以下代码:
group.add(new fabric.Rect({ ... }));
要从组的中心添加100 px的矩形,请执行以下操作:
group.add(new fabric.Rect({ ... left: 100, top: 100 }));
要在组的中心添加一个矩形并更新组的尺寸,请使用以下代码:
group.addWithUpdate(new fabric.Rect({ ... left: group.getLeft(), top: group.getTop() }));
要在距组中心100 px处添加一个矩形并更新组的尺寸,请执行以下操作:
group.addWithUpdate(new fabric.Rect({ ... left: group.getLeft() + 100, top: group.getTop() + 100 }));
最后,如果要使用画布上已经存在的对象创建一个组,则需要首先克隆它们:
// 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);
序列化
一旦开始构建某种有状态的应用程序(可能允许用户将画布内容的结果保存在服务器上或将内容流式传输到其他客户端),就需要画布序列化。 始终可以选择将画布导出到图像,但是将大图像上传到服务器需要大量带宽。 就文本大小而言,文本是无与伦比的,这就是为什么Fabric为画布序列化和反序列化提供出色支持的原因。
toObject,toJSON
Fabric中画布序列化的主干是fabric.Canvas#toObject
和fabric.Canvas#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格式,主要由“对象”和“背景”属性组成。 “对象”属性当前为空,因为画布上没有任何内容,“背景”具有默认的透明值(“ 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)"}'
哇! 乍一看,已经发生了很多变化,但是更仔细地观察,您可以看到新添加的对象现在已成为“对象”数组的一部分,并序列化为JSON。 请注意,其表示形式如何包括其所有视觉特征-左,上,宽度,高度,填充,描边等。
如果我们要添加另一个对象(例如,位于矩形旁边的红色圆圈),您将看到表示也发生了相应的变化:
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')
获得的字符串的十分之一(! 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
…还有大约17,000个字符。
您可能想知道为什么还会有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
输出既聪明又懒惰。 您在“对象”数组中看到的是迭代所有画布对象并委托给每个对象自己的toObject
方法的结果。 例如, fabric.Path
都有自己toObject
,知道返回路径的“点”阵列,并fabric.Image
有toObject
,知道返回图像的“源”属性。 以真正的面向对象的方式,所有对象都可以序列化自己。
这意味着,当您创建自己的类或仅需要自定义对象的序列化表示形式时,您要做的就是使用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)"}'
我使用附加属性“名称”扩展了对象现有的toObject
方法,这意味着该属性现在是toObject
输出的一部分,因此它出现在画布JSON表示中。 值得一提的另一项是,如果您扩展这样的对象,则还需要确保对象的“类”(在这种情况下为fabric.Rect
)在“ stateProperties”数组中具有此属性,以便从中加载画布字符串表示形式将解析并将其正确添加到对象中。
到SVG
另一种有效的基于文本的画布表示形式为SVG格式。 由于Fabric专注于在画布上进行SVG解析和渲染,因此有一个双向过程并提供画布到SVG的转换是有意义的。 让我们将相同的矩形添加到画布中,看看toSVG
方法返回了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
方法,该方法专用于对象的类型。 如果您需要修改或扩展对象的SVG表示形式,则可以使用toSVG
与之前使用toObject
进行的操作相同的toObject
。
与Fabric专有的toObject
/ toJSON
相比,SVG表示的好处是您可以将其扔到任何支持SVG的渲染器(浏览器,应用程序,打印机,照相机等)中,并且应该可以正常工作。 但是,使用toObject
/ toJSON
时,首先需要将其加载到画布上。
说到将内容加载到画布上,现在您知道如何将画布序列化为有效的文本块,如何将这些数据加载回画布上?
反序列化和SVG解析器
与序列化一样,有两种从字符串加载画布的方法:从JSON表示或从SVG。 使用JSON表示形式时,有fabric.Canvas#loadFromJSON
和fabric.Canvas#loadFromDatalessJSON
方法。 使用SVG时,有fabric.loadSVGFromURL
和fabric.loadSVGFromString
。
注意,前两个方法是实例方法,可以直接在画布实例上调用,而其他两个方法是静态方法,可以在“ fabric”对象上而不是在画布上调用。
这些方法中的大多数没有太多要说的。 它们的工作完全符合您的期望。 让我们以画布上的先前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)"}');
两个对象都神奇地出现在画布上, 如图6所示。
图6.在画布上渲染的圆和正方形
因此,从字符串加载画布非常容易,但是那种看起来很奇怪的loadFromDatalessJSON
方法呢? 与我们刚刚使用的loadFromJSON
有何不同? 要了解为什么需要这种方法,请看一看具有或多或少复杂路径对象的序列化画布, 如图7所示。
图7.在画布上渲染的复杂形状
图7中形状的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],...
……而这只是全部产量的20%!
这里发生了什么? 好吧,事实证明,这个fabric.Path
实例(这种形状)实际上由数百条Bezier线组成,这些线决定了渲染的精确度。 JSON表示中的所有这些[“ c”,0,2.67,-0.979,5.253,-2.048,9.079]块均对应于这些曲线中的每条曲线。 而且当它们有数百个(甚至数千个)时,画布表示形式最终会变得非常庞大。
在这种情况下, fabric.Canvas#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)"}
那肯定更小,那发生了什么? 注意,在调用toDatalessJSO
N之前,我给路径(龙形状)对象提供了“ /assets/dragon.svg”的sourcePat
h属性。 然后,当我调用toDatalessJSON
,前一个输出中的整个巨大路径字符串(数百个路径命令)被替换为单个“ dragon.svg”字符串。
当您处理许多复杂的形状时, toDatalessJSON
允许您进一步减少画布表示形式,并使用指向SVG的简单链接替换庞大的路径数据表示形式。
您可能会猜到loadFromDatalessJSON
方法只是允许您从画布表示形式的无数据版本加载画布。 loadFromDatalessJSON
方法几乎知道如何获取那些“路径”字符串(例如“ /assets/dragon.svg”),加载它们并将它们用作相应路径对象的数据。
现在,让我们看一下SVG加载方法。 我们可以使用字符串或URL。 让我们首先看一下字符串示例:
fabric.loadSVGFromString('...', function(objects, options) { var obj = fabric.util.groupSVGElements(objects, options); canvas.add(obj).renderAll(); });
第一个参数是SVG字符串,第二个参数是回调函数。 解析和加载SVG并接收两个参数(对象和选项)时,将调用该回调。 第一个是对象,包含从SVG解析的对象数组-路径,路径组(用于复杂对象),图像,文本等。 要将这些对象分组为一个有凝聚力的集合-并使它们看起来像它们在SVG文档中的fabric.util.groupSVGElement
-我们正在使用fabric.util.groupSVGElement
并将其同时传递对象和选项。 作为回报,我们获得fabric.Path
或fabric.PathGroup
的实例,然后可以将其添加到画布上。
fabric.loadSVGFromURL
方法的工作方式相同,除了传递的字符串包含URL而不是SVG内容。 请注意,Fabric将尝试通过XMLHttpRequest获取该URL,因此SVG需要符合通常的SOP规则。
子类化
由于Fabric是按照真正的面向对象的方式构建的,因此可以使子类化和扩展变得简单自然。 如本系列第一篇文章所述,Fabric中存在对象的现有层次结构。 所有二维对象(路径,图像,文本等)都从fabric.Object
继承,并且某些“类”(例如fabric.PathGroup
)甚至形成了第三级继承。
那么,如何在Fabric中子类化现有的“类”之一,或者甚至创建自己的类?
对于此任务,您需要fabric.util.createClass
实用程序方法。 该方法不过是对JavaScript原型继承的简单抽象。 让我们首先创建一个简单的Point“类”:
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
方法接受一个对象,并使用该对象的属性创建具有实例级属性的类。 唯一经过特殊处理的属性是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“类”,它告诉createClass
将其用作该类的父类。 为了避免重复,我们使用了callSuper
方法,该方法调用父类的方法。 这意味着,如果我们要更改Point
,则更改也将传播到ColoredPoint
类。
这是ColoredPoint的作用:
var redPoint = new ColoredPoint(15, 33, '#f55'); redPoint.x; // 15 redPoint.y; // 33 redPoint.color; // "#f55" redPoint.toString(); "15/35 (color: #f55)"
现在,让我们看看如何使用现有的Fabric类。 例如,让我们创建一个LabeledRect
类,该类本质上将是一个具有某种与之关联的标签的矩形。 当在画布上渲染时,该标签将被表示为矩形内的文本(类似于前面的带有圆圈和文本的组示例)。 在使用Fabric时,您会注意到可以通过使用组或使用自定义类来实现这样的组合抽象。
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); } });
看起来这里发生了很多事情,但是实际上很简单。 首先,我们将父类指定为fabric.Rect
,以利用其渲染功能。 接下来,我们定义类型属性,将其设置为“ labeledRect
”。 这只是为了保持一致性,因为所有Fabric对象都具有type属性(rect,circle,path,text等)。然后有一个已经很熟悉的构造函数(initialize),在该构造函数中,我们再次使用callSuper
。 此外,我们将对象的标签设置为通过选项传递的值。 最后,剩下两个方法toObject
和_render
。 正如您从序列化部分已经知道的, toObjec
t方法负责实例的对象(和JSON)表示。 由于LabeledRect
具有与常规rect
相同的属性,还具有标签,因此我们扩展了父级的toObject
方法,并简单地在其中添加了标签。 最后但并非最不重要的_render
是, _render
方法负责实际绘制实例。 其中还有另一个callSuper
调用,它是呈现矩形的内容,另外还有三行文本呈现逻辑。
如果要渲染此类对象,请执行以下操作。 图8显示了结果。
var labeledRect = new LabeledRect({ width: 100, height: 50, left: 100, top: 100, label: 'test', fill: '#faa' }); canvas.add(labeledRect);
图8.labeledRect的呈现
如您在此处和图9所示 ,更改标签值或任何其他通常的矩形属性显然可以按预期工作。
labeledRect.set({ label: 'trololo', fill: '#aaf', rx: 10, ry: 10 }
图9.修改后的labeledRect
当然,在这一点上,您可以随意更改此类的行为。 例如,您可以将某些值设为默认值,以避免每次将其传递给构造函数,也可以使某些可配置属性在实例上可用。 如果确实要配置其他属性,则可能要在toObject
进行解释,并进行initialize
,如我在此处所示:
... 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); } ...
包起来
结束了本系列的第三部分,在该系列中,我已经介绍了Fabric的一些更高级的方面。 在组,序列化和反序列化以及类的帮助下,您可以将您的应用提升到一个全新的水平。