theydy / notebook

记录读书笔记 + 知识整理,vuepress 迁移中 https://theydy.github.io/notebook/
0 stars 0 forks source link

fabric.js part 1 #2

Open theydy opened 5 years ago

theydy commented 5 years ago

Why fabric?

时至今日,我们已经能够使用canvas 在web 上创建出一些非常精美的图形了。不过令人失望的是,原生canvas 提供的API 都太难用了,除非你只是想用canvas 画出一些简单的图形。否则一旦碰上需要实现各种交互效果,在图片上的任何一点做改动又或者是画一些更复杂的多边形的情况,使用原生API 就变成了一件非常痛苦的事情。

Fabric 就是为了解决这些问题而生的。

原生canvas 方法只允许我们使用一些简单的图形命令来盲目的修改整个canvas 画布。比如,想要画一个矩形?可以用 fillRect(left, top, width, height) 。想要画一条线?你需要 moveTo(left, top)lineTo(x, y) 的组合。这就像是我们用一只笔在画布上画画,除了最原始最简单的一笔一笔往上画之外别无它法。

和原生canvas 提供的这些低级API 不同的是,Fabric 在基于原生的方法上为我们提供了同样简单但是却更强大的对象模型,它不仅会帮我们负责画布的状态和渲染,还允许我们使用对象指令操作canvas。

现在让我们通过一个简单的例子来对比其中的差异吧,比如说我们想要在画布上画一个红色的矩形,使用原生的API 我们可以这么做。

// reference canvas element (with id="c")
var canvasEl = document.getElementById('c');

// get 2d context to draw on (the "bitmap" mentioned earlier)
var ctx = canvasEl.getContext('2d');

// set fill color of context
ctx.fillStyle = 'red';

// create rectangle at a 100,100 point, with 20x20 dimensions
ctx.fillRect(100, 100, 20, 20);

现在,让我们看看Fabric 是怎么做的。

// create a wrapper around native canvas element (with id="c")
var canvas = new fabric.Canvas('c');

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

// "add" rectangle onto canvas
canvas.add(rect);

image

目前为止,两者的差别可能并不大——这两个例子非常相似。然而,你已经可以看到两者对于canvas 的操作有所不同了。在使用原生的例子中,我们操作context——一个表示画布的canvas 对象。在使用Fabric 的例子中,我们操作一个Fabric 实例对象,改变它的属性,最后add 进canvas 中。你可以发现在fabric 中这些对象是一等公民。

只是画一个红色矩形未免太无聊了,也许我们还能做一些更有趣的事情,比如旋转?

让我们来试试旋转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);

image

这里都发生了些什么?

我们在Fabric 中唯一做的事就是指定了object 的angle 属性值为 45。然而在使用原生方法的例子中,事情好像变得“有趣”了,记住在原生方法中,我们不能直接操作object,取而代之的是,我们调整了整个画布的位置和角度 (ctx.translate, ctx.rotate) 来适应我们的需求,然后再画一个矩形,不要忘了计算矩形坐标时需要考虑到画布坐标的偏移(-10,-10),以便它还是从100,100 这个点开始渲染。在旋转画布时我们还需要额外做一件事,就是把旋转的角度转换为弧度。

我相信你已经发现为什么Fabric 会有存在的必要了,并且发现Fabric 为我们隐藏了大量底层的细节。

让我们再看一个例子——追踪canvas 的状态。

假设现在有这么个需求,我们想要把原来的红色矩形移动到一个不同的位置?在不操作object 的情况向我们能怎么做?难道只是再调用一次 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 canvas = new fabric.Canvas('c');
...
canvas.add(rect);
...

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

image

请注意这里有一个非常重要的不同,在Fabric 中,我们不再需要在修改绘画内容前先把画布清空了,我们只需要对object 做一些工作,简单的改变他的属性值,然后重新渲染画布获得新的图片。

Objects

