群组是Fabric最强大的功能之一。它们就是它们听起来的样子——一种将任何fabric
对象组合成单个实体的简单方法。我们为什么要这么做?当然,能够将这些对象作为一个对象来工作!
还记得canvas上的任意数量的Fabric对象是如何用鼠标分组的吗?一旦分组,所有对象都可以一起移动甚至修改。他们组成一个团体。我们可以缩放这个组,旋转,甚至改变它的表示属性——颜色、透明度、边框等等。
这正是组的作用,每次您在画布上看到这样的选择时,Fabric都会在幕后隐式地创建一组对象。只有以编程方式提供对使用组的访问才有意义。
让我们创建一组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(group);
首先,我们创建了一个“hello world”
文本对象。将originX
和originY
设置为center'
将使其位于组的中心;默认情况下,组成员是相对于组的左上角定向的。然后,以100px为半径,填充“#eef”
颜色,垂直缩小(scaleY=0.5)
。然后我们创建了一个fabric.Group
,将这两个对象的数组传递给它,并将其位置设置为150/100
和-10deg
。最后,将组添加到canvas
中,就像添加其他对象一样(使用canvas.add()
)。
你在画布上看到一个物体,看起来像一个带标签的椭圆。请注意,为了修改该对象,我们只是更改了组的属性,并赋予它自定义的左值、顶值和角值。现在可以将此对象作为单个实体来处理。
现在我们在画布上创建了一个群组,让我们稍微修改一下:
// 为了使用setFill命名setter,您需要添加可选的命名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()
方法访问组中的各个对象,并修改它们的属性。第一个对象是缩小圆,第二个对象是文本。让我们看看会发生什么:
到目前为止,您可能已经注意到的一件重要事情是,组中的对象都相对于组的中心进行定位。当我们改变文本对象的文本时,即使改变了它的宽度,它仍然保持居中。如果不需要此行为,则需要指定对象的左/顶坐标。在这种情况下,它们将根据这些坐标组合在一起。
让我们创建并分组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);
在处理组时要记住的另一件事是对象的状态。例如,当使用图像组成一个组时,您需要确保这些图像已被完全加载。由于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 }))
});
});
});
那么,在处理组时,还有哪些方法可用呢?有getObjects()
方法,它的工作原理与fabric.Canvas#getObjects()
完全相同,返回一个组中所有对象的数组。size()
表示组中所有对象的数量。contains()
允许检查特定对象是否在组中。我们前面看到的item()允许检索组中的特定对象。还有forEachObject()
,它再次镜像了fabric
.Canvas#forEachObject
,仅与组对象相关。最后是add()
和remove()
方法,用于相应地从组中添加和删除对象。
要在组的中心添加矩形:
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);
这个功能是将canvas
的添加的图像对象进行序列化不同的数据,可以变成JSON
、imageURL(base64)格式
、object对象
toObject
、toJSON
这2个是一样的,返回值都是canvas
画布的数据
例子:
var canvas = new fabric.Canvas('c');
JSON.stringify(canvas); // '{"objects":[],"background":"rgba(0, 0, 0, 0)"}'
发现返回一个对象,object
保存的是图像对象数据,后面参数是canvas
自己的属性,其他没有设置就按照默认设置.
我们添加一些图像在canvas
画布上看看.
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)"}'
需要注意,由于性能考虑,图像不是所有属性都可以进行序列化,
fabric
只是包装了一些常用的属性,所以如果设置了一些比较少用的属性,在加载JSON数据的时候需要重新修改图像的属性.
这是对象可以进行序列化的属性,可以清楚看到我们已经设置了的属性和值,没有设置的属性都是0或者
null
.但是有一些属性比如:hasRotatingPoint
、transparentCorners
等这些属性,需要自己重新设置
当你获取了JSON格式之后,我们只需要调用一个fabric
的loadFormJSON
方法即可.
需要注意还有一个方法,loadFormDatalessJSON
,这个加载的也是canvas
的对象数据,但是不同的是获取的方法不一致.
loadFormDatalessJSON
对应的是toDatalessJSON
而loadFromJSON
对应的是toJSON
或者是toObject
详细的还需要大家看文档,文档说的比较基础,还有一个toSVG
的方法,但是感觉使用的场景比较局限,就不在这里介绍了,有兴趣的可以去文档看看.
由于Fabric是以面向对象的方式构建的,它的设计使子类化和扩展变得简单和自然,正如您在本系列的第1部分中所了解的,Fabric中存在对象层次结构。所有2D对象(路径、图像、文本等)都继承自fabric。对象,以及一些“类”,如fabric.IText
甚至形成3级继承。
这个任务需要fabric.util.createClass
实用方法。createClass
只是对Javascript
原型继承的一个简单抽象。让我们首先创建一个简单的“类”:
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
类的子类——比如一个彩色的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 + ')';
}
});
注意,现在如何将具有实例级属性的对象作为第二个参数传递。第一个参数接收点“class”,它告诉createClass
使用它作为这个类的父“类”。为了避免重复,我们使用callSuper
方法,它调用父类的方法。这意味着如果我们改变点,变化也会传播到色点1。要查看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: 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
。使用它的渲染能力。
接下来,我们定义“type”
属性,将其设置为“labeledRect”
。这只是为了保持一致性,因为所有的Fabric对象都具有类型属性(rect、circle、path、text等)
,
所以我们再次使用callSuper
时已经使用了熟悉的构造函数(initialize)
。
此外,我们将对象的标签设置为通过选项传递的任何值。最后,我们剩下两个方法——toObject
和_render
。
正如您在序列化一章中已经知道的,toObject
负责对象(和JSON)表示实例。由于LabeledRect
具有与常规rect
相同的属性,而且也是一个标签,所以我们正在扩展parent
的toObject
方法,并简单地将标签添加到其中。最后但并非最不重要的是,_rendermethod
负责实际绘制实例。其中还有另一个callSuper
调用,它呈现矩形,以及额外的3行文本呈现逻辑。
var labeledRect = new LabeledRect({
width: 100,
height: 50,
left: 100,
top: 100,
label: 'test',
fill: '#faa'
});
canvas.add(labeledRect);
labeledRect.set({
label: 'trololo',
fill: '#aaf',
rx: 10,
ry: 10
});
当然,在这一点上,您可以随意修改这个“类”的行为。例如,将某些值设置为默认值,以避免每次将它们传递给构造函数。或在实例上提供某些可配置属性。如果你确实使额外的属性可配置,你可能想要解释他们在toObject
和初始化:
...
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);
}
...