原文链接:

image.png

今天我开始像您介绍 Fabric,一个功能强大的 Javascript 库,运行在 HTML5 canvas上,Fabric为 canvas 提供了缺失的对象模型,以及 SVG 解析器,一个交互层,以及一整套其他必不可少的工具。它是一个完全开放源码的项目,在MIT许可,多年来做出了许多贡献。

三年前我开始开发Fabric,在发现使用原生canvas的API之后,我正在为 printio.ru 创建一个互动设计编辑器:我的创业公司允许用户设计自己的服装。在这些日子里,我们想要的那种交互性只存在于Flash应用中。而现在,”Fabric” 成为可能。

让我们来看看吧!

一、为什么要做 fabric

Canvas 可以让我们在网络上创造出绝对惊人的图形。但它提供的API是令人失望的。如果我们只想在画布上画几条基本的形状,不会觉得有什么繁琐。但是一旦需要任何形式的互动,任何时候改变图片或绘制更复杂的形状,代码复杂度会急剧增加

Fabric旨在解决这个问题

原生canvas方法只允许我们触发简单的图形命令来盲目地修改canvas的位图。想画一个矩形?使用fillRect(left, top, width, height).。想画一条线?使用moveTo(left, top)lineTo(x, y)的组合命令。就好像我们用刷子画画,上层涂上越来越多的颜料,几乎没有控制。

Fabric不是在这么低的层次上运行,而是在原生方法之上提供简单而强大的对象模型。它负责 canvas 状态和渲染,并让我们直接使用绘制后的“对象”

让我们来看一个简单的例子来说明这个差异。假设我们想在画布上画一个红色的矩形。以下是我们如何使用原生的 API。

  1. // 有一个id是 c 的canvas元素
  2. var canvasEl = document.getElementById('c');
  3. // 获取2d位图模型
  4. var ctx = canvasEl.getContext('2d');
  5. // 设置填充颜色
  6. ctx.fillStyle = 'red';
  7. // 创建一个坐标100,190,尺寸是20,20的矩形
  8. ctx.fillRect(100, 100, 20, 20);

现在使用Fabric做同样的事情:

  1. // 用原生canvas元素创建一个fabric实例
  2. var canvas = new fabric.Canvas('c');
  3. // 创建一个矩形对象
  4. var rect = new fabric.Rect({
  5. left: 100,
  6. top: 100,
  7. fill: 'red',
  8. width: 20,
  9. height: 20
  10. });
  11. // 将矩形添加到canvas画布上
  12. canvas.add(rect);

image.png
在这种情况下,这两个例子非常相似,大小几乎没有差别。但是,您可以看到使用 canvas 的方法有多么不同。使用原生方法,我们在上下文中操作(表示整个画布位图的对象)。在Fabric中我们操作对象,实例化它们,更改其属性,并将其添加到 canvas 上,你可以看到这些对象是Fabric中的第一等公民

但渲染纯正的红色矩形就如此无聊。我们至少可以做一些有趣的事情!也许,稍稍旋转?

旋转45度,首先使用原生的canvas方法:

  1. var canvasEl = document.getElementById('c');
  2. var ctx = canvasEl.getContext('2d');
  3. ctx.fillStyle = 'red';
  4. ctx.translate(100, 100);
  5. ctx.rotate(Math.PI / 180 * 45);
  6. ctx.fillRect(-10, -10, 20, 20);

使用Fabric:

  1. var canvas = new fabric.Canvas('c');
  2. // 创建一个45度的矩形
  3. var rect = new fabric.Rect({
  4. left: 100,
  5. top: 100,
  6. fill: 'red',
  7. width: 20,
  8. height: 20,
  9. angle: 45
  10. });
  11. canvas.add(rect);

image.png
这里发生了什么?

我们在Fabric中所做的一切都是将对象的“角度”值更改为45。然而使用原生的方法,事情变得更加有趣,记住我们无法对对象进行操作,相反,我们调整整个画布位图(ctx.translate,ctx.rotate)的位置和角度,以适应我们的需要。然后,我们再次绘制矩形,但记住要正确地偏移位图(-10,-10),所以它仍然呈现在100,100点。作为练习,我们不得不在旋转画布位图时将度数转换为弧度。

我相信你刚刚开始明白为什么 Fabric 存在,以及它解决了多少低级写法

