网站地图    收藏   

主页 > canvas引擎 > Fabricjs >

Fabric.js 简介。第 3 部分。

来源:未知    时间:2022-07-03 21:51 作者:小飞侠 阅读:

[导读] Fabric.js 简介。 第 3 部分。 我们已经在本系列的第一 部分和 第二 部分中 介绍了大部分基础知识。 让我们继续更高级的东西! 团体 我们首先要讨论的是组。 组是 Fabric 最强大的功能之...

Fabric.js 简介。第 3 部分。

我们已经在本系列的第一部分和第二部分中介绍了大部分基础知识。让我们继续更高级的东西!

团体

我们首先要讨论的是组。组是 Fabric 最强大的功能之一。它们正是它们听起来的样子——一种将任何 Fabric 对象组合成单个实体的简单方法。我们为什么要这样做?当然,要能够将这些对象作为一个单元来工作!

还记得如何用鼠标将画布上的任意数量的 Fabric 对象分组,形成一个单一的选择吗?分组后,所有对象都可以移动,甚至可以一起修改。他们组成一个群体。我们可以缩放、旋转,甚至改变它的表现属性——颜色、透明度、边框等。

这正是组的用途,每次您在画布上看到这样的选择时,Fabric 都会在幕后隐式创建一组对象。只有以编程方式提供与组合作的访问权限才有意义。这是fabric.Group为了什么。

让我们创建一组 2 个对象,圆形和文本:

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(组);

首先,我们创建了一个“hello world”文本对象。SettingoriginXoriginYto'center'将使它在组内居中;默认情况下,组成员的方向相对于组的左上角。然后,半径为 100px 的圆,填充“#eef”颜色并垂直挤压(scaleY=0.5)。然后我们创建了一个fabric.Group实例,将这两个对象传递给它的数组,并给它 150/100 的位置和 -10 的角度。最后,该组被添加到画布中,就像任何其他对象一样(带有canvas.add())。

瞧!您会在画布上看到一个对象,它看起来像一个带标签的椭圆。请注意,为了修改该对象,我们只需更改组的属性,为其提供自定义左侧、顶部和角度值。您现在可以将此对象作为单个实体使用。

现在我们在画布上有一个组,让我们稍微改变一下:

// 为了使用 setFill 命名的 setter,您需要添加可选的命名 setter/getter // 来自 src/util/named_accessors.mixins.js 的代码group.item(0).set('fill', 'red') ; group.item(1).set({ 
  text: 'trololo', 
  fill: 'white' });

这里发生了什么?我们通过item()方法访问组中的单个对象,并修改它们的属性。第一个对象是压缩的圆圈,第二个是文本。让我们看看发生了什么:

您现在可能已经注意到的一件重要的事情是,组中的对象都相对于组的中心定位。当我们更改文本对象的文本时,即使更改了它的宽度,它也保持居中。如果您不想要这种行为,您需要指定对象的左/上坐标。在这种情况下,它们将根据这些坐标组合在一起。

让我们创建并分组 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(组);

使用组时要记住的另一件事是对象的状态例如,当用图像组成一个组时,您需要确保这些图像已完全加载。由于 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' , 函数(img){ 
      var img3 = img.scale(0.1).set({ left: 250, top: 250 }); 
      canvas.add(new fabric.Group([ img1, img2, img3], { left: 200 , 顶部: 200 })) 
    }); 
  }); });

那么在与组合作时还有哪些其他方法可用?有一个方法,它的工作原理与返回一个组中所有对象的数组getObjects()完全相同。fabric.Canvas#getObjects()代表size()一个组中所有对象的数量。contains()哪些允许检查特定对象是否在组中。item(),我们之前看到的,允许检索组中的特定对象。forEachObject()再次,镜像fabric.Canvas#forEachObject,仅与组对象有关最后,还有相应的add()方法remove()可以在组中添加和删除对象。

您可以通过 2 种方式从组中添加/删除对象 - 更新组尺寸/位置和不更新。我们建议使用更新尺寸,除非您正在执行批量操作并且在此过程中您对组的宽度/高度错误没有任何问题

在组的中心添加矩形:

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' }));

要在距离组中心 100 像素处添加矩形并更新组的尺寸:

group.addWithUpdate(new fabric.Rect({ 
  ... 
  left: group.get('left') + 100, 
  top: group.get('top') + 100, 
  originX: 'center', 
  originY: 'center' }));

最后,如果你想用画布上已经存在的对象创建一个组,你需要先克隆它们:

// 创建一个包含现有 (2) 个对象副本的组var group = new fabric.Group([ 
  canvas.item(0).clone(), 
  canvas.item(1).clone() ]); // 移除所有对象并重新渲染canvas.clear().renderAll(); // 添加组到画布canvas.add(group);

