我們已經(jīng)介紹了本系列的第一部分和第二部分的大部分基礎(chǔ)知識(shí)。我們繼續(xù)前進(jìn)到更高級(jí)的技巧!
組合(Group)

我們首先談?wù)摰氖墙M合。組合是Fabric最強(qiáng)大的功能之一。 將任何Fabric對(duì)象分組成一個(gè)單一實(shí)體的簡單方法,為什么要這樣做?當(dāng)然是為了能夠?qū)⑦@些對(duì)象作為一個(gè)單元來處理。
可以用鼠標(biāo)來將畫布上的任何數(shù)量的Fabric對(duì)象進(jìn)行分組,形成單一的組合形式,一旦分組,組內(nèi)Fabric對(duì)象可以一起移動(dòng),甚至一起修改。他們組成一個(gè)組。我們可以縮放這個(gè)組合,旋轉(zhuǎn),甚至改變其表現(xiàn)性質(zhì):顏色、透明度、邊界等。
這正是組合所要做的,每當(dāng)您在畫布上看到這樣的選擇時(shí),F(xiàn)abric將在內(nèi)部創(chuàng)建一組對(duì)象。只有以編程方式提供使用組的訪問才有意義。這就是是fabric的組合。
讓我們創(chuàng)建一個(gè)包含兩個(gè)Fabric對(duì)象的組合,圓和文本:
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);
首先,我們創(chuàng)建了一個(gè)“hello world”文本對(duì)象。將originX和originY設(shè)置為“center”會(huì)使得將以組合為中心,默認(rèn)情況下,組合成員相對(duì)于組合的左上角定位。然后,圓圈以100px半徑填充“#eef”顏色并垂直縮放0.5倍(scaleY = 0.5)。然后,我們創(chuàng)建了一個(gè)fabric.Group實(shí)例,通過一個(gè)包含這兩個(gè)Fabric對(duì)象的數(shù)組傳遞給這個(gè)組合,并給它的位置為150/100和-10角度。最后,將這個(gè)組合添加到畫布(使用canvas.add())。
您會(huì)看到畫布上的一個(gè)對(duì)象,看起來像一個(gè)橢圓標(biāo)簽。請(qǐng)注意,如果我們要修改他們的樣子,可以將該他們作為單個(gè)實(shí)體使用,我們只是改變組合的屬性,給出要更改的的left,top和angle值。

讓我們?cè)賮硇薷囊幌耤anvas畫布上的這個(gè)組合:
group.item(0).setFill('red');
group.item(1).set({
text: 'trololo',
fill: 'white'
});
這里發(fā)生了什么?我們通過item()方法訪問組中的各個(gè)對(duì)象,并修改其屬性。第一個(gè)對(duì)象是橢圓,第二個(gè)是文字。讓我們看看發(fā)生了什么:

您現(xiàn)在可能注意到的一件重要的事情是,組合中的對(duì)象都相對(duì)于組合的中心定位,當(dāng)我們改變了text對(duì)象中的內(nèi)容時(shí),即使內(nèi)容的寬度改變了,但是它仍然保持居中的位置,如果你不想要這個(gè)行為,您需要指定對(duì)象的left/right坐標(biāo)。在這種情況下,根據(jù)這些坐標(biāo)將它們分組在一起。
讓我們創(chuàng)建并組合3個(gè)圓,使它們相互水平定位:
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);

使用團(tuán)體時(shí)要注意的另一件事是對(duì)象的狀態(tài),舉個(gè)例子,當(dāng)使用圖片形成組合時(shí),您需要確保這些圖像已經(jīng)完全加載。由于Fabric已經(jīng)提供了幫助方法來確保圖像被加載,這變得相當(dāng)容易:
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 }))
});
});
});