如果在某些时候,我们想将现在熟悉的红色矩形移动到canvas上稍微不同的位置怎么办?我们如何在无法操作对象的情况下执行此操作?我们会在canvas位图上调用另一个fillRect吗?

不完全的。调用另一个fillRect命令实际上在canvas上绘制的东西所有之上绘制矩形。还记得我前边说的用刷子画画吗?为了“移动”它,我们需要先擦除以前绘制的内容,然后在新的位置绘制矩形。

  1. var canvasEl = document.getElementById('c');
  2. ...
  3. ctx.strokRect(100, 100, 20, 20);
  4. ...
  5. // 擦除整个画布
  6. ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
  7. ctx.fillRect(20, 50, 20, 20);

我们如何用Fabric完成这个?

  1. var canvas = new fabric.Canvas('c');
  2. ...
  3. canvas.add(rect);
  4. ...
  5. rect.set({ left: 20, top: 50 });
  6. canvas.renderAll();

image.png
注意一个非常重要的区别。使用Fabric,在尝试“修改”任何内容之前,我们不再需要擦除内容。我们仍然使用对象,只需更改其属性,然后重新绘制 canvas 即可获得“最新图片”。

二、对象

我们已经看到如何通过实例化 fabric.Rect 构造函数来处理矩形。当然,Fabric也涵盖了所有其他的基本形状:圆,三角形,椭圆等。所有的这些在就是Fabric “命名空间”下的:如 fabric.Circlefabric.Trianglefabric.Ellipse等。

Fabric提供了7种基础形状:

想画一个圈子?只需创建一个圆形对象,并将其添加到画布。与任何其他基本形状相同:

  1. var circle = new fabric.Circle({
  2. radius: 20, fill: 'green', left: 100, top: 100
  3. });
  4. var triangle = new fabric.Triangle({
  5. width: 20, height: 30, fill: 'blue', left: 50, top: 50
  6. });
  7. canvas.add(circle, triangle);

image.png
这是一个绿色的圆形在100,100的位置,蓝色的三角形在50,50的位置。

2.1 操作对象

创建图形对象矩形,圆形或其他东西,当然只是开始。在某些时候,我们可能想修改这些对象。或许某些行为需要触发状态的变化,或进行某种动画。或者我们可能希望在某些鼠标交互中更改对象属性(颜色,不透明度,大小,位置)。

Fabric 为我们处理 canvas 渲染和状态管理。我们只需要自己修改对象。

以前的例子演示了set方法,以及如何从对象前一个位置调用set({left:20,top:50})来移动对象。以类似的方式,我们可以改变对象的任何其他属性。但这些属性是什么?

那么,正如你所期望的那样,可以改变定位:top、left,尺寸:width、height,渲染:fill、opacity、stroke、strokeWidth,缩放和旋转:scaleX、scaleY、angle,甚至可以翻转:flipX、flipY,歪斜:skewX、skewY。

是的,在 Fabric 中翻转对象非常简单,将flip[X|Y]设置为true即可。

您可以通过get方法读取任何这些属性,并通过set进行设置。我们尝试改变一些红色矩形的属性:

  1. var canvas = new fabric.Canvas('c');
  2. ...
  3. canvas.add(rect);
  4. rect.set('fill', 'red');
  5. rect.set({ strokeWidth: 5, stroke: 'rgba(100,200,200,0.5)' });
  6. rect.set('angle', 15).set('flipY', true);

image.png
首先,我们将“fill”值设置为“red”,使对象成为红色。下一个语句设置“strokeWidth”和“stroke”值,给出矩形为淡绿色的5px笔画。最后,我们正在改变“angle”和“flipY”的属性。注意每个3个语句中的每个语句使用的语法略有不同。

这表明set是一种通用的方法。你可能经常使用它,所以它被设计得尽可能的方便。

说了如何set的方法,那么如何获取呢?这就有了与之对应的get方法,还有一些具体的get*。要读取对象的“width”值,可以使用get('width')getWidth()。获取“scaleX”值使用get('scaleX')getScaleX(),等等。对于每个“公有”对象属性(“stroke”,“strokeWidth”,“angle”等),都可以使用getWidthgetScaleX等方法。