序列化

一旦您开始构建某种有状态的应用程序,可能允许用户将画布内容的结果保存在服务器上,或者将内容流式传输到不同的客户端,您就需要画布序列化你还怎么发送画布内容?当然,总是可以选择将画布导出到图像,但是将图像上传到服务器肯定会占用大量带宽。在大小方面没有什么比文本更好的了,这正是 Fabric 为画布序列化/反序列化提供出色支持的原因。

toObject, toJSON

Fabric 中画布序列化的支柱是fabric.Canvas#toObject()fabric.Canvas#toJSON()方法。我们来看一个简单的例子,先序列化一个空的画布:

var canvas = new fabric.Canvas('c'); JSON.stringify(画布); // '{"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 = '红色'; JSON.stringify(画布); // '{"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":假,"不透明度":1,"可选择":真,"hasControls":真,"hasBorders":真,"hasRotatingPoint":假,"透明角":真,"perPixelTargetFind":假,"rx":0, "ry":0}],"背景":"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":假,"不透明度":1,"可选择":真,"hasControls":真,"hasBorders":真,"hasRotatingPoint":假,"透明角":真,"perPixelTargetFind":假,"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}],"背景":"rgba(0, 0, 0, 0)"}'hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false,"radius":50}],"background":"rgba(0, 0, 0, 0)"}'hasRotatingPoint":false,"transparentCorners":true,"perPixelTargetFind":false,"radius":50}],"background":"rgba(0, 0, 0, 0)"}'

我突出显示"type":"rect""type":"circle"部分,以便您可以更好地看到这些对象的位置。尽管一开始看起来有很多输出,但与图像序列化所得到的相比,这根本算不上什么只是为了比较,让我们看一下你会得到的字符串的大约 1/10(!)canvas.toDataURL('png')

数据:图像/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAK8CAYAAAAXo9vkAAAgAElEQVR4Xu3dP4xtBbnG4WPAQOQ2YBCLK1qpoQE1/m+NVlCDwUACicRCEuysrOwkwcJgAglEItRQaWz9HxEaolSKtxCJ0FwMRIj32zqFcjm8e868s2fNWo/Jygl+e397rWetk5xf5pyZd13wPwIECBAgQIAAAQIECBxI4F0H+AgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECAsQzQIAAAQIECBAgQIDAwQQEyMGofRABAgQIECBAgAABAgLEM0CAAAECBAgQIECAwMEEBMjBqH0QAQIECBAgQIAAAQICxDNAgAABAgQIECBAgMDBBATIwah9EAECBAgQIECAAAECyw+Qb134R/U2fevC8q+5esGWESBAgAABAgQIEFiOwPL/MC5AlvO0OBMCBAgQIECAAAECJxQQICcE9HYCBAgQIECAAAECBPYXECD7W3klAQIECBAgQIAAAQInFBAgJwT0dgIECBAgQIAAAQIE9hcQIPtbeSUBAgQIECBAgAABAicUECAnBPR2AgQIECBAgAABAgT2FxAg+1t5JQECBAgQIECAAAECJxQQICcE9HYCBAgQIECAAAECBPYXECD7W3klAQIECBAgQIAAAQInFBAgJwTc9+3z49yvmNd+dI7PzPHJOW6Y4wNzXD3HlXNc9pZdb85/vzbHK3P8aY7n5vj1HL+Y43dz417f97O9jgABAgQIECBAgMBSBATIKd2JCY5dWNwyx5fn+PwcV5U/6tXZ99M5fjjHk3Mjd6HifwQIECBAgAABAgQWLSBAirdnouP6WXfvHHfOcU1x9T6rXp4XPTLHA3NTX9jnDV5DgAABAgQIECBA4NACAuSE4hMdl8+Kr83xzTmuO+G61ttfnEXfnuN7c4PfaC21hwABAgQIECBAgMBJBQTIJQpOeFw7b71/jtsvccWh3vbYfNB9c6NfOtQH+hwCBAgQIECAAAECFxMQIMd8No7C4+F5283HfOtZv/ypOYG7hMhZ3wafT4AAAQIECBDYtoAA2fP+H/1Vqwd3f4jf8y1Lfdkunu7xV7OWenucFwECBAgQIEBg3QICZI/7O/Fxx7xs9wf3t36r3D3evciX7L7F7+6rIY8u8uyc

...还有17000 个字符

您可能想知道为什么还有fabric.Canvas#toObject很简单,toObject返回与 相同的表示toJSON,只是以实际对象的形式,没有字符串序列化。例如,以前面的只有一个绿色矩形的画布为例,`canvas.toObject()` 的输出是这样的:

