Fabric.js - 轻而易举操作canvas的神奇库

本文来自于Fabricjs中文版,这个站点是我经过社区允许后部署的一份国内版,希望招募同样对fabricjs感兴趣的人一起来翻译建设,在每篇文章下面都有提PR的链接,只要使用github在写编辑即可


Fabric.js - 轻而易举操作canvas的神奇库_第1张图片
今天 我要向你们介绍 Fabric.js — 一个能够让你轻而易举操作canvas的神奇的库. Fabric 不仅提供了一个虚拟canvas对象, 还有svg渲染器, 交互层, 还有一整套十分有用的工具. 这是一个完全开源的项目, MIT协议, 多年以来依靠许多贡献者共同维护.

Fabric 开始与2010, 在经历过原生canvas 繁琐的API操作之后. 原作者就写了一个可交互的编辑器 printio.ru — 允许用户自定义外观. 那时候只有flash app需要这种交互. 光阴似箭,经过一点点积累形成了现在的 Fabric.

让我们进一步看一看!

为什么选择fabric?
现在的Canvas 支持我们去创造一些 充满创造力 神奇的 图形 但是它提供的api实在是 水平低到令人发指. 如果我们只是想画一些简单的图形. 但是却需要一系列操作, 各种修改中心点, 如果要画一个复杂图形 — 那操作就“更有意思了”.

Fabric 的目标就是解决这些问题.

原生 canvas 方法 只允许我们使用一些简单的图形操作, 然后在画布上瞎子摸象. 想画一个矩形? 使用 fillRect(left, top, width, height). 想画一条线? 用 moveTo(left, top) 和 lineTo(x, y)组合. 这感觉就像 用画笔在画布上画画, 随着画的越来越多, 画布内容的可控性就越差.

为了避免这种低水平操作, Fabric 在基础上提供了一个简单且强大的对象模型. 更注重于画布的状态和渲染, 现在, 让我们开始学习使用 “对象”吧.

让我们通过一个简单的例子 画一个红色的矩形 来看看两者有什么不同. 这是原生 API的实现


// 获取画布的引用
var canvasEl = document.getElementById('c');

// 获取2d context 对象 用来操作 (之前提到的bitmap)
var ctx = canvasEl.getContext('2d');

// 给当前上下文设置颜色
ctx.fillStyle = 'red';

// 在100, 100的位置创建一个20*20的矩形
ctx.fillRect(100, 100, 20, 20);
现在,让我们看看fabricjs怎么实现同样的效果:

// 包裹一下canvas (with id="c")
var canvas = new fabric.Canvas('c');

// 新建一个矩形对象
var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 20,
  height: 20
});

// 将矩形添加到canvas里
canvas.add(rect);

Fabric.js - 轻而易举操作canvas的神奇库_第2张图片
目前为止, 最不同的地方在尺寸的设置 — 两个例子很像. 但是你应该也意识到两种操作思想的不同了吧. 用原生方法, 我们 操作context上下文 — 代表着整个canvas. 用 Fabric, 我们 在具体对象上操作 — 实例化它们, 修改他们的属性, 然后给它们添加到canvase上. 这些对象是fabricjs世界的第一公民.

但是画一个红色的矩形没啥难度. 我们来整点有意思的! 比如, 稍微的旋转一下?

我们试试旋转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 canvas = new fabric.Canvas('c');

// create a rectangle with angle=45
var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 20,
  height: 20,
  angle: 45
});

canvas.add(rect);

Fabric.js - 轻而易举操作canvas的神奇库_第3张图片

发生了什么?

我们只需要修改对象的's “angle” 为 45. 使用原生方法, 事情变得越来越 “有趣了”. 我们不能操作对象. 反而为了实现需求, 我们给整个 canvas bitmap旋转了 (ctx.translate, ctx.rotate). 然后在画矩形上去, 别忘了原点定位为 (-10, -10), 这样才能看起来是在(100, 100).

现在我确信你已经清楚了fabricjs存在的意义,还有他帮助我门减少了多少低级代码.