我们已经知道如何通过实例化fabric.Rect 来操作一个矩形了,理所当然的Fabric 也支持其他的所有基本图形——圆,三角,椭圆等等,所有的这些基本图形都暴露在 fabirc 这个命名空间下,fabric.Circlefabric.Trianglefabric.Ellipse 等等。

Fabric 提供了7种内置基本图形。

想过画个圆?只要新建一个circle 的对象,然后添加进画布即可,你可以用其他基本图形做同样的事。

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

image

通过上面的例子,我们就能够得到一个位于100,100 的绿色圆形,和一个位于50,50 的蓝色三角形。

Manipulating objects

创建一个图形对象——矩形,圆或者是其他——这只是开始的第一步,在某些情况下,我们可能想要对对象做些改动,可能某些改动需要触发状态的变化,或者是播放某种动画,又或者是在某个鼠标交互下改变对象的某个属性(颜色,透明度,大小,位置) 。

Fabric 会帮我们管理canvas 的渲染和转态,使得我们只需要关心改变object 本身。

早先的例子展示了 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 方法获得任何一个属性值,或用 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);

image

首先,我们设置fill 的值为red,实质上是指定对象为红色,接着声明了strokeWidth 和stroke 的值,给矩形设置5px 的淡绿色边框,最后,我们改变了angle 和flipY 属性,注意这三句声明代码在语法上都略有不同。

可以发现 set 是一个使用率很高的方法,你以后可能会经常使用它,这也意味着它应该竟可能易于使用。

我们说完setters,应该说getters 了?很明显的,不仅有通用的 get 方法,而且还有特殊的 get* 方法,想要获得"width"的值,你可以使用 get('width')getWidth()。读取"scaleX"的值——get('scaleX')getScaleX() 等等。每个属性都有类似 getWidth()getScaleX() 这样的方法("stroke", "strokeWidth", "angle" 等等)

通过前面的例子你也许会注意到,无论是我们在创建object 时传递options 设置属性值还是之后我们调用 set 方法设置属性值,最后结果是一样的。因为这两者确实有着相同的效果。你可以选择在创建时传递参数设置对象属性值,或者在之后使用 set 设置属性值。

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

// or functionally identical

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

Default options

看到这里,你可能会问——如果我们在创建对象时不传配置项,又会发生什么呢? 最后返回的对象实例有没有这些属性呢?

当然有,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 坐标,黑色完全不透明并且无边框无大小(高度宽度都为0)的矩形。因为没有大小,所以我们不能在画布中看到它,但只要给它一个宽高,就能在左上角看到一个黑色矩形。

image

Hierarchy and Inheritance

Fabric 对象相互之间并不是完全独立的,而是有着非常精准的层次结构的。

大部分的对象都继承至 fabric.Objectfabric.Object 代表一个位于二维平面的二维图形,它也有left/top 和width/height 属性,以及一系列图形特征,我们前面接触的那些属性——fill,stroke,angle,opacity,flip* 等等——这些所有fabric 对象共有的属性都继承至 fabric.Object

由于有这一层继承的存在,就允许我们通过在 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> 元素时会被使用到。

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

Canvas

我们已经详细的说完了fabric 对象方法,现在让我们回到canvas 看看。

你能看到在所有的Fabric 例子中,第一件事永远是先创建一个canvas 对象——new fabric.Canvas('...')。fabric.Canvas 充当一个\<canvas> 元素的包裹器,并负责管理这块画布上的所有fabric 对象。它在创建时需要传递一个元素id,返回一个 fabric.Canvas 实例。

我们可以把fabric 对象 add 进fabric.Canvas 实例中,引用它们或删除它们。

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

canvas.add(rect); // add object

canvas.item(0); // reference fabric.Rect added earlier (first object)
canvas.getObjects(); // get all objects on canvas (rect will be first and only)

canvas.remove(rect); // remove previously-added 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(){ /* ... */ };
// ...

Interactivity

to be continue...