{“背景”:“rgba(0,0,0,0)”,
  “对象”:[ 
    { 
      “角度”:0,
      “填充”:“绿色”,
      “flipX”:假,
      “flipY”:假,
      “hasBorders”:true,
      “hasControls”:true,
      “hasRotatingPoint”:false,
      “height”:20,
      “left”:50,
      “opacity”:1,
      “overlayFill”:null,
      “perPixelTargetFind”:false,
      “scaleX " : 1, 
      "scaleY" : 1, 
      "selectable" : true, 
      "stroke" :空, 
      “strokeDashArray”:空,
      “strokeWidth”:1,
      “top”:50,
      “transparentCorners”:true,
      “类型”:“矩形”,
      “宽度”:20 
    } 
  ] }

如您所见,toJSON输出本质上是一个字符串化的toObject输出。现在,有趣(而且有用!)的事情是toObject输出既聪明又懒惰。您在“对象”数组中看到的是遍历所有画布对象并委托给它们自己的toObject方法的结果。fabric.Path有自己的toObject——知道返回路径的“points”数组,并且fabric.Image有自己的toObject——知道返回图像的“src”属性。在真正面向对象的方式中,所有对象都能够序列化自己。

这意味着当您创建自己的“类”,或者只需要自定义对象的序列化表示时,您需要做的就是使用toObject方法——要么完全替换它,要么扩展它。让我们试试这个:

var rect = new fabric.Rect(); rect.toObject = function() { 
  return { name: 'trololo' }; }; 画布.添加(矩形);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); 画布.添加(矩形);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”数组中具有此属性,以便从字符串表示加载画布将解析和将其正确添加到对象中。

您可以将对象标记为不可导出excludeFromExport设置true这样,您可以在画布上拥有的一些帮助对象在序列化期间不会被保存。

到SVG

另一种有效的基于文本的画布表示是 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>使用 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="翻译(50 50)" /></svg>'

就像toJSONandtoObject一样toSVG——当在画布上调用时——将其逻辑委托给每个单独的对象,并且每个单独的对象都有自己toSVG的特定于对象类型的方法。如果您需要修改或扩展对象的 SVG 表示,您可以使用toSVG我们对toObject.

与 Fabric 的专有toObject/相比,SVG 表示的好处toJSON是您可以将其放入任何支持 SVG 的渲染器(浏览器、应用程序、打印机、相机等)中,并且它应该可以正常工作。但是,使用toObjecttoJSON,您首先需要将其加载到画布上。说到在画布上加载东西,现在我们可以将画布序列化为有效的文本块,我们将如何加载回画布?

反序列化,SVG解析器

与序列化类似,有两种方法可以从字符串加载画布:从 JSON 表示形式或从 SVG 加载。使用 JSON 表示时,有fabric.Canvas#loadFromJSONfabric.Canvas#loadFromDatalessJSON方法。使用 SVG 时,有fabric.loadSVGFromURL一些fabric.loadSVGFromString

请注意,前 2 个方法是实例方法,直接在画布实例上调用,而后两种方法是静态方法,在“结构”对象而不是画布上调用。

这些方法没什么好说的。它们完全按照您的预期工作。例如,让我们从画布中获取先前的 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,"

..两个对象“神奇地”出现在画布上:

所以从字符串加载画布非常容易。但是那种看起来很奇怪的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],...869,5.251,1.869,5.251],["c",-0.801,-0.443,-0.891,-1.067,-0.891,-3.473],...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]块对应于每一条这样的曲线。当有数百个(甚至数千个)它们时,画布表示最终会变得非常巨大。

该怎么办?

这是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" }],"背景":"rgba(0, 0, 0, 0)"}

嗯,那肯定更小!所以发生了什么事?注意在调用之前toDatalessJSON,我们给路径(龙形)对象“/assets/dragon.svg”的“sourcePath”属性。然后,当我们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 被解析和加载并接收 2 个参数时调用回调 -objectsoptionsobjects包含从 SVG 解析的对象数组——路径、路径组(用于复杂对象)、图像、文本等。为了将所有这些对象分组到一个有凝聚力的集合中,并使它们看起来与 SVG 文档中的相同,我们使用fabric.util.groupSVGElements传递它objectsoptions作为回报,我们得到fabric.Pathor的一个实例fabric.Group,然后我们可以将其添加到画布上。

fabric.loadSVGFromURL工作方式相同,除了您传递包含 URL 而不是 SVG 内容的字符串。请注意,Fabric 将尝试通过 XMLHttpRequest 获取该 URL,因此 SVG 需要符合通常的 SOP 规则。

