前言熟悉canvas的朋友一定用过或者听说过Fabric.js。Fabric是一个老牌的画布库。从第一个版本发布到现在已经8年了。是时候了。我也在项目中使用了将近一年。作为用户,我想简单说说我的感受:方便,只有想不到,没有做不到的。源码写的真好,代码规范,注释清晰。源码的优缺点很清楚,但总的来说,如果你想做一个在线编辑的项目,比如在线PPT、在线绘图等应用,fabric绝对是一个不错的选择。那么这个系列文章讲的是什么呢?这里我就不主要介绍fabric的使用方法了。主要内容是总结阅读源码过程中原理相关的知识,比如图形、canvas的相关知识、fabric中的设计思路等。所以,如果你对fabric还不是很了解,建议去官网找几个demo试试。现在让我们进入本文的正题。本文主要介绍fabric.canvas涉及的一些内容。通过创建画布来创建织物非常简单:constcanvas=newfabric.Canvas("domId",options);在这么一行代码的背后,fabric主要做了以下几件事:创建一个缓存canvas,构建一个两层的canvas元素:lower-canvas和upper-canvas绑定事件处理retinascreen……我会解释相关内容下面一一介绍。canvas缓存介绍canvas缓存,fabric中的缓存类似。简单来说就是使用离屏画布进行预渲染,使用drawImage代替直接在真实画布上绘制图形。我们先来看一个例子。可以打开FPS计,切换按钮,看看FPS值和不使用缓存和使用缓存还是有很大差距的。我的电脑在使用缓存的时候基本在60fps,会掉到15fps左右。您可以打开控制台或在此处查看代码。下面列出了主要的代码片段:this.cacheCanvas=document.createElement("canvas");//离屏画布的宽高是要渲染的图形的宽高,不是真实画布的宽高,否则会渲染很多无用的区域this.cacheCanvas.width=2*(this.r+BORDER_WIDTH);this.cacheCanvas.height=2*(this.r+BORDER_WIDTH);this.cacheCtx=this.cacheCanvas.getContext("2d");这个缓存();}}paint(){//使用缓存直接创建离屏画布,否则直接绘制图形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();}}解释一下两者的区别:使用缓存:在实例化每个图形(渲染前)时,先将图形渲染到离屏画布上。渲染时,直接使用drawImage渲染离屏画布,不使用缓存:直接渲染图形,使用缓存时,需要注意一件事,就是需要控制离屏画布的大小。不能直接获取和渲染画布的实际宽高,否则会渲染出很多无用的空间。比如上例中每一个离屏画布的宽高只需要和对应图形的宽高一致即可。this.cacheCanvas.width=2*(this.r+BORDER_WIDTH);this.cacheCanvas.height=2*(this.r+BORDER_WIDTH);上面代码最省时的地方是在paint函数中使用drawImage比直接绘制Graphics省时,但是所有的场景都是这样吗?让我们再看看下面的例子。这个例子不同于上面只绘制图形的代码://从复杂图形到简单图形cache(){this.cacheCtx.save();this.cacheCtx.lineWidth=BORDER_WIDTH;这个.cacheCtx.beginPath();this.cacheCtx.strokeStyle=this.color;this.cacheCtx.arc(this.r+BORDER_WIDTH,this.r+BORDER_WIDTH,this.r,0,2*Math.PI);这。缓存Ctx.stroke();this.cacheCtx.restore();}只是在缓存方法中将复杂的图形变成了简单的图形。但实际效果却大不相同。使用缓存和不使用缓存的性能差距不大,不使用时fps值更高。所以看起来图形的复杂程度会直接影响画布缓存的效果。我们在开发过程中不能盲目引入缓存,一定要权衡利弊。fabric中的缓存默认是开启的,也可以通过设置objectCaching为false来关闭。如果你关注lower-canvas和upper-canvas,你应该会发现,当我们执行newfabric.Canvas('domeId')时,页面上的dom元素发生了变化,fabric复制了一层canvas来覆盖我们的定义canvas之上:fabric的设计将渲染层和交互层分离,下层canvas只负责渲染元素;所有交互,例如框选择和事件处理都在上层画布上。顺便说一下,fabric提供了一种渲染静态画布的方法。如果您的画布不需要任何交互,仅用于显示,您可以使用newfabric.StaticCanvas('domId',options)对其进行初始化。这时候dom结构就会只有一个canvas,没有upper-canvas。说到这里,很多同学可能会想,事件是怎么绑定的呢?其实两个canvas的size等属性是一样的,所以坐标也可以对应。比如你点击了upper-canvas的某个位置,那么你可以到lower-canvas,通过这个坐标来判断是否点击。到了一个元素,问题就来了,怎么判断一个点在一个图中呢?怎么判断图中的点是网上比较常见的解决方法,就是画一条射线,通过交点的奇偶性来判断。如下图所示:设置目标点P,从点P向任意方向画一条射线,并保证不与图形的顶点相交;记录射线与图形的交点数n;当n为奇数时,P在图中,否则在图外。但是,这种方法并没有用在织物上。原因很简单。该算法有一个前提:发射的射线不能与图的任何顶点相交。这个前提我们主观判断很简单,但是程序中的处理可能需要大量的代码来判断是否与交点相交,如果相交则重新生成一条射线。fabric中使用的算法对上述算法进行了改进,我们结合下图进行说明:其中e1~e5为多边形的边,P为目标点,黑色实心点为多边形的顶点polygon,r是信号P发出的沿X轴的射线(与上面的方法不同,这里我们约定r射线只能沿X轴发射)。设置目标点P,使P沿X轴(y=Py)画一条射线,设置intersectionCount=0遍历多边形的所有边,如果p1y