在使用組合時(shí)可以使用哪些其他方法?使用getObjects()方法,可以返回一個(gè)包含組合中所有對(duì)象的數(shù)組,使用size()可以知道組合中所有對(duì)象的數(shù)量。使用contains()可以檢查某個(gè)對(duì)象是否在組合中。我們之前看到的item()可以檢索組中的特定對(duì)象。使用forEachObject可以遍歷每個(gè)組合中的對(duì)象。還有add()和remove()方法來相應(yīng)地從組中添加和刪除對(duì)象。
您可以通過2種方式添加/刪除組中的對(duì)象:更新組合的尺寸/位置,我們建議使用更新尺寸,除非您正在執(zhí)行批量處理操作,并且在進(jìn)程中組合的寬度/高度都沒有問題。
在組合中心添加一個(gè)長方形:
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'
}));
最后,如果您想創(chuàng)建一個(gè)已經(jīng)存在于畫布上的對(duì)象的組合,則需要首先克隆它們:
// 創(chuàng)建一個(gè)包含兩個(gè)已存在對(duì)象的副本的組合
var group = new fabric.Group([
canvas.item(0).clone(),
canvas.item(1).clone()
]);
// 移除所有對(duì)象并且重新渲染
canvas.clear().renderAll();
// 將組合添加到canvas畫布
canvas.add(group);
序列化
一旦你開始構(gòu)建一種有狀態(tài)的應(yīng)用程序,也許允許用戶在服務(wù)器上保存畫布內(nèi)容的結(jié)果,或者將內(nèi)容流傳輸?shù)讲煌目蛻舳?,你都需?strong>canvas序列化。如果仍然要發(fā)送畫布內(nèi)容,也是可以的,有一個(gè)選項(xiàng)可以將畫布導(dǎo)出到圖像。但是上傳圖片到服務(wù)器無疑是相當(dāng)占用帶寬的,論大小,沒有什么可以比得過文本了,這就是為什么Fabric為canvas畫布序列化/反序列化提供了極好的支持。
toObject, toJSON
Fabric中的canvas序列化方法主要是toObject()和toJSON()方法。我們來看一個(gè)簡單的例子,首先序列化一個(gè)空的畫布:
var canvas = new fabric.Canvas('c');
JSON.stringify(canvas);
// '{"objects":[],"background":"rgba(0, 0, 0, 0)"}'
我們使用ES5 JSON.stringify()方法,如果toJSON方法存在,則會(huì)隱性調(diào)用。由于Fabric中的canvas實(shí)例已經(jīng)具有toJSON方法,就相當(dāng)于我們調(diào)用了canvas.toJSON()。
注意返回的表示空的canvas畫布的字符串。它是JSON形式,并且由“objects”和“background”屬性組成?!皁bjects”當(dāng)前為空,因?yàn)閏anvas上沒有任何內(nèi)容,而background的默認(rèn)值為“rgba(0,0,0,0)”)。
讓我們給畫布不同的背景,看看它有什么變化:
canvas.backgroundColor = 'red';
JSON.stringify(canvas);
// '{"objects":[],"background":"red"}'
正如所料,畫布表現(xiàn)現(xiàn)在反映出新的背景顏色?,F(xiàn)在,我們來添加一些Fabric對(duì)象!
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”數(shù)組新增了一個(gè)對(duì)象,序列化為JSON。請(qǐng)注意它的表示方式是如何包含其所有視覺特征:left, top, width, height, fill, stroke等。
如果我們要添加另一個(gè)對(duì)象,比如,一個(gè)位于矩形旁邊的紅色圓圈,您會(huì)看到相應(yīng)改變:
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”部分,即使起初看起來可能會(huì)有很多輸出,但是與圖像序列化所得到的結(jié)果無關(guān)。只是為了比較,讓我們來看看你將用canvas.toDataURL('png')獲得的一個(gè)字符串的大約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
你可能會(huì)想知道為什么還有toObject。很簡單,toObject返回與toJSON相同的表示形式,只能以實(shí)際對(duì)象的形式,而不用字符串序列化。例如,以一個(gè)綠色矩形的canvas的之前的例子。
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輸出本質(zhì)上是一個(gè)字符串的toObject輸出?,F(xiàn)在,有趣的(有用的)事情是,toObject輸出是聰明和懶惰的。您在“objects”數(shù)組中看到的內(nèi)容是遍歷canvas畫布上所有的對(duì)象并調(diào)用該對(duì)象本身自己的toObject方法的結(jié)果。fabric.Path有自己的toObject,返回路徑的“points”數(shù)組,
fabric.Image也有自己的toObject,返回圖像的“src”屬性。以真正的面向?qū)ο蟮姆绞剑袑?duì)象都能夠自己序列化。
這意味著當(dāng)您創(chuàng)建自己的“類”,或者需要定制對(duì)象的序列化表示時(shí),您需要做的就是使用toObject方法 - 完全替換它或擴(kuò)展它。我們來試試一下:
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數(shù)組現(xiàn)在有一個(gè)自定義的展示。這種覆蓋可能不是很有用 - 盡管把重點(diǎn)提到了:我們?nèi)绾瓮卣购透木匦蔚?code>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”來擴(kuò)展對(duì)象的現(xiàn)有toObject方法,所以“name”屬性是toObject輸出的一部分,值得一提的還有,如果你擴(kuò)展這樣的對(duì)象,您還需要確保對(duì)象所屬的“class”(在這種情況下為fabric.Rect)在“stateProperties”數(shù)組中具有此屬性,以便從字符串表示中加載canvas將解析并將其正確添加到對(duì)象中。
您可以將對(duì)象標(biāo)記為不可導(dǎo)出設(shè)置excludeFromExport為true。以這種方式,一些在canvas上用來輔助的對(duì)象在序列化過程中不會(huì)被保存。
toSVG
另一種高效的基于文本的畫布表示是SVG格式。由于Fabric專門從事畫布上的SVG解析和渲染,所以將其作為雙向過程并提供畫布到SVG轉(zhuǎn)換是有意義的。讓我們將相同的矩形添加到畫布中,并查看從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在canvas上調(diào)用時(shí),將其邏輯委托給每個(gè)單獨(dú)的對(duì)象,并且每個(gè)單獨(dú)的對(duì)象都有自己的toSVG方法,它是特殊的對(duì)象類型。如果您需要修改或擴(kuò)展對(duì)象的SVG表示,那么可以使用toSVG做同樣的事情,就像我們對(duì)toObject一樣。
與Fabric的專有的toObject / toJSON相比,SVG表示的好處是可以將其投放到任何支持SVG的渲染器(瀏覽器,應(yīng)用程序,打印機(jī),相機(jī)等)中,并且它應(yīng)該可以正常工作。但是,您首先需要將其加載到canvas畫布上。談到在畫布上加載東西,現(xiàn)在我們可以將畫布序列化成一個(gè)有效的文本塊,我們將如何加載到畫布上?
反序列化,SVG解析器
與序列化類似,有兩種方式從字符串加載canvas:從JSON表示,或從SVG。當(dāng)使用JSON表示時(shí),使用loadFromJSON和oadFromDatalessJSON方法。SVG時(shí),使用fabric.loadSVGFromURL和fabric.loadSVGFromString。
請(qǐng)注意,前2個(gè)方法是實(shí)例方法,直接在canvas實(shí)例上調(diào)用,而最后2個(gè)方法是靜態(tài)方法,在“fabric”對(duì)象上而不是在canvas上調(diào)用。
這些方法沒有什么可說的。他們的工作正如你所期望的那樣。例如,我們先從畫布中獲取以前的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)"}');
兩個(gè)物體“神奇”出現(xiàn)在畫布上:

所以從字符串加載畫布是很容易的。但是那個(gè)奇怪的loadFromDatalessJSON方法是干什么的?與我們剛剛使用的loadFromJSON有什么不同?為了理解為什么我們需要這種方法,我們需要看一下具有或多或少的復(fù)雜路徑對(duì)象的序列化畫布。像這個(gè):

而這個(gè)形狀的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],...
...這只是整個(gè)輸出的其中一部分!
這里發(fā)生了什么?事實(shí)證明,這個(gè)fabric.Path實(shí)例(這個(gè)形狀)包括數(shù)百條貝塞爾線,決定了它是如何被渲染的。JSON表示中的所有這些[“c”,0,2.67,-0.979,5.253,-2.048,9.079]數(shù)據(jù)片段對(duì)應(yīng)于這些曲線中的每一個(gè)。而當(dāng)數(shù)百(甚至數(shù)千)的這些數(shù)據(jù)片段構(gòu)成畫布的表現(xiàn)最終是相當(dāng)巨大的。
該怎么辦?
這時(shí)使用toDatalessJSON會(huì)方便很多。我們來試試吧:
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)"}
這樣看起來小多了,發(fā)生了什么?注意在調(diào)用toDatalessJSON之前做了什么,我們給了“sourcePath”屬性一個(gè)路徑:“/assets/dragon.svg”,然后,當(dāng)我們調(diào)用toDatalessJSON時(shí),從前一個(gè)輸出(這幾百個(gè)路徑命令)的整個(gè)堆積如山的路徑字符串被一個(gè)單獨(dú)的“dragon.svg”字符串所取代。你可以看到它顯示在上面。
當(dāng)使用大量復(fù)雜的形狀時(shí),toDessessJSON可以讓我們進(jìn)一步減少畫布表現(xiàn),并用簡單的SVG鏈接代替巨大的路徑數(shù)據(jù)表示。
現(xiàn)在回到loadFromDatalessJSON方法...你可以猜到,它只是允許從無數(shù)據(jù)版本的canvas表示中加載canvas。```loadFromDatalessJSON``幾乎非常了解如何使用這些“路徑”字符串(如“/assets/dragon.svg”),加載它們,并用作相應(yīng)路徑對(duì)象的數(shù)據(jù)。
現(xiàn)在,我們來看看SVG加載方法。我們可以使用字符串或URL:
fabric.loadSVGFromString('...', function(objects, options) {
var obj = fabric.util.groupSVGElements(objects, options);
canvas.add(obj).renderAll();
});
第一個(gè)參數(shù)是SVG字符串,第二個(gè)是回調(diào)函數(shù),當(dāng)SVG被解析和加載時(shí)調(diào)用回調(diào),并且接收到2個(gè)參數(shù) - objects和option,objects包含從SVG路徑解析的對(duì)象數(shù)組,路徑組(復(fù)雜對(duì)象),圖像,文本等。為了將所有這些對(duì)象分組成一個(gè)連貫的集合,并使它們與SVG文檔中的方式相同,我們使用fabric.util.groupSVGElements傳遞objects和option。在返回值中,我們可以獲得fabric.Path或fabric.Group的一個(gè)實(shí)例,然后我們可以將其添加到畫布上。
fabric.loadSVGFromURL的工作方式相同,除了將SVG內(nèi)容的字符串替換為URL,還要注意,F(xiàn)abric將嘗試通過XMLHttpRequest獲取該URL,因此SVG需要符合通常的SOP(標(biāo)準(zhǔn)操作程序)規(guī)則。
子類
由于Fabric以真正的面向?qū)ο蟮姆绞綐?gòu)建,它旨在使子類化和擴(kuò)展簡單自然。從這個(gè)系列的第1部分你知道,F(xiàn)abric中存在對(duì)象的現(xiàn)有層次結(jié)構(gòu)。所有2D對(duì)象(路徑,圖像,文本等),還有一些“classes”,都繼承自fabric.Object,像fabric.IText,甚至形成3級(jí)繼承。
那么我們?cè)趺慈グ袴abric中的一個(gè)現(xiàn)有的“class”分類呢?或者甚至創(chuàng)造自己的?
對(duì)于這個(gè)任務(wù),我們需要使用fabric.util.createClass實(shí)用程序。createClass只不過是對(duì)JavaScript的原型繼承的簡單抽象。我們先創(chuàng)建一個(gè)簡單的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接受一個(gè)對(duì)象并使用該對(duì)象的屬性創(chuàng)建具有實(shí)例級(jí)屬性的“class”。唯一需要特別處理的屬性是“initialize”用作構(gòu)造函數(shù)(相當(dāng)于constructor)。所以現(xiàn)在,當(dāng)初始化Point時(shí),我們將創(chuàng)建一個(gè)帶有“x”和“y”屬性的實(shí)例,和“toString”方法:
var point = new Point(10, 20);
point.x; // 10
point.y; // 20
point.toString(); // "10/20"
如果我們想創(chuàng)建一個(gè)“Point”類的子類“ColoredPoint”,我們將使用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 + ')';
}
});
注意如何將具有實(shí)例級(jí)屬性的對(duì)象作為第二個(gè)參數(shù)傳遞,而第一個(gè)參數(shù)接收Point“class”,這告訴createClass將其用作這個(gè)父類的“class”。為了避免重復(fù),我們使用的是callSuper方法,它調(diào)用父類“class”的方法。這意味著如果我們要改變Point,這些更改也會(huì)傳播到ColoredPoint???code>ColoredPoint的行為:
var redPoint = new ColoredPoint(15, 33, '#f55');
redPoint.x; // 15
redPoint.y; // 33
redPoint.color; // "#f55"
redPoint.toString(); "15/35 (color: #f55)"
所以現(xiàn)在我們?nèi)?chuàng)建我們自己的“類”和“子類”,我們來看看如何使用已經(jīng)存在的Fabric。例如,我們創(chuàng)建一個(gè)LabeledRect“類”,它基本上是一個(gè)與它相關(guān)聯(lián)的一些標(biāo)簽的矩形。當(dāng)在畫布上渲染時(shí),該標(biāo)簽將被表示為矩形內(nèi)的文本。類似于以前的組合示例與圓和文本。當(dāng)您使用Fabric時(shí),您會(huì)注意到,可以通過使用組合或使用自定義類來實(shí)現(xiàn)這樣的組合抽象。
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);
}
});
在這里似乎還有很多事情,但實(shí)際上很簡單。
首先,我們將父類“class”指定為fabric.Rect,以利用其渲染能力。接下來,我們定義“type”屬性,將其設(shè)置為“l(fā)abeledRect”。這只是為了一致,因?yàn)樗械腇abric對(duì)象都有類型屬性(rect,circle,path,text等)。然后,我們已經(jīng)熟悉的構(gòu)造函數(shù)(initialize),我們?cè)俅问褂?code>callSuper。此外,我們將對(duì)象的標(biāo)簽設(shè)置為通過選項(xiàng)傳遞的值。最后,我們剩下兩個(gè)方法:toObject和_render。 toObject,正如您從序列化章節(jié)已經(jīng)知道的那樣,負(fù)責(zé)實(shí)例的對(duì)象(和JSON)表示。由于LabeledRect具有與常規(guī)Rect相同的屬性,也是一個(gè)標(biāo)簽,因此我們將擴(kuò)展父類的toObject方法,并在其中添加標(biāo)簽。最后但并非最不重要的是,_rendermethod是實(shí)際繪制實(shí)例的原因。還有一個(gè)callSuper調(diào)用,它是渲染矩形,有3行文本渲染邏輯。
現(xiàn)在,如果我們要渲染這樣的對(duì)象:
var labeledRect = new LabeledRect({
width: 100,
height: 50,
left: 100,
top: 100,
label: 'test',
fill: '#faa'
});
canvas.add(labeledRect);
我們會(huì)得到這個(gè):
更改標(biāo)簽值或任何其他的矩形屬性將按預(yù)期工作:
labeledRect.set({
label: 'trololo',
fill: '#aaf',
rx: 10,
ry: 10
});
當(dāng)然,在這一點(diǎn)上,你可以隨意修改這個(gè)“類”的行為。例如,將某些值設(shè)置為默認(rèn)值,以避免每次傳遞給構(gòu)造函數(shù)?;蛘呤箤?shí)例上的某些可配置屬性可用。如果您使其他屬性可配置,則可能需要在toObject和initialize中對(duì)其進(jìn)行計(jì)算。
...
initialize: function(options) {
options || (options = { });
this.callSuper('initialize', options);
// 給所有標(biāo)注的矩形固定寬/高100/50
this.set({ width: 100, height: 50 });
this.set('label', options.label || '');
}
...
_render: function(ctx) {
// 使標(biāo)簽的字體和填充值可配置
ctx.font = this.labelFont;
ctx.fillStyle = this.labelFill;
ctx.fillText(this.label, -this.width/2, -this.height/2 + 20);
}
...
在這個(gè)說明中,我正在包裝這個(gè)系列的第三部分,其中我們分為了Fabric的一些更先進(jìn)的方面。在群組,課程和(反)序列化的幫助下,您可以將您的應(yīng)用程序提升到一個(gè)全新的水平。
下一章:Fabric.js介紹 第四部分