子类化

由于 Fabric 是以真正面向对象的方式构建的,因此它旨在使子类化和扩展变得简单而自然。正如您在本系列的第 1 部分中所知道的,Fabric 中有一个现有的对象层次结构。所有 2D 对象(路径、图像、文本等)都继承自fabric.Object,并且一些“类”——比如fabric.IText——甚至形成 3 级继承。

那么我们将如何对 Fabric 中现有的“类”之一进行子类化呢?或者甚至创造我们自己的?

对于这个任务,我们需要fabric.util.createClass实用方法。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); 点.x; // 10点.y; // 20点.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'); 红点.x; // 15 redPoint.y; // 33 redPoint.color; // "#f55" redPoint.toString(); “15/33(颜色:#f55)”

所以现在我们已经创建了自己的“类”和“子类”,让我们看看如何使用已经存在的 Fabric 类。例如,让我们创建一个LabeledRect“类”,它本质上是一个带有某种标签的矩形。在画布上呈现时,该标签将表示为矩形内的文本。类似于前面带有圆圈和文本的组示例。当您使用 Fabric 时,您会注意到像这样的组合抽象可以通过使用组或使用自定义类来实现。

var LabeledRect = fabric.util.createClass(fabric.Rect, { 
  type: 'labeledRect', 
  // 初始化可以是 function(options) 或 function(property, options) 类型,如文本。
  // 不允许其他签名。
  初始化:function(options) { 
    options || (options = { }); 
    this.callSuper('initialize', options); 
    this.set('label', options.label || ''); 
  }, 
  toObject: 函数() { 
    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我们callSuper再次使用已经熟悉的构造函数 ( )。此外,我们将对象的标签设置为通过选项传递的任何值。最后,我们剩下 2 种方法 -toObject_rendertoObject,正如您在序列化章节中已经知道的那样,它负责实例的对象(和 JSON)表示。由于LabeledRect具有与常规 rect 相同的属性,而且还有一个标签,我们正在扩展 parent 的toObject方法并简单地将标签添加到其中。_render方法负责实际绘制实例。其中还有另一个callSuper调用,即渲染矩形,以及额外的 3 行文本渲染逻辑。

现在,如果我们要渲染这样的对象:

var tagsRect = new LabeledRect({ 
  width: 100, 
  height: 50, 
  left: 100, 
  top: 100, 
  label: 'test', 
  fill: '#faa' }); canvas.add(labeledRect);

..我们会得到这个:

更改标签值或任何其他常见的矩形属性显然会按预期工作:

标签矩形.set({
  标签:'trololo',
  填充:'#aaf',
  rx:10,
  ry:10 });

当然,此时,您可以随意修改这个“类”的行为。例如,将某些值设为默认值,以避免每次都将它们传递给构造函数。或者使实例上的某些可配置属性可用。如果您确实使其他属性可配置,您可能希望在toObjectand中考虑它们initialize

...初始化:函数(选项){
  选项|| (选项 = { }); 
  this.callSuper('initialize', options); 
  // 给所有标记的矩形固定宽度/高度 100/50 this.set({ width: 100, height: 50 }); 
  this.set('label', options.label || ''); } ... _render: function(ctx) { 
  // 使标签的字体和填充值可配置ctx.font = this.labelFont; 
  ctx.fillStyle = this.labelFill; 
  ctx.fillText(this.label, -this.width/2, -this.height/2 + 20); } ...

为了克隆和保存/恢复这个类,你需要添加一个`fromObject`静态方法,最重要的是,要添加到主fabricObject的子类:

// 标准选项 type: fabric.labeledRect.fromObject = function(object, callback) { 
  return fabric.Object._fromObject('LabeledRect', object, callback); } ... // 参数 + 选项类型:// 在本例中,aProp 是对象中包含值的属性// 在 `new fabric.MyClass(someValue, options)` fabric.labeledRect.fromObject中的 someValue = function(object, callback) { 
  return fabric.Object._fromObject('LabeledRect', object, callback, 'aProp'); }

在此说明中,我将结束本系列的第 3 部分,其中我们深入探讨了 Fabric 的一些更高级的方面。在组、类和(反)序列化的帮助下,您可以将您的应用程序提升到一个全新的水平。

阅读第 4 部分


自学PHP网专注网站建设学习,PHP程序学习,平面设计学习,以及操作系统学习

京ICP备14009008号-1@版权所有www.zixuephp.com

网站声明:本站所有视频,教程都由网友上传,站长收集和分享给大家学习使用,如由牵扯版权问题请联系站长邮箱904561283@qq.com

添加评论