您可能会注意到,在早期的示例中,对象在实例化的时候,我们直接传入的配置参数,而上边的例子我们才实例化对象的时候没有传入配置,而是使用的set方法传递配置。这是因为它是完全一样的。您可以在创建时“配置”对象,也可以使用 set 方法:

  1. var rect = new fabric.Rect({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });
  2. // or functionally identical
  3. var rect = new fabric.Rect();
  4. rect.set({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });

2.2 默认选项

在这一点上,您可能会问,当我们创建对象而不传递任何“配置”对象时会发生什么。它还有这些属性吗?

当然是。 Fabric中的对象总是具有默认的属性集。当在创建过程中发生变化时,这是对给定的“给定”的默认属性集。我们可以自己试试看看:

  1. var rect = new fabric.Rect(); // notice no options passed in
  2. rect.get('width'); // 0
  3. rect.get('height'); // 0
  4. rect.get('left'); // 0
  5. rect.get('top'); // 0
  6. rect.get('fill'); // rgb(0,0,0)
  7. rect.get('stroke'); // null
  8. rect.get('opacity'); // 1

我们的矩形有一个默认的属性集。它位于0,0,是黑色,完全不透明,没有描边,没有尺寸(宽度和高度为0)。由于没有尺寸,我们无法在canvas上看到它。但是给它宽度/高度的任何正值肯定会在画布的左上角显示一个黑色矩形。
image.png

2.3 层次和继承

Fabric 对象不仅彼此独立存在。它们形成一个非常精确的层次。

大多数对象从根fabric.Object继承fabric.Object 几乎代表二维形状,位于二维 canvas 平面,它是一个具有 left/topwidth/height 属性的实体,以及一些其他图形特征。我们在对象上看到的那些属性(fill、stroke、angle、opacity、flip[X|Y])对于从 fabric.Object 继承的所有Fabric对象都是通用的

这个继承允许我们在 fabric.Object 上定义方法,并在所有的“类”之间共享它们。例如,如果您想在所有对象上使用 getAngleInRadians方法,您只需在 fabric.Object.prototype 上创建它即可:

  1. fabric.Object.prototype.getAngleInRadians = function() {
  2. return this.get('angle') / 180 * Math.PI;
  3. };
  4. var rect = new fabric.Rect({ angle: 45 });
  5. rect.getAngleInRadians(); // 0.785...
  6. var circle = new fabric.Circle({ angle: 30, radius: 10 });
  7. circle.getAngleInRadians(); // 0.523...
  8. circle instanceof fabric.Circle; // true
  9. circle instanceof fabric.Object; // true

您可以看到,方法立即在所有实例上可用

虽然子类“class”继承自 fabric.Object,但它们通常也定义自己的方法和属性。例如,fabric.Circle 需要有“radius”属性。而 fabric.Image(我们稍后会看),需要使用getElement/setElement 方法来访问/设置图像实例的真实HTMLFabric.js介绍(Part1) - 图8元素。

使用原型来获取自定义渲染和行为对于高级项目来说是非常普遍的。

三、Canvas

现在我们更详细地讨论了对象,让我们回到 canvas。

之前演示的所有的 Fabric 例子都会先创建canvas对象:new fabric.Canvas('...')。fabric.Canvas 作为围绕元素的包装器,负责管理该 canvas 上的所有 Fabric 对象。它需要一个元素的 id,并返回一个fabric.Canvas 的实例。

我们可以添加对象,引用它们,或者移除它们:

  1. var canvas = new fabric.Canvas('c');
  2. var rect = new fabric.Rect();
  3. canvas.add(rect); // add object
  4. canvas.item(0); // reference fabric.Rect added earlier (first object)
  5. canvas.getObjects(); // get all objects on canvas (rect will be first and only)
  6. canvas.remove(rect); // remove previously-added fabric.Rect

Fabric.Canvas 主要目的是用于管理对象,但它自身也是可配置的。假设需要为整个画布设置背景颜色或图像?亦或所有内容剪切到某个区域?亦或设置不同的宽度/高度?亦或指定画布是否互动?针对这些场景,无论是在创建时还是之后,都可以在fabric.Canvas 进行选项设置:

  1. var canvas = new fabric.Canvas('c', {
  2. backgroundColor: 'rgb(100,100,200)',
  3. selectionColor: 'blue',
  4. selectionLineWidth: 2
  5. // ...
  6. });
  7. // or
  8. var canvas = new fabric.Canvas('c');
  9. canvas.setBackgroundImage('http://...');
  10. canvas.onFpsUpdate = function(){ /* ... */ };
  11. // ...