让我们再看另一个例子 — 追踪canvas状态.

假设在某一个点, 我们想要移动刚才的矩形到canvas上的另一个点? 如果不能操作对象,我们会怎么做? 是不是只能再调一遍 fillRect?

不仅如此. 在调用另一个 fillRect 时候,我们在画布上画了一个新的矩形,但是现在已经有一个了. 记得我之前提到的擦出功能吗? 为了 “移动” , 我们必须先 擦除之前的画布, 然后在新的位置画新的矩形.


var canvasEl = document.getElementById('c');

...
ctx.strokRect(100, 100, 20, 20);
...

// 清除整个画布
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
ctx.fillRect(20, 50, 20, 20);
用fabric怎么实现?

var canvas = new fabric.Canvas('c');
...
canvas.add(rect);
...

rect.set({ left: 20, top: 50 });
canvas.renderAll();

Fabric.js - 轻而易举操作canvas的神奇库_第4张图片
注意最重要的区别. 用过Fabricjs, 我们不用再为了实现“移动”清除上一个画布. 只需要操作对象, 简单的修改它们的属性, 然后 re-render canvas 获取 “最新的画面”.

对象
现在我们已经了解了怎么操作 fabric.Rect 构造函数生成实例. 当然Fabric默认包含了许多基础形状 — 原, 三角, 椭圆, 等等. 这些都挂载在 fabric “变量下” 像 fabric.Circle, fabric.Triangle, fabric.Ellipse, 等等.

Fabric 提供的7中基础图形:

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);

Fabric.js - 轻而易举操作canvas的神奇库_第5张图片

..现在我们在100, 100有了一个绿色的原型, 50, 50有了一个蓝色的三角形.

操纵 对象
创建几何图形 — 矩形, 圆, 或者其他 — 仅仅是开始. 以后, 我们可能需要修改这些对象. 也许是某些动作触发的改变, 或播放某种动画. 或者想要在鼠标事件时修改对象某些属性 (颜色, 透明度, 尺寸, 位置).

Fabric 替我们关心了渲染和状态维护的事情. 我们只需要在对象上做手脚就行.

之前的例子展示了 set 方法 并且调用 set({ left: 20, top: 50 })从之前的位置 “移动”走了. 类似的方式, 我们可以修改任何属性. 但是有啥属性呢?

正如你期望的,与位置相关 — left, top; 尺寸相关 — width, height; 渲染相关 — fill, opacity, stroke, strokeWidth; 缩放和旋转 — scaleX, scaleY, angle; 甚至有翻转 — flipX, flipY 和倾斜 skewX, skewY

是的, 在fabric中创建翻转对象 只需要给flip*属性设置为 true.

你可通过 get method读取所有属性, 然后使用 set方法. 让我们试试修改红色矩形的属性:

var canvas = new fabric.Canvas('c');
...
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);

Fabric.js - 轻而易举操作canvas的神奇库_第6张图片

首先, 我们设置 “fill” 为 “red”, 将图像变成红色的了. 下一行设置了 “画笔宽度” 和 “画笔颜色” 的值, 给矩形一个5px宽的浅绿色边框. 最后, 我们修改 “angle” 和 “flipY” 属性. 注意三行不同的设置语法,都支持.

这个例子展示了 set方法的通用性. 你以后会经常用到它, 所以这个方法尽量的支持各种使用方法.

讲完了设置属性, 那么如果获取呢? 只需要使用通用的 get 方法 , 当然还有各种特殊的 get* ,来获取一个属性. 想要获取一个对象的“width”, 可以用 get('width') 或者 getWidth(). 想要获取 “scaleX” 属性 — get('scaleX') 或者 getScaleX(), 等等. 对象 “公共” 属性都有 getWidth 或 getScaleX这种方法 (“stroke”, “strokeWidth”, “angle”, 等.)

你可能注意到了,在之前的例子里 写初始化配置生成的对象和使用 set 方法创建的没什么区别.这是因为他们就是完全一样. 你可以在初始化的时候使用 “配置”, 或者在创建对象之后再使用 set 方法:

var rect = new fabric.Rect({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });

// 一样的

var rect = new fabric.Rect();
rect.set({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });

默认配置
到这里, 你可能想问 — 不传 “配置”创建对象发生了什么. 还有那些属性吗?

当然有. Fabricjs中的对象一直会有默认属性的. 如果在创建的时候省略, 就是使用默认值. 试试看:

var rect = new fabric.Rect(); // 没有配置传入

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, 黑色, 完全不透明, 没有边框 没有尺寸 (宽高都是0). 因为都是0, 所以我们看不见. 只要给宽高附上正整数后,我们就能看在一个黑色矩形在画布左上角.

Fabric.js - 轻而易举操作canvas的神奇库_第7张图片

Hierarchy and Inheritance
Fabric 对象并不是独立存在的.他们都继承自一个源对象

大多数对象都继承自根对象 fabric.Object. fabric.Object 代表着一个二维,有着坐标和宽高, 以及一系列其他图形特征. 这些就是之前看到的对象属性 — fill, stroke, angle, opacity, flip*, 等.

使用继承,可以让我们在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.Circle 需要 “radius” 属性. fabric.Image — 在下面会讲到 — 需要 getElement/setElement 方法 用于访问/设置HTML元素.
在高级项目中 使用原型来获取自定义渲染和行为非常常见.

Canvas
现在已经详细的说完了Fabricjs对象, 让我门回过头来说一下canvas.

在所有的Fabricjs例子中,你看到的第一行是不是创建canvas对象? — new fabric.Canvas('...'). fabric.Canvas 包裹着 元素, 它负责着所有Fabric.Object对象. 传入一个id, 返回 fabric.Canvas 实例.

我们可以将对象 add 进去, 通过引用关系, 也可以删除他们:


var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect();

canvas.add(rect); // 添加进去

canvas.item(0); //  获取刚才添加的fabric.Rect
canvas.getObjects(); // 获取画布中所有的对象

canvas.remove(rect); // 删除fabric.Rect

所以 fabric.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(){ /* ... */ };
// ...

交互功能
我们现在来说一说交互功能. 一个独特的Fabricjs功能 — 内置的 — 在对象层之上的交互层.

对象模型的存在允许以编程方式访问和操作画布上的对象. 但是对于用户来说, 需要用鼠标或者手指操作. 当你通过 new fabric.Canvas('...')初始化画布之后, 可以选择 拖动 旋转 缩放等,甚至是 群选 之后一起操作!

Fabric.js - 轻而易举操作canvas的神奇库_第8张图片Fabric.js - 轻而易举操作canvas的神奇库_第9张图片

如果我们想让用在在画布上拖拽一些东西 — 比如一张图片 — 我们只需要创建画布,加一个图片进去. 不需要任何额外的操作.

我们可以使用把布尔值传给 Fabric's “selection” 或者对象的穿一个布尔值给对象的“selectable”字段 来控制是否可交互.


var canvas = new fabric.Canvas('c');
...
canvas.selection = false; // 关闭群选
rect.set('selectable', false); // 单个对象不可选

如果不想要这个交互功能? 你可以使用fabric.StaticCanvas 代替 fabric.Canvas . 别的都一样.


var staticCanvas = new fabric.StaticCanvas('c');

staticCanvas.add(
  new fabric.Rect({
    width: 10, height: 20,
    left: 100, top: 100,
    fill: 'yellow',
    angle: 30
  }));

这样创建了一个 “轻量” 版本的 canvas, 没有任何事件处理逻辑. 你还是可以操作 全部的对象模型 — 添加对象, 删除或修改他们, 或者修改 canvas 配置 — 这些都还是一如既往能用. 只是事件系统没了.

总之,如果你需要一个不需要交互的画布,就要择更轻量的 StaticCanvas 就够了.

图片
说到图片…

