摘要:前言熟悉的朋友想必都使用或者听说过,算是一个元老级的库了,从第一个版本发布到现在,已经有年时间了。中缓存是默认开启的,同时也可以设置为禁用。处理屏屏幕模糊的问题,直接给出处理方法,就不展开说了。
前言
熟悉 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...
https://www.cnblogs.com/axes/...
http://fabricjs.com/docs/
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/104044.html
摘要:用于设置缩放比例,可以是任意数值的比例。禁止,不禁止是否就职浏览器识别页面中的邮件地址。具体可参考如下代码以前,我不知道或中添加有什么用,但上面的例子是它的一个用途。其他的使用可参考或模拟原生效果。更多的前端相关资源,可关注我的 meta meta中有这样几个常用属性:http-equiv,name,content,包括html5新增的charset。 注意:content属性用来存储...
摘要:今天要讲的,是我从的源码实现文件中学到的几个很基础,却又容易被忽略的知识点。在函数式编程中,函数是一等公民,它可以只是根据参数,做简单的组合操作,再作为别的函数的返回值。所以,阅读源码,是一种很棒的重温基础知识的方式。 showImg(https://segmentfault.com/img/bVbpTSY?w=750&h=422); 前言 上一篇文章 「前端面试题系列8」数组去重(1...
阅读 3536·2021-09-10 10:51
阅读 2507·2021-09-07 10:26
阅读 2481·2021-09-03 10:41
阅读 810·2019-08-30 15:56
阅读 2896·2019-08-30 14:16
阅读 3488·2019-08-30 13:53
阅读 2102·2019-08-26 13:48
阅读 1911·2019-08-26 13:37