媒介
熟习 canvas 的朋侪想必都运用或许听说过 Fabric.js,Fabric 算是一个元老级的 canvas 库了,从第一个版本宣布到如今,已经有 8 年时刻了。我近一年时刻也在项目中运用,作为用户简朴说说感觉:
- 轻易,只要想不到,没有做不到
- 源码写的真好,代码范例,诠释清楚
- 社区真匮乏,国内资本特别少
- 看文档不如看源码
优缺点都很鲜亮,但总的来讲,假如你要做一个在线编辑类的项目,比方在线 PPT,在线制图等运用,fabric 相对是个很好的挑选。
那末这一系列文章要写什么?这里不会重要引见如何运用 fabric,重要写的内容是把在浏览源码过程当中,把涉及到原理相干的学问总结出来,比方相干图形学学问、canvas 相干、fabric 中的设想头脑等的相干学问。所以,假如你如今还对 fabric 不是很相识,发起先去官网找几个 demo 试一下。
下面我们进入此次的正题,这篇文章重要引见 fabric.canvas 涉及到的部分内容。
从建立画布最先
fabric 建立画布很简朴:
const canvas = new fabric.Canvas("domId", options);
在如许一行代码背地,fabric 重要做了下面这几件事变:
- 建立缓存 canvas
- 构建两层 canvas 元素:lower-canvas 和 upper-canvas
- 绑定事宜
- 处置惩罚 retina 屏
- …
下面我把相干内容逐一论述。
canvas 缓存
引见 canvas 缓存,fabric 中的缓存也是相似的原理,简朴来讲,就是运用一个离屏 canvas 来做预衬着,在实在画布上用 drawImage 替代直接绘制图形。
我们先来看个 例子,人人能够把 FPS meter 翻开,切换按钮能够看到,不运用缓存和运用缓存 FPS 值差异照样挺大的,我电脑在运用缓存的时刻基础在 60fps,不运用会降到 15fps 摆布。人人能够翻开掌握台或许在 这里 检察代码。
下面列出重要的代码片断:
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 禁用。
lower-canvas 和 upper-canvas
假如人人仔细的话应该会发明,当我们实行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 上就能够用这个坐标去找是不是点击到了一个元素,那末题目来了,如何推断一个点在一个图形中呢?
如何推断点在图形中
这个题目网上有个比较广泛的计划,就是经由过程画一条射线,经由过程交点奇偶性来推断。如下图:
- 设目标点 P,使 P 点向恣意一个方向画一条射线,保证不与图形的极点订交;
- 纪录射线与图形的交点数目 n;
- n 为奇数时,P 就在图形内,反之则在图形外。
而 fabric 中并没有效这类要领,缘由很简朴,这个算法是有条件的:发出的射线不能与图形任何极点订交。 这个条件关于我们主观来推断是很简朴的,但顺序中处置惩罚能够就须要大批的代码去推断是不是与交点订交,假如订交再从新天生一条射线。
fabric 中运用的算法对上述算法进行了革新,我们连系下图来诠释:
个中 e1 ~ e5 分别为多边形的边,P 为目标点,黑色实心点为多边形的极点,r 为 P 延 X 轴发出的射线(差别于上面的要领,这里我们商定 r 射线只能延 X 轴发出)。
- 设目标点 P,使 P 延 X 轴方向画一条射线( y=Py ),设
intersectionCount = 0
遍历多边形的一切边,设边的极点为 p1, p2
- 假如 p1y < Py,而且 p2y < Py,跳过(也就是这条边在 P 点下方)
- 假如 p1y >= Py,而且 p2y >= Py,跳过(也就是这条边在 P 点上方)
- 不然,设射线与这条边的交点为 S,假如 Sx >= Px,
intersectionCount
加 1
- 终究假如
intersectionCount
为奇数,则在图形内,反之则在图形外。
推断的部分用代码完成相似:
// 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 屏
Retina 屏幕隐约的题目,直接给出处置惩罚要领,就不睁开说了。
- canvas.width, canvas.height 放大至 dpi 倍
- canvas.style.width, canvas.style.height 设为原始 canvas 宽高
- ctx 缩放 dpi 倍
代码:
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 屏幕的学问,作为系列的第一篇文章,能够会有许多题目,若有毛病及看法,迎接批评指正。
参考文献:
http://idav.ucdavis.edu/~okre…
http://www.geog.ubc.ca/course…