(二)Fabric: Part 1

原文链接
使用
<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.Circlefabric.Trianglefabric.Ellipse等在fabric命名空间下暴露出来。
Fabric提供了其中基础形状:

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度角,并且略微透明!不错。

image.png

现在,如果我们在文档中没有真正的图像,但只有图像的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、路径

我们尝试过简单的形状,然后是图像。更复杂,更丰富的形状和内容呢?
强大的PathGropus就派上用场了。
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”告诉绘制笔关闭当前路径并最终确定形状。最终我们得到一个三角形的形状。

image.png

由于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);
image.png

出于好奇,让我们来看一个稍微复杂的路径语法。你会明白为什么手工创建路径可能不是最好的主意。

...
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”作为该行末尾的控制点。然后,整个事情之后是十几个其他立方贝塞尔命令,最终创建一个漂亮的箭头形状。

image.png

幸运的是,你不会直接来操作这些“野兽”级别的绘制。相反,您可以使用fabric.loadSVGFromStringfabric.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 groupelsewhere
这里讨论,或直接看docs, wiki, and source.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352