原文链接
使用
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.22/fabric.min.js"></script>
Fabric.js可以让你轻而易举的使用HTML5中的canvas功能。它为画布提供了原生画布所缺少的对象模型、SVG解析器、交互层以及一整套其他必不可少的工具。它是一个完全开源的项目,在MIT获得许可,这些年一直有贡献者在贡献代码。
三年前,在我发现原生canvas API的使用痛苦之后,我开发了Fabric.js。我创建了一个交互式设计编辑器printio.ru,它允许用户设计自己的服装。我们想要的那种交互性当时只能在Flash应用程序中运行。即使是现在,很少有人开发出接近Fabric所实现的目标。
让我们看看它到底有多神奇!
1、为什么要使用Fabric
如今,Canvas让我们可以在网站中创建一些十分令人惊叹的图形。但是canvas提供的API十分的有限。如果仅仅只花一些简单的图形,那就当我没说。但是一旦需要各种交互,比如在任意一点改变图片,或者绘制更复杂的图形,情况就截然不同了。
Fabric
就是解决这样的难题的。
原生canvas只允许我们在画布上画一些简单的图形,盲目地修改整个画布位图。
使用fillRect(left, top, width, height)
绘制矩形;
使用moveTo(left, top)
以及lineTo(x, y)
来绘制一条线;
就像我们用画笔画画布一样,不受控制地在上面绘制。
为了替代这种繁琐的操作,Fabric
在原有的方法上提供了简单但强大的对象模型。它考虑了canvas的状态以及渲染,让我们可以直接操作对象。
让我们用一个简单的例子来证明它的不同之处。
我们想要在画布上画一个红色的矩形。使用canvas原生的API,我们会这样做:
// 获取canvas元素
var canvasEl = document.getElementById('c');
// 获取2d环境(早些提到的‘bitmap’(位图))
var ctx = canvasEl.getContext('2d');
// 填充2d环境的颜色
ctx.fillStyle = 'red';
// 在点(100,100)处绘制一个大小是20x20的矩形
ctx.fillRect(100, 100, 20, 20);
现在我们使用Fabric
做同样的事情
// 获取canvas元素,这一步文档少了,不能少
var canvasEl = document.getElementById('c');
// 创建canvas对象
var canvas = new fabric.Canvas('canvasEl ');
// 创建矩形对象
var rect = new fabric.Rect({
left: 100, //这里不要加引号,是数字类型,不是字符串
top: 100, //这里不要加引号,是数字类型,不是字符串
fill: 'red',
width: 20, //这里不要加引号,是数字类型,不是字符串
height: 20 //这里不要加引号,是数字类型,不是字符串
});
// 将矩形添加到canvas上
canvas.add(rect);
此刻,两种方法在大小上几乎没有任何区别,看起来一模一样。但是你可以看到两种方法的不同使用方式。原生的方法是在2d环境上操作,一个对象代表整个画布位图。而在Fabric上,我们操作对象,进行实例化,改变它们的属性,然后添加到canvas上。你可以看到这些对象在Fabric中占据很重要的地位。
但是仅仅渲染一个红色的矩形很无聊,让我们玩点有趣的!比如,稍微旋转?
让我们试试旋转45度。
- 原生方法
var canvasEl = document.getElementById('c');
var ctx = canvasEl.getContext('2d');
ctx.fillStyle = 'red';
ctx.translate(100, 100);
ctx.rotate(Math.PI / 180 * 45);
ctx.fillRect(-10, -10, 20, 20);
- Fabric方法
var canvasEl = document.getElementById('c');
var canvas = new fabric.Canvas('canvasEl ');
// create a rectangle with angle=45
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'red',
width: 20,
height: 20,
angle: 45 //只需要添加一个angle属性
});
canvas.add(rect);
发生了什么?
我们在Fabric中只需要改变对象的angle
属性为45。原生API相对来说就繁琐一些。记住,原生方法中我们不能操作对象,而是调整整个画布位图的定位和角度(ctx.translate,ctx.rotate)以满足我们的需求。然后我们再次绘制矩形,但要正确地偏移位图(-10,-10),以便它仍然在100,100点渲染,除此之外,我们还必须在旋转画布位图时将度数转换为弧度。
我相信你已经明白了Fabrci存在的意义,以及原生API背后的不足。
让我们看另外一个栗子-追踪canvas的状态
如果在某些时候,我们想将现在熟悉的红色矩形移动到画布上稍微不同的位置怎么办?如果不能对对象进行操作,我们如何做到这一点?在画布位图上调用另一个fillRect方法吗?
当然不是。调用另一个fillRect命令实际上在已经在画布上绘制了内容的画布上绘制矩形。还记得我之前提到用画笔绘画吗?为了“移动”它,我们需要先擦除以前绘制的内容,然后在新位置绘制矩形。
原生方法
var canvasEl = document.getElementById('c');
...
ctx.strokRect(100, 100, 20, 20);
...
// erase entire canvas area
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
ctx.fillRect(20, 50, 20, 20);
Fabric
var canvasEl = document.getElementById('c');
var canvas = new fabric.Canvas('canvasEl ');
...
canvas.add(rect);
...
rect.set({ left: 20, top: 50 });
canvas.renderAll();
注意一个非常重要的区别。使用Fabric,我们不再需要在尝试“修改”任何内容之前删除内容。我们仍然使用对象,只需更改其属性,然后重新渲染画布以获得“新图片”。
2、对象
我们已经知道如何使用fabric.Rect
构造函数实例化矩形了。fabric当然也包括其他基础的图形-圆形、三角形、椭圆形等。所有这些都作为fabric.Circle
,fabric.Triangle
,fabric.Ellipse
等在fabric命名空间下暴露出来。
Fabric提供了其中基础形状:
- fabric.Circle
- fabric.Ellipse
- fabric.Line
- fabric.Polygon
- fabric.Polyline
- fabric.Rect
-
fabric.Triangle
想画一个圆形?仅仅只需要创建一个圆形对象,然后添加到canvas中。其他基础形状也是一样。
var circle = new fabric.Circle({
radius: 20, fill: 'green', left: 100, top: 100
});
var triangle = new fabric.Triangle({
width: 20, height: 30, fill: 'blue', left: 50, top: 50
});
canvas.add(circle, triangle);
..我们在100,100位置绘制了一个绿色圆圈,在50,50位置绘制了一个蓝色三角形。
操作对象
创建图形对象 - 矩形,圆形或其他对象- 当然只是一个开始。在某些时候,我们可能想要修改这些对象。也许某些动作需要触发状态的改变,或者添加某种类型的动画。或者我们可能想要在某些鼠标交互中更改对象属性(颜色,不透明度,大小,位置)。Fabric考虑了画布渲染和状态管理,我们只需要修改对象本身。前面的示例演示了set({left:20,top:50})
如何从前一个位置“移动”对象.以类似的方式,我们可以更改对象的任何其他属性。但那些属性是什么?
正如你所料,有:
- positioning — left, top;
- dimension — width, height;
- rendering — fill, opacity, stroke, strokeWidth;
- scaling and rotation — scaleX, scaleY, angle;
- flipping — flipX, flipY
- skewing — skewX, skewY
是的,在Fabric中创建翻转对象就只需要将flip *属性设置为true。您可以通过get方法读取这些属性中的任何一个,并通过set设置它们。让我们尝试更改一些红色矩形的属性:
var canvasEl = document.getElementById('c');
var canvas = new fabric.Canvas('canvasEl ');
...
canvas.add(rect);
rect.set('fill', 'red');
rect.set({ strokeWidth: 5, stroke: 'rgba(100,200,200,0.5)' });
rect.set('angle', 15).set('flipY', true);
首先,我们将“fill”值设置为“红色”,基本上将对象设置为红色。下一个语句设置“strokeWidth”和“stroke”值,矩形为5px的淡绿色边框。最后,我们改变“angle”和“flipY”属性。请注意3个语句中的每个语句使用的语法略有不同。这表明set是一种通用方法。您可能会经常使用它,因此它也尽可能使我们的操作方便。
我们已经介绍了setters,那getters呢?很明显,有一般的get方法,但也有一些特定的get *方法。要读取对象的“width”值,可以使用get('width')或getWidth()。要获得“scaleX”值 - get('scaleX')或getScaleX(),依此类推。对于每个“公共”对象属性(“stroke”,“strokeWidth”,“angle”等),都有像getWidth或getScaleX这样的方法。
你可能注意到,在前面使用的set方法中,我们使用了相同的配置hash值来创建对象。因为它就是几乎相同的。您可以在创建时就“配置”对象,也可以在以下代码后使用set方法:
// 在创建对象的时候就传入配置参数
var rect = new fabric.Rect({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });
// 或者在调用构造函数之后使用set方法
var rect = new fabric.Rect();
rect.set({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });
默认配置
你可能会问,如果在创建对象的时候,我们没有传入任何配置参数会发生什么?它仍然会有那些属性吗?
当然有!Fabric的对象都会有一些列的默认属性。如果在创建对象时没有传入配置参数,默认情况下会将默认配置属性传给对象,我们自己可以试一试:
var rect = new fabric.Rect(); // notice no options passed in
rect.get('width'); // 0
rect.get('height'); // 0
rect.get('left'); // 0
rect.get('top'); // 0
rect.get('fill'); // rgb(0,0,0)
rect.get('stroke'); // null
rect.get('opacity'); // 1
我们的矩形有一组默认属性。它位于(0,0),黑色,完全不透明,没有描边(stroke属性),没有尺寸(宽度和高度为0)。由于没有尺寸,我们无法在画布上看到它。但是给它任何宽度/高度的正值肯定会在画布的左/上角显示一个黑色矩形。
层次结构和继承
Fabric对象不仅彼此独立存在。它们形成了非常精确的层次结构。
大部分对象都继承了fabric.Object
,fabric.Object
几乎代表一个二维形状,位于二维画布平面中。它是一个具有左/顶部和宽度/高度属性的实体,以及一系列其他图形的特征。 我们在对象上看到的那些属性 - fill, stroke, angle, opacity, flip*等 - 都是从fabric.Object继承的,并且对所有Fabric对象都是通用的。这种继承允许我们在fabric.Object
上自定义方法,并能在所有的子类中公用。例如,如果你想所有的对象都有getAngleInRadians
方法,那么你只需要在fabric.Object.prototype
上创建即可。
fabric.Object.prototype.getAngleInRadians = function() {
return this.get('angle') / 180 * Math.PI;
};
var rect = new fabric.Rect({ angle: 45 });
rect.getAngleInRadians(); // 0.785...
var circle = new fabric.Circle({ angle: 30, radius: 10 });
circle.getAngleInRadians(); // 0.523...
circle instanceof fabric.Circle; // true
circle instanceof fabric.Object; // true
正如你所看到的,该方法立即能在所有实例上进行调用。
所有子类都从fabric.Object
哪里继承了公用的方法和属性,然而它们也可以定义自己的方法和属性。比如使用fabric.Circle
画一个圆,它需要半径radius
属性.再比如fabric.Image
- 我们稍后会看到 - 需要有getElement / setElement
方法来访问/设置图像实例所源自的HTML <img>元素。
对于高级项目,使用原型来获取自定义渲染和行为是很常见的。
3、Canvas
我们介绍了Fabric对象的很多细节,让我们回到canvas。
你可以看到,我们使用new fabric.Canvas('...')
来创建某种Fabric对象。fabric.Canvas
作为<canvas>元素的包裹器,并负责管理这个canvas画布上所有的fabric对象。它需要传入canvas元素的id,然后返回fabric.Canvas实例。我们可以向画布上添加fabric对象,使用或者删除这些对象。
var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect();
canvas.add(rect); // 添加对象
canvas.item(0); // 使用早期添加的(第一个对象)fabric.Rect
canvas.getObjects(); // 获取所有canvas上的对象 (矩形将会是第一个也是唯一一个)
canvas.remove(rect); // 删除前面添加的fabric.Rect
fabric.Canvas
主要用来生成canvas实例,但它也是可配置的。需要为整个画布设置背景颜色或图像吗?将所有内容剪辑到某个区域?设置不同的宽度/高度?指定画布是否是交互式的?所有这些选项(和其他选项)都可以在fabric.Canvas上设置,无论是在创建时还是在之后:
var canvas = new fabric.Canvas('c', {
backgroundColor: 'rgb(100,100,200)',
selectionColor: 'blue',
selectionLineWidth: 2
// ...
});
// or
var canvas = new fabric.Canvas('c');
canvas.setBackgroundImage('http://...');
canvas.onFpsUpdate = function(){ /* ... */ };
// ...
交互
谈到canvas元素,就会涉及交互性。 Fabric的一个内置独特功能就是我们所创建的对象模型上的交互层。对象模型允许我们以编程的方式访问和操纵画布上的对象。但在用户级别,用户通过鼠标(或触摸,触摸设备)来操纵这些对象。一旦你通过new fabric.Canvas('...')
初始化canvas对象,用户很可能选中对象,拖动它们,缩放或旋转它们,甚至组合在一起操作!如果我们希望允许用户在画布上拖动某些东西 - 比如图像 - 我们需要做的就是初始化画布并在其上添加一个对象。无需其他配置或设置。为了控制这种交互性,我们可以在canvas上使用Fabric的“selection
”属性,以及单个对象的“selectable
”属性。
var canvas = new fabric.Canvas('c');
...
canvas.selection = false; // disable group selection
rect.set('selectable', false); // make object unselectable
但是如果你根本不想要这样的交互层呢?如果是这种情况,您可以随时用fabric.StaticCanvas
替换fabric.Canvas
。初始化的语法完全相同;你只使用StaticCanvas而不是Canvas。
var staticCanvas = new fabric.StaticCanvas('c');
staticCanvas.add(
new fabric.Rect({
width: 10,
height: 20,
left: 100,
top: 100,
fill: 'yellow',
angle: 30
}));
这创建了一个“更轻”的画布版本,没有任何事件处理逻辑。但你仍然有一个完整的对象模型来操作对象-添加对象,删除或修改它们,以及更改任何画布配置 - 所有这些操作仍然有效。仅仅是去掉了事件逻辑。
稍后,当我们查看自定义构建选项时,您将看到如果您只需要StaticCanvas,您可以仅仅创建更轻的Fabric版本。如果您需要非交互式图表,或者应用程序中包含过滤器的非交互式图像,这可能是一个不错的选择。
4、图片
说到图片....
既然在画布上画些矩形、圆形等很有趣,那我们为什么不玩些图片呢?正如你所想的那样,Fabric让这个操作变得很简单。实例化一个fabric.Image
对象,然后把它添加到canvas上
// html
<canvas id="c"></canvas>
<img src="my_image.png" id="my-image">
// js
var c= document.getElementById('canvas');
var canvas = new fabric.Canvas('c');
var imgElement = document.getElementById('my-image');
var imgInstance = new fabric.Image(imgElement, {
left: 100,
top: 100,
angle: 30,
opacity: 0.85
});
canvas.add(imgInstance);
请注意我们如何将图像元素传递给fabric.Image
构造函数。这将创建一个fabric.Image实例,它看起来就像文档中的图像。此外,我们立即将左/上值设置为100/100,角度设置为30,不透明度设置为0.85。一旦添加到画布,图像将以100,100位置呈现,呈30度角,并且略微透明!不错。
现在,如果我们在文档中没有真正的图像,但只有图像的URL,该怎么办?不是问题。我们来看看如何使用fabric.Image.fromURL
fabric.Image.fromURL('my_image.png', function(oImg) {
canvas.add(oImg);
});
看起来很简单,不是吗?只需使用图像的URL调用fabric.Image.fromURL,并在加载和创建图像后给它一个回调来调用。回调函数接收已创建的fabric.Image对象作为第一个参数。此时,您可以将其添加到画布或先更改图片的属性,然后将图片添加到画布:
fabric.Image.fromURL('my_image.png', function(oImg) {
// scale image down, and flip it, before adding it onto canvas
oImg.scale(0.5).set('flipX, true);
canvas.add(oImg);
});
5、路径
我们尝试过简单的形状,然后是图像。更复杂,更丰富的形状和内容呢?
强大的Path
和Gropus
就派上用场了。
Fabric中的路径表示可以以其他方式填充,描边和修改的形状的轮廓。路径由一系列命令组成,这些命令基本上模仿一支笔从一个点到另一个点。通过“移动”,“线”,“曲线”或“弧形”等,路径可以形成极其复杂的形状。通过使用路径组(PathGroup),可能性会更高。
Fabric中的路径与SVG 中<path>元素非常相似。它们使用相同的命令集,以<path>元素创建并序列化。我们稍后会仔细研究序列化和SVG解析,但是现在值得一提的是,很可能很少手动创建Path实例,相反,您将使用Fabric的内置SVG解析器。但为了了解Path对象是什么,我们手工创建一个简单的Path对象:
var canvas = new fabric.Canvas('c');
var path = new fabric.Path('M 0 0 L 200 100 L 170 200 z');
path.set({ left: 120, top: 120 });
canvas.add(path);
我们通过传递一串路径指令实例化fabric.Path
对象。虽然它看起来很神秘,但它实际上很容易理解。“M”代表“移动”命令,并告诉隐形笔移动到0,0点。“L”代表“线”并使笔画一条线到200,100点。然后,另一个“L”创建一条到170,200的线。最后,“z”告诉绘制笔关闭当前路径并最终确定形状。最终我们得到一个三角形的形状。
由于fabric.Path就像Fabric中的任何其他对象一样,我们也能够改变它的一些属性。但我们可以进一步修改它:
...
var path = new fabric.Path('M 0 0 L 300 100 L 200 300 z');
...
path.set({ fill: 'red', stroke: 'green', opacity: 0.5 });
canvas.add(path);
出于好奇,让我们来看一个稍微复杂的路径语法。你会明白为什么手工创建路径可能不是最好的主意。
...
var path = new fabric.Path('M121.32,0L44.58,0C36.67,0,29.5,3.22,24.31,8.41\
c-5.19,5.19-8.41,12.37-8.41,20.28c0,15.82,12.87,28.69,28.69,28.69c0,0,4.4,\
0,7.48,0C36.66,72.78,8.4,101.04,8.4,101.04C2.98,106.45,0,113.66,0,121.32\
c0,7.66,2.98,14.87,8.4,20.29l0,0c5.42,5.42,12.62,8.4,20.28,8.4c7.66,0,14.87\
-2.98,20.29-8.4c0,0,28.26-28.25,43.66-43.66c0,3.08,0,7.48,0,7.48c0,15.82,\
12.87,28.69,28.69,28.69c7.66,0,14.87-2.99,20.29-8.4c5.42-5.42,8.4-12.62,8.4\
-20.28l0-76.74c0-7.66-2.98-14.87-8.4-20.29C136.19,2.98,128.98,0,121.32,0z');
canvas.add(path.set({ left: 100, top: 200 }));
发生了什么?
好吧,“M”仍然代表“移动”命令,因此笔在“121.32,0”点开始绘制之旅。然后是“L”命令将它带到“44.58,0”。到现在为止还挺好。下一步是什么? “C”命令,代表“cubic bezier”,它使笔从当前点绘制贝塞尔曲线到“36.67,0”之一。它使用“29.5,3.22”作为一行开头的控制点,并使用“24.31,8.41”作为该行末尾的控制点。然后,整个事情之后是十几个其他立方贝塞尔命令,最终创建一个漂亮的箭头形状。
幸运的是,你不会直接来操作这些“野兽”级别的绘制。相反,您可以使用
fabric.loadSVGFromString
或fabric.loadSVGFromURL
方法来加载整个SVG文件,并让Fabric的SVG解析器完成其遍历所有SVG元素以及创建相应Path对象的工作。说到整个SVG文档,虽然Fabric的Path通常代表SVG <path>元素,但SVG文档中经常出现一系列路径集合表示Groups(fabric.Group实例)。现在你知道了,Group只是一组Paths和其他对象。而且由于fabric.Group继承自fabric.Object,它可以像任何其他对象一样添加到canvas中,并以相同的方式进行操作。
就像使用Paths一样,您可能不会直接使用Groups。但是如果你在解析SVG文档之后偶然发现了一个,你就会确切地知道它是什么以及它可以做什么。
6、后记
我们仅仅了解了Fabric的一点皮毛。您现在可以轻松创建任何简单的形状,复杂的形状,图像;将它们添加到画布中,并以您想要的任何方式进行修改 - 位置,尺寸,角度,颜色,笔触,不透明度 - 由您决定。
在本系列的下一部分中,我们将介绍groups、动画、文本、SVG解析、渲染、序列化、事件、图像滤镜以及更多。
期间,随意看看 annotated demos或者benchmarks,在 google group或 elsewhere
这里讨论,或直接看docs, wiki, and source.