熟悉 canvas 的朋友想必都使用或者据说过 Fabric.js,Fabric 算是一个元老级的 canvas 库了,从第一个版本发布到如今,已经有 8 年时间了。我近一年时间也在项目中使用,做为用户简单说说感觉:javascript
优缺点都很鲜明,但总的来讲,若是你要作一个在线编辑类的项目,好比在线 PPT,在线制图等应用,fabric 绝对是个很好的选择。html
那么这一系列文章要写什么?这里不会主要介绍如何使用 fabric,主要写的内容是把在阅读源码过程当中,把涉及到原理相关的知识总结出来,好比相关图形学知识、canvas 相关、fabric 中的设计思想等的相关知识。因此,若是你如今还对 fabric 不是很了解,建议先去官网找几个 demo 试一下。java
下面咱们进入此次的正题,这篇文章主要介绍 fabric.canvas 涉及到的部份内容。git
fabric 建立画布很简单:github
const canvas = new fabric.Canvas("domId", options);
复制代码
在这样一行代码背后,fabric 主要作了下面这几件事情:算法
下面我把相关内容一一阐述。canvas
介绍 canvas 缓存,fabric 中的缓存也是相似的道理,简单来讲,就是使用一个离屏 canvas 来作预渲染,在真实画布上用 drawImage 代替直接绘制图形。缓存
咱们先来看个 例子,你们能够把 FPS meter 打开,切换按钮能够看到,不使用缓存和使用缓存 FPS 值差距仍是挺大的,我电脑在使用缓存的时候基本在 60fps,不使用会降到 15fps 左右。你们能够打开控制台或者在 这里 查看代码。 下面列出主要的代码片断:dom
class Ball {
constructor(x, y, vx, vy, useCache = true) {
// ...
if (useCache) {
this.useCache = useCache;
this.cacheCanvas = document.createElement("canvas");
// 离屏 canvas 宽高取要渲染图形的宽高,不能够取真实 canvas 的宽高,不然会渲染大量无用区域
this.cacheCanvas.width = 2 * (this.r + BORDER_WIDTH);
this.cacheCanvas.height = 2 * (this.r + BORDER_WIDTH);
this.cacheCtx = this.cacheCanvas.getContext("2d");
this.cache();
}
}
paint() {
// 使用缓存直接使用建立的离屏canvas,不然直接绘制图形
if (!this.useCache) {
ctx.save();
ctx.lineWidth = BORDER_WIDTH;
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
ctx.stroke();
ctx.restore();
} else {
ctx.drawImage(
this.cacheCanvas,
this.x - this.r,
this.y - this.r,
this.cacheCanvas.width,
this.cacheCanvas.height
);
}
}
move() {
// ...
}
cache() {
// 绘制图形
this.cacheCtx.save();
this.cacheCtx.lineWidth = BORDER_WIDTH;
this.cacheCtx.beginPath();
this.cacheCtx.strokeStyle = this.color;
this.cacheCtx.arc(
this.r + BORDER_WIDTH,
this.r + BORDER_WIDTH,
this.r,
0,
2 * Math.PI
);
this.cacheCtx.stroke();
this.cacheCtx.restore();
}
}
复制代码
解释一下两者区别:函数
使用缓存:在实例化每一个图形的时候(渲染以前),先将图形渲染到一个离屏的 canvas 上,在渲染的时候,直接用 drawImage
将离屏的 canvas 渲染。
不使用缓存: 在渲染的时候直接绘制图形
使用缓存的时候,有一点须要注意的是要控制好离屏 canvas 的大小,不能够直接取和渲染 canvas 的实际宽高,不然会渲染不少无用的空间,好比上面例子中每一个离屏 canvas 的宽高只须要和对应图形的宽高一致。
this.cacheCanvas.width = 2 * (this.r + BORDER_WIDTH);
this.cacheCanvas.height = 2 * (this.r + BORDER_WIDTH);
复制代码
上述代码中主要节省时间的地方在 paint
函数中使用 drawImage
会比直接绘制图形节省时间,那么是否全部场景都是这样呢?咱们再来看下面这个 例子.
这个例子和上面的只有绘制图形的代码不一样:
// 从复杂图形变成了简单图形
cache() {
this.cacheCtx.save();
this.cacheCtx.lineWidth = BORDER_WIDTH;
this.cacheCtx.beginPath();
this.cacheCtx.strokeStyle = this.color;
this.cacheCtx.arc(
this.r + BORDER_WIDTH,
this.r + BORDER_WIDTH,
this.r,
0,
2 * Math.PI
);
this.cacheCtx.stroke();
this.cacheCtx.restore();
}
复制代码
只是cache
方法中把复杂图形变成了简单的图形。但实际效果相差甚远,使用缓存和不使用性能差距并不大,甚至不使用时 fps 值还更高一些。
因此看来图形的复杂度,直接会影响 canvas 缓存的效果,咱们在开发过程当中,也不能盲目引入缓存,要权衡利弊。fabric 中缓存是默认开启的,同时也能够设置 objectCaching
为 false 禁用。
若是你们细心的话应该会发现,当咱们执行new fabric.Canvas('domeId')
的时候,在页面上 dom 元素就改变了,fabric 复制了一层 canvas 盖在了咱们定义的 canvas 上面:
fabric 这样设计将渲染层和交互层作了分离,lower-canvas 只负责渲染元素;全部的交互,好比框选,事件处理都在 upper-canvas 上。
顺便提一下,fabric 提供了渲染静态画布的方法,若是你的画布不须要任何交互,只用来展现,那么能够用new fabric.StaticCanvas('domId', options)
来初始化,这时候 dom 结构中就只有一个 canvas,没有 upper-canvas 了。
说到这里,不少同窗可能会想到,事件是怎样绑定的呢?其实两个 canvas 大小等属性都是一致的,因此坐标也是能够对应上的,好比在 upper-canvas 上某个位置点击了一下,那么就能够去 lower-canvas 上就能够用这个坐标去找是否点击到了一个元素,那么问题来了,如何判断一个点在一个图形中呢?
这个问题网上有个比较广泛的方案,就是经过画一条射线,经过交点奇偶性来判断。以下图:
而 fabric 中并无用这种方法,缘由很简单,这个算法是有前提的:发出的射线不能与图形任何顶点相交。 这个前提对于咱们主观来判断是很简单的,但程序中处理可能就须要大量的代码去判断是否与交点相交,若是相交再从新生成一条射线。
fabric 中使用的算法对上述算法进行了改进,咱们结合下图来解释:
其中 e1 ~ e5 分别为多边形的边,P 为目标点,黑色实心点为多边形的顶点,r 为 P 延 X 轴发出的射线(不一样于上面的方法,这里咱们约定 r 射线只能延 X 轴发出)。
intersectionCount = 0
intersectionCount
加 1intersectionCount
为奇数,则在图形内,反之则在图形外。判断的部分用代码实现相似:
// point 目标点,lines多边形的全部边
function checkPoint(point, lines) {
let intersectionCount = 0;
let { x, y } = point;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
// 两个顶点
let { p1, p2 } = line;
if ((p1.y < y && p2.y < y) || (p1.y >= y && p2.y >= y)) {
continue;
} else {
const sx = ((y - p1.y) / (p2.y - p1.y)) * (p2.x - p1.x) + p1.x;
if (sx >= x) {
intersectionCount++;
}
}
}
return intersectionCount % 2 === 0;
}
复制代码
Retina 屏幕模糊的问题,直接给出处理方法,就不展开说了。
代码:
function initRetina(canvas, ctx) {
const dpi = window.devicePixelRatio;
canvas.style.width = canvas.width + "px";
canvas.style.height = canvas.height + "px";
canvas.setAttribute("width", canvas.width * dpi);
canvas.setAttribute("height", canvas.height * dpi);
ctx.scale(dpi, dpi);
}
复制代码
本篇文章主要针对fabric.canvas
模块,介绍了相关 canvas 缓存,fabric 中判断点在图形中的算法以及如何处理 retina 屏幕的知识,做为系列的第一篇文章,可能会有不少问题,若有错误及意见,欢迎批评指正。
参考文献:
idav.ucdavis.edu/~okreylos/T…
www.geog.ubc.ca/courses/kli…
www.cnblogs.com/axes/p/3567…
fabricjs.com/docs/