本文来自于 Fabricjs中文版 ,这个站点是我经过社区允许后部署的一份国内版,希望招募同样对fabricjs感兴趣的人一起来翻译建设,在每篇文章下面都有提PR的链接,只要使用github在写编辑即可
今天 我要向你们介绍 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 在基础上提供了一个简单且强大的对象模型. 更注重于画布的状态和渲染, 现在, 让我们开始学习使用 “对象”吧.
让我们通过一个简单的例子 画一个红色的矩形 来看看两者有什么不同. 这是原生 <canvas> 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);
目前为止, 最不同的地方在尺寸的设置 — 两个例子很像. 但是你应该也意识到两种操作思想的不同了吧. 用原生方法, 我们 操作context上下文 — 代表着整个canvas. 用 Fabric, 我们 在具体对象上操作 — 实例化它们, 修改他们的属性, 然后给它们添加到canvase上. 这些对象是fabricjs世界的第一公民.
但是画一个红色的矩形没啥难度. 我们来整点有意思的! 比如, 稍微的旋转一下?
我们试试旋转45度. 首先, 使用原生 <canvas> 方法:
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);
发生了什么?
我们只需要修改对象的'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();
注意最重要的区别. 用过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);
..现在我们在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);
首先, 我们设置 “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, 所以我们看不见. 只要给宽高附上正整数后,我们就能看在一个黑色矩形在画布左上角.
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 包裹着 <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'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)
<canvas id="c"></canvas>
<img src="my_image.png" id="my-image">
(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度, 还有轻微的透明度. 不错吧
那么, 如果文档中没有这个图片元素怎么办, 我们只是有一个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 <path> 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.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);
出于好奇, 让我们试一试稍微复杂一点的图形. 你会发现我之前说的对,没有办法手写路径.
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\