3.1 互动性

我们来谈谈互动性。Fabric的独特功能之一,在我们刚刚看到的所有的对象模型之上有一层交互性

对象模型的存在使得我们可以进行编程访问和操纵 canvas 上的对象。但在外部,在用户层面上,有一种方式可以通过鼠标(或触摸,触摸设备)来操纵这些对象。一旦您通过 new fabric.Canvas('...')初始化 canvas,可以选择对象,拖动它们,缩放或旋转它们,甚至组合在一起操纵一个组合!
image.png
image.png
如果我们希望用户允许在画布上拖动某些东西,比如一个图片,我们需要做的就是初始化 canvas 并在其上添加一个对象,不需要额外的配置或设置。

为了控制这种交互性,我们可以在画布上使用Fabric的“selection”布尔属性,结合各个对象的“selectable”布尔属性来使用。

  1. var canvas = new fabric.Canvas('c');
  2. ...
  3. canvas.selection = false; // disable group selection
  4. rect.set('selectable', false); // make object unselectable

但是如果你不想要这样的交互层,该怎么办?如果是这种情况,您可以随时用 fabric.StaticCanvas 替换fabric.Canvas。初始化的语法是相同的,初始化时使用 StaticCanvas而不是 Canvas

  1. var staticCanvas = new fabric.StaticCanvas('c');
  2. staticCanvas.add(
  3. new fabric.Rect({
  4. width: 10, height: 20,
  5. left: 100, top: 100,
  6. fill: 'yellow',
  7. angle: 30
  8. }));

这将创建一个“较轻”的画布版本,没有任何事件处理逻辑。请注意,您仍然有一个完整的对象模型来使用:添加对象、删除或修改它们,以及更改任何画布配置,所有这一切仍然有效。这只是事件处理没有了。

当我们浏览自定义构建选项时,您会看到,如果 StaticCanvas 是您需要的选项,您甚至可以创建一个较轻的Fabric版本。如果您需要非交互式图表,或者在应用程序中使用过滤器的非交互式图像,这可能是一个不错的选择。

四、图像

说到图像…

在画布上添加矩形和圆圈很有趣,但为什么我们不玩一些图像呢?正如你现在想象的那样,Fabric 使这个很容易。我们来实例化 fabric.Image 对象并将其添加到画布中:

  1. <canvas id="c"></canvas>
  2. <img src="my_image.png" id="my-image">
  1. var canvas = new fabric.Canvas('c');
  2. var imgElement = document.getElementById('my-image');
  3. var imgInstance = new fabric.Image(imgElement, {
  4. left: 100,
  5. top: 100,
  6. angle: 30,
  7. opacity: 0.85
  8. });
  9. canvas.add(imgInstance);

注意我们如何将图像元素传递给 fabric.Image 构造函数。这将创建一个 fabric.Image 的实例,就像文档中的图像一样。此外,我们立即将左/顶值设置为100/100,角度为30,不透明度为0.85。一旦添加到画布中,图像呈现在100,100 位置,30度角,并且稍微透明!不错。
image.png

现在,如果我们在文档中真的没有图像,而只是一个图像的URL呢?我们来看看如何使用fabric.Image.fromURL

  1. fabric.Image.fromURL('my_image.png', function(oImg) {
  2. canvas.add(oImg);
  3. });

看起来很简单,不是吗?只需调用具有图像URL的fabric.Image.fromURL,并在加载和创建图像后给它一个回调函数来调用。回调函数接收已经创建的 fabric.Image 对象作为第一个参数。此时,您可以将其添加到画布中,也可以先更改,然后添加到画布:

  1. fabric.Image.fromURL('my_image.png', function(oImg) {
  2. // scale image down, and flip it, before adding it onto canvas
  3. oImg.scale(0.5).set('flipX', true);
  4. canvas.add(oImg);
  5. });

五、路径(Paths)

我们已经看过简单的形状,然后看了图像。那么更复杂,丰富的形状和内容呢?

认识更强大的搭档:路径(Path)组合(Group)

Fabric中的 path 表示可以填充,描边和修改的形状的轮廓。path由一系列命令组成,基本上模仿了从一个点到另一个点的笔。借助“move”,“line”,“curve”或“arc”等命令,path可以形成令人难以置信的复杂形状。在Paths组(PathGroup’s)的帮助下,可能性更大。

Fabric中的路径与SVG元素非常相似(注:也就是在Canvas基础上,Fabric内置了SVG解析器,可以实现SVG这种写法的解析)。它们使用相同的命令,可以从元素创建,并将其序列化。稍后我们将更加仔细地观察序列化和SVG解析,但现在值得一提的是,您很可能很少手动创建Path实例,相反,您将使用Fabric的内置SVG解析器。但是要了解Path对象是什么,我们来尝试用手创建一个简单的对象:

  1. var canvas = new fabric.Canvas('c');
  2. var path = new fabric.Path('M 0 0 L 200 100 L 170 200 z');
  3. path.set({ left: 120, top: 120 });
  4. canvas.add(path);

image.png
我们通过传递一串路径指令,实例化 fabric.Path对象,虽然看起来很神秘,但实际上很容易理解。“M”代表“move”命令,并告诉笔移动到0,0坐标。“L”表示“line”,笔画线为200,100坐标。然后,另一个“L”创建一个连接170,200坐标的线段。最后,“z”指示绘画笔关闭当前路径并完成形状。结果,我们得到一个三角形。

由于fabric.Path与Fabric中的任何其他对象一样,我们还可以更改其某些属性。但是我们可以修改更多:

  1. ...
  2. var path = new fabric.Path('M 0 0 L 300 100 L 200 300 z');
  3. ...
  4. path.set({ fill: 'red', stroke: 'green', opacity: 0.5 });
  5. canvas.add(path);

image.png
出于好奇,我们来看一下稍微更复杂的path语法。你会看到为什么手动创建路径可能不是最好的主意。

  1. ...
  2. var path = new fabric.Path('M121.32,0L44.58,0C36.67,0,29.5,3.22,24.31,8.41\
  3. 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,\
  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\
  5. 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\
  6. -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,\
  7. 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\
  8. -20.28l0-76.74c0-7.66-2.98-14.87-8.4-20.29C136.19,2.98,128.98,0,121.32,0z');
  9. canvas.add(path.set({ left: 100, top: 200 }));

看一下这里发生了什么?

那么“M”仍然代表“move”的命令,所以笔在“121.32,0”点开始绘画。然后有“L”命令使其为“44.58,0”。到现在为止还可以理解。下一步是什么? “C”指令,代表“cubic bezier”(贝塞尔曲线)。它使笔从当前点绘制贝塞尔曲线到“36.67,0”,它以“29.5,3.22”为起点的控制点,“24.31,8.41”为终点的控制点。这整个事情之后是十几个其他的“cubic bezier”命令,最终创建一个漂亮的箭头形状。
image.png
不过你可能永远不会使用这样复杂的命令,相反,您可能需要使用像 fabric.loadSVGFromStringfabric.loadSVGFromURL方法来加载整个SVG文件,然后让 Fabric 的 SVG 解析器完成对所有 SVG 元素的遍历和创建相应的 Path 对象的工作。

谈到整个 SVG 文件,而 Fabric 的路径通常表示SVG<path>元素,SVG 文档中通常存在的路径集合表示为Group(fabric.Group 实例)。你可以想像,Group(组合)只不过是一组 Path 实例和其他对象。而且由于 fabric.Groupfabric.Object 继承,它可以像任何其他对象一样添加到画布中,并以相同的方式进行操作。

就像 Path 一样,你可能不会直接的使用它。但是,如果您在解析SVG文档之后偶然发现了一个问题,那么您将确切地知道它是什么以及它的目的是什么。

六、后记

我们只触及了Fabric的表面。你现在可以很容易地创建任何一个简单的形状、复杂的形状、图像,将它们添加到画布中,并以任何你想要的方式进行修改:位置、尺寸、角度、颜色、笔画、不透明度等。

在本系列的下一部分,我们将看看组合,动画,文本,SVG解析,渲染,序列化,事件,图像,滤镜等。

与此同时,你可以随意查看带注释的演示基准,加入Google小组其他地方的讨论,或者直接访问docswiki源代码

做一些有趣的实验!我希望你喜欢这次旅行。