在画布上玩矩形圆形没什么意思,我们来试试玩图片? 正如你认为的, Fabric 也让这个变得简单. 让我们实例化一个 fabric.Image 对象然后把它添加进canvas:

(html)


(js)

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度, 还有轻微的透明度. 不错吧

Fabric.js - 轻而易举操作canvas的神奇库_第10张图片

那么, 如果文档中没有这个图片元素怎么办, 我们只是有一个url? 那么就到了 fabric.Image.fromURL派上用场的时候了

fabric.Image.fromURL('my_image.png', function(oImg) {
canvas.add(oImg);
});
看起来是不是非常简单直观? 只是调用 fabric.Image.fromURL, 传了url, 在图片加载完成之后调用一下回调函数. 回调函数的第一个默认传参就是 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);
});

路径
我们先了解了简单的图形, 然后是图片. 下面看看更复杂的图形和内容

首先看一对强力组合 — 路径 和 分组.

在Fabric中,路径代表着一个可以背修改,填充,描边的形状. 路径是由一堆命令组成的, 本质上是在模仿一支笔从一个点到另一个点. 通过 “move”, “line”, “curve”, 或者 “arc”命令, 可以组成神奇的图案. 借助路径的分组功能Paths (PathGroup's), 让用户发挥想象的空间就更大了.

Fabric中的Paths 与 SVG elements很像. 使用相同的语法, 所以可以互相转化. 稍后我们将更仔细地研究序列化和 SVG 解析, 先提醒你一下,你后几乎不会手动创建Path实例. 而你会经常使用 Fabric's 内置的 SVG 渲染器. 但是为了理解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.js - 轻而易举操作canvas的神奇库_第11张图片

我们实例化了一个 fabric.Path 对象, 给它传了一串字符串路径指令. 虽然看起来神秘, 但是它其实很容易理解. “M” 代表 “move” 命令, 命令那只不可见的笔指在0,0的位置. “L” 代表着 “line” 用笔画到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);

Fabric.js - 轻而易举操作canvas的神奇库_第12张图片
出于好奇, 让我们试一试稍微复杂一点的图形. 你会发现我之前说的对,没有办法手写路径.


...
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” 还是代表着 “move”, 所以画笔从 “121.32, 0” 开始. 然后 “L” 代表这画一条直线搭到 “44.58, 0”. 目前位置还能接受. “C” 命令, 代表着 “三次贝塞尔曲线”. 命令画笔从当前位置到 “36.67, 0”画一条三次贝塞尔曲线. 开始控制点为“29.5, 3.22”,结束控制点为 “24.31, 8.41”. 然后再跟上一堆的贝塞尔曲线, 最终形成了这个好看的箭头.

Fabric.js - 轻而易举操作canvas的神奇库_第13张图片

通常来说, 你不会直接这么“粗暴”的使用, 你可能会用到 fabric.loadSVGFromString 或 fabric.loadSVGFromURL 这类方法来加载SVG文件, 把这些工作全部交给Fabric.

说到整个 SVG 文档, Fabric的path 代表着 SVG 元素, SVG 文档中经常出现的路径集合, 在Fabric中就是组的概念 (fabric.Group 实例). 正如你想到的, 组不过是一组路径和其他对象的集合. 并且fabric.Group 继承自 fabric.Object, 所以它的添加、修改行为和其他对象一样.

就像使用路径一样,您可能不会直接使用它们. 但是一旦你需要,你应该知道它的原理.

后记
我们只是介绍了Fabric一些基础的东西. 你现在可以轻松的在canvas上操作简单的复杂的图形,图片了。 — 位置, 尺寸, 旋转, 颜色, 边框, 透明度.

该系列的下一章, 我们会讲组; 动画; 文班; SVG 解析, 渲染, 序列化; 事件; 图片滤镜等等.

与此同时, 去看看 示例 或者 基础数据 或者 别的地方, 或者直接看 文档, wiki, 和 源代码.

希望Fabric能给你带来乐趣.

阅读 Part 2.

你可能感兴趣的:(Fabric.js - 轻而易举操作canvas的神奇库)