简介:3.组、序列化、子类化
我们已经覆盖了大多数的基本知识在这个系列的 第1部分 和 第2部分 ,接下来让我们玩点高级的!
组
我们首先要谈论的是组(groups)。组是Fabric最强大的功能之一。它们的确像是听起来的样子—一种将任何Fabric对象分组为单个实体的简单方法。
我们为什么要这样做?当然是要将这些对象作为一个单元来使用!
还记得如何用鼠标将画布上任意数量的Fabric对象组合在一起,形成一个选择吗?分组后,所有对象都可以一起移动甚至修改。他们组成一个小组。我们可以缩放该组,旋转甚至更改其外观属性-颜色,透明度,边框等。
这正是组的用途,每当你在画布上看到这样的选择时,Fabric都会在幕后隐式创建一组对象。仅允许以编程方式提供使用组的权限。这就是fabric组的魅力,更多精彩实战,请关注webmote。
让我们创建一个由2个对象组成的组,即圆圈和文本:
首先,我们创建了一个“ hello world”文本对象。设置originX和originY以’center’让它在组内居中; 默认情况下,组成员相对于组的左上角定向。然后,以100px半径的圆圈,填充“ #eef”颜色并垂直压缩(scaleY = 0.5)。然后fabric.Group,我们创建了一个实例,将这两个对象以数组形式传给它,并将其位置设置为150/100和-10角度。最后,将该组添加到画布上,就像其他任何对象一样(用canvas.add()
)。
瞧!你会在画布上看到一个看起来像椭圆的对象。注意,如何修改该对象,我们只是简单地更改了一个组的属性,为其提供了自定义的left,top和angle值。现在,你可以将此对象作为单个实体使用。
现在我们在画布上有了一个组,让我们对其进行一些更改:
发生了什么?我们正在通过item()
方法访问组中的单个对象,并修改它们的属性。第一个对象是压缩的圆圈,第二个对象是文本。让我们看看发生了什么:
你现在可能已经注意到的一件事很重要,那就是组中的所有对象都相对于组的中心定位。当我们更改文本对象的文本时,即使更改了宽度,它也保持居中。如果你不希望出现这种情况,则需要指定对象的left/top坐标。在这种情况下,将根据这些坐标将它们分组在一起。
让我们创建3个圆并对其进行分组,使它们彼此水平放置:
使用组时要记住的另一件事是对象的状态。例如,在与图像组成组时,需要确保这些图像已完全加载。由于Fabric已经提供了用于确保加载图像的辅助方法,因此这变得相当容易:
那么在与groups一起工作时还有哪些其他方法可用?有getObjects()
方法,其工作原理与之完全相同,fabric.Canvas#getObjects()
并返回一组中所有对象的数组。有size()
代表组中的所有对象的数量。还有contains()
,它允许检查特定对象是否在group中。还有item()
,我们前面看到的,允许在一个group中检索特定对象。还有forEachObject()
,再次反映fabric.Canvas#forEachObject
,只是相对于组对象。最后,有一种add()
和remove()
方法可以相应地从组中添加和删除对象。
你可以通过两种方式从group中添加/删除对象-更新或不更新group dimensions/position。我们建议使用更新dimensions,除非你正在执行批处理操作,并且在处理过程中组的宽度/高度不存在问题时没有问题。
-
要将矩形添加到组的中心,请执行以下操作:
-
要在组中心之外添加100px的矩形,请执行以下操作:
-
要将矩形添加到组的中心并更新组的尺寸,请执行以下操作:
-
要在距组中心100px处添加矩形并更新组的尺寸,请执行以下操作:
-
最后,如果你想使用画布上已经存在的对象创建一个组,则需要首先克隆它们:
序列化
一旦开始构建某种有状态的应用程序(可能允许用户将画布内容的结果保存在服务器上,或将内容流式传输到其他客户端),你将需要canvas序列化。如果仍然要发送画布内容?也是可以的,有一个选项可以将画布导出到图像。但是上传图片到服务器无疑是相当占用带宽的,论大小,没有什么可以比得过文本了,这就是为什么Fabric为canvas画布序列化/反序列化提供了极好的支持。
toObject, toJSON 方法
Fabric中的canvas序列化方法主要是toObject()
和toJSON()
方法。我们来看一个简单的例子,首先序列化一个空的画布:
我们正在使用ES5 JSON.stringify()
方法,该方法在传递的对象上隐式调用toJSON
方法(如果存在)。由于Fabric中的canvas实例具有toJSON
方法,就像我们调用JSON.stringify(canvas.toJSON())
一样。
请注意,返回的字符串代表空canvas。它采用JSON格式,并且基本上由“objects”和“background”属性组成。 “objects”当前为空,因为画布上没有任何内容,并且背景具有默认的透明值(“ rgba(0,0,0,0)”)。
让我们为画布提供不同的背景,看看情况如何变化:
正如你所料,画布表示现在反映了新的背景色。现在,让我们添加一些对象!
..日志输出如下:
😲哇哦!😲乍一看变化很大,但仔细看,我们发现新添加的对象,现在是“objects”数组的一部分,序列化为JSON。请注意,它的表示方式包含了它的所有视觉特征-左、上、宽、高、填充、笔划等等。 如果我们要添加另一个对象(例如,矩形旁边的一个红色圆),你将看到表示方式相应地发生了变化:
..日志输出如下:
我突出显示了“ type”:“ rect”
和“ type”:“ circle”
部分,以便你可以更好地看到这些对象的位置。尽管一开始看起来可能有很多输出,但是与图像序列化相比,这没什么。
为了进行比较,让我们看一下用canvas.toDataURL('png')
获得的字符串的大约1/10(!)。
你可能想知道为什么还会有fabric.Canvas#toObject
。
很简单,toObject
仅以实际对象的形式返回与toJSON
相同的表示形式,而没有字符串序列化。例如,以较早的仅带有绿色矩形的画布为例,canvas.toObject()
的输出是这样的:
如你所见,toJSON
输出本质上是一个字符串化的toObject
输出。现在,有趣的是(有用的!)事情是toObject
的输出既聪明又懒。你在“objects”数组中看到的是迭代所有画布对象并将它们委托给它们自己的toObject
方法的结果。 fabric.Path
有自己的toObject
—返回路径的“points”数组,fabric.Image
有自己的toObject
—返回图像的“ src”属性。以一种真正的面向对象的方式,所有对象都可以序列化自己。
这意味着,当你创建自己的“class”或仅需要自定义对象的序列化表示形式时,你要做的就是使用toObject
方法-完全替换它或对其进行扩展。让我们尝试一下:
..日志如下:
如你所见,objects数组现在有了矩形的自定义表示。这种覆盖可能不是很有用(尽管提到了重点),我们还是用额外的属性来扩展矩形的toObject方法吧。
..日志输出如下:
我们使用附加属性“name”扩展了对象现有的toObject
方法,因此该属性现在是toObject
输出的一部分,以canvas JSON表示形式出现。值得一提的是,如果你像这样扩展对象,则还需要确保对象的“class”(在这种情况下为fabric.Rect
)在“ stateProperties”数组中具有此属性,以便从字符串形式中加载画布并将其解析并正确添加到对象。
你可以将对象标记为不可导出,将excludeFromExport
设置为true
。这样,你在画布上可以拥有的一些辅助对象将不会在序列化过程中保存。
toSVG 方法
另一种有效的基于文本的画布表示形式为SVG格式。由于Fabric专注于在画布上进行SVG解析和渲染,因此只有将其设为双向过程并提供画布到SVG的转换才有意义。让我们将相同的矩形添加到画布上,看看toSVG方法返回了哪种表示形式:
..日志输出如下:
就像toJSON
和toObject
一样,toSVG
(在画布上调用时)将其逻辑委托给每个单独的对象,并且每个单独的对象都有其自己的toSVG
方法,该方法专用于对象类型。如果你需要修改或扩展对象的SVG表示形式,则可以使用toSVG
来完成与toObject
相同的操作。
与Fabric专有的toObject
/ toJSON
相比,SVG表示的优点是你可以将其放入任何支持SVG的渲染器(浏览器,应用程序,打印机,照相机等)中,并且应该可以正常工作。但是,使用toObject
/ toJSON
,你首先需要将其加载到画布上。
说到将内容加载到画布上,现在我们可以将画布序列化为高效的文本块了,我们将如何重新加载到画布上呢?
反序列化, SVG 解析器
与序列化类似,有两种从字符串加载画布的方法:从JSON表示或从SVG加载。使用JSON表示形式时,有fabric.Canvas#loadFromJSON
和fabric.Canvas#loadFromDatalessJSON
方法。使用SVG时,有fabric.loadSVGFromURL
和fabric.loadSVGFromString
。
请注意,前两个方法是实例方法,可以直接在canvas实例上调用,而后两个方法是静态方法,可以在“ fabric”对象上而不是在画布上调用。 这些方法没什么可说的。它们的工作与你期望的完全一样。让我们以画布上的先前JSON输出为例,并将其加载到干净的画布上:
..两个物体“神奇地”出现在画布上:
因此,从字符串加载画布非常容易。但是那种看起来很奇怪的loadFromDatalessJSON
方法呢?与我们刚刚使用的loadFromJSON
有什么不同?为了了解为什么需要此方法,我们需要查看具有或多或少复杂路径对象的序列化画布。像这个:
..此形状的JSON.stringify(canvas)输出为:
..那只是整个输出的小部分!
这里发生了什么?好吧,事实证明,这个fabric.Path
实例-这种形状-实际上由数百条贝塞尔曲线组成,这些贝塞尔曲线决定了渲染的精确程度。 JSON表示形式中的所有这些[“ c”,0,2.67,-0.979,5.253,-2.048,9.079]块均对应于此类曲线中的每条曲线。而且当它们有数百个(甚至数千个)时,画布表示形式最终会变得非常庞大。
该怎么办?
这是fabric.Canvas#toDatalessJSON
派上用场的时候。让我们尝试一下:
..日志输出如下:
好吧,那肯定很不错!所以发生了什么事?请注意,在调用toDatalessJSON
之前,我们为path(龙形)对象提供值为“ /assets/dragon.svg”的“ sourcePath”属性。然后,当我们调用toDatalessJSON
时,先前输出中的整个巨大路径字符串(数百条路径命令)被替换为单个“ dragon.svg”字符串。你可以看到上面突出显示的内容。
在处理许多复杂形状时,toDatalessJSON
允许我们进一步减少画布表示,并使用指向SVG的简单链接替换巨大的路径数据表示。
现在回到loadFromDatalessJSON
方法…你可能会猜到,它只是允许从无数据版本的画布表示中加载画布。 loadFromDatalessJSON
非常了解如何获取那些“path”字符串(例如“ /assets/dragon.svg”),加载它们并将它们用作相应路径对象的数据。
现在,让我们看一下SVG加载方法。我们可以使用字符串或URL:
第一个参数是SVG字符串,第二个参数是回调函数。当解析和加载SVG并接收2个参数(objects和options)时,将调用该回调。 objects包含从SVG解析的对象数组-paths,path groups(用于复杂对象),images,text等。为了将所有这些对象分组为一个内聚的集合,并使其与SVG文档中的外观相同,我们使用fabric.util.groupSVGElements
传递对象和选项。作为回报,我们获得fabric.Path
或fabric.Group
的实例,然后可以将其添加到画布上。
fabric.loadSVGFromURL
的工作方式相同,只不过你传递的是包含URL而不是SVG内容的字符串。请注意,Fabric将尝试通过XMLHttpRequest
获取该URL,因此SVG需要符合通常的SOP规则。
子类化
由于Fabric是按照真正的面向对象的方式构建的,因此可以使子类化和扩展变得简单自然。从本系列的第1部分中可以知道,Fabric中存在对象的现有层次结构。所有2D对象(paths,images,text等)都从fabric.Object继承,并且某些“类”(例如fabric.IText
)甚至形成3级继承。 那么,我们将如何在Fabric中对现有的“类”之一进行子类化呢?也许甚至创建我们自己的? 对于此任务,我们需要fabric.util.createClass
实用程序方法。 createClass
只是对Javascript原型继承的简单抽象。
让我们首先创建一个简单的Point “ 类”:
createClass
接受一个对象,并使用该对象的属性创建具有实例级属性的“类”。唯一经过特殊处理的属性是“初始化”,它用作构造函数。因此,现在初始化Point时,我们将创建一个具有“ x”和“ y”属性以及“ toString”方法的实例:
如果我们要创建“ Point”类的子级(比如说一个彩色的点),则可以使用createClass,如下所示:
请注意,现在如何将具有实例级属性的对象作为第二个参数传递。第一个参数接收Point“类”,该点告诉createClass
将该点用作该类的父“类”。为了避免重复,我们使用callSuper
方法,该方法调用父“类”的方法。这意味着,如果我们要更改Point
,则更改也将传播到ColoredPoint
。要查看ColoredPoint
的实际效果:
现在我们开始创建自己的“类”和“子类”,让我们看看如何与已经存在的Fabric一起使用。例如,让我们创建一个LabeledRect
“类”,该类实际上是一个具有某种与之关联的标签的矩形。当在画布上渲染时,该标签将被表示为矩形内的文本。与上一个带有圆圈和文字的小组示例相似。在使用Fabric时,你会注意到可以通过使用组或使用自定义类来实现这样的组合抽象。
似乎这里发生了很多事情,但是实际上很简单。
首先,我们将父“类”指定为fabric.Rect
,以利用其渲染功能。接下来,我们定义“ type”属性,将其设置为“ labeledRect”。这只是为了保持一致性,因为所有Fabric对象都具有type属性(矩形,圆形,路径,文本等)。然后,已经熟悉了构造函数(initialize
),在该构造函数中,我们再次使用callSuper
。此外,我们将对象的标签设置为通过选项传递的任何值。最后,剩下2种方法-toObject
和_render
。如你从序列化一章已经知道的,toObject
负责实例的对象(和JSON)表示。由于LabeledRect
具有与常规rect相同的属性,但也具有标签,因此我们扩展了父对象的toObject
方法,并简单地向其中添加了标签。最后但并非最不重要的一点是,_render
方法是负责实际绘制实例的原因。其中还有另一个callSuper
调用,它是呈现矩形的内容,另外还有3行文本呈现逻辑。
现在,如果我们要渲染这样的对象:
..我们会得到这个:
更改标签值或任何其他常规矩形属性显然可以按预期工作:
当然,在这一点上,你可以随意更改此“类”的行为。例如,将某些值设为默认值,以避免每次将它们传递给构造函数。或使某些可配置属性在实例上可用。如果确实使其他属性可配置,则可能要在toObject
中说明它们并进行initialize
:
为了克隆和保存/还原此类,你需要添加一个fromObject静态方法,并在其之上添加要添加到主fabricObject的子类:
关于这一点,我总结了本系列的第3部分,其中我们探讨了Fabric的一些更高级的方面。在组,类和(反)序列化的帮助下,你可以将你的应用提升到一个全新的水平。