我从 fabric.js 中学到了什么

更新日期: 2019-05-03阅读: 2.8k标签: canvas

前言

熟悉 canvas 的朋友想必都使用或者听说过 Fabric.js,Fabric 算是一个元老级的 canvas 库了,从第一个版本发布到现在,已经有 8 年时间了。我近一年时间也在项目中使用,作为用户简单说说感受:

  1. 方便,只有想不到,没有做不到
  2. 源码写的真好,代码规范,注释清晰
  3. 社区真匮乏,国内资源尤其少
  4. 看文档不如看源码

优缺点都很鲜明,但总的来说,如果你要做一个在线编辑类的项目,比如在线 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 上就可以用这个坐标去找是否点击到了一个元素,那么问题来了,如何判断一个点在一个图形中呢?


如何判断点在图形中

这个问题网上有个比较普遍的方案,就是通过画一条射线,通过交点奇偶性来判断。如下图:


  1. 设目标点 P,使 P 点向任意一个方向画一条射线,保证不与图形的顶点相交;
  2. 记录射线与图形的交点数量 n;
  3. n 为奇数时,P 就在图形内,反之则在图形外。

而 fabric 中并没有用这种方法,原因很简单,这个算法是有前提的:_发出的射线不能与图形任何顶点相交。_ 这个前提对于我们主观来判断是很简单的,但程序中处理可能就需要大量的代码去判断是否与交点相交,如果相交再重新生成一条射线。

fabric 中使用的算法对上述算法进行了改进,我们结合下图来解释:


其中 e1 ~ e5 分别为多边形的边,P 为目标点,黑色实心点为多边形的顶点,r 为 P 延 X 轴发出的射线(不同于上面的方法,这里我们约定 r 射线只能延 X 轴发出)。

  1. 设目标点 P,使 P 延 X 轴方向画一条射线( y=Py ),设 intersectionCount = 0
  2. 遍历多边形的所有边,设边的顶点为 p1, p2

    1. 如果 p1y < Py,而且 p2y < Py,跳过(也就是这条边在 P 点下方)
    2. 如果 p1y >= Py,而且 p2y >= Py,跳过(也就是这条边在 P 点上方)
    3. 否则,设射线与这条边的交点为 S,如果 Sx >= Px,intersectionCount加 1
  3. 最终如果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 屏幕模糊的问题,直接给出处理方法,就不展开说了。

  1. canvas.width, canvas.height 放大至 dpi 倍
  2. canvas.style.width, canvas.style.height 设为原始 canvas 宽高
  3. 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://segmentfault.com/a/1190000019054853


链接: https://fly63.com/article/detial/3177

HTML5 Canvas绘图基本使用方法, H5使用Canvas绘图

Canvas 是H5的一部分,允许脚本语言动态渲染图像。Canvas 定义一个区域,可以由html属性定义该区域的宽高,javascript代码可以访问该区域,通过一整套完整的绘图功能(API),在网页上渲染动态效果图。

web图片前端裁剪功能实现_利用html5 canvas技术实现图片裁剪

上传截图很多做法是把图像发送到后端,把裁剪后的结果发送给浏览器,这种方式会增加处理时延。用canvas提供的API实现纯前端的剪切:这里头关键有三步:显示未经处理的图片,得到裁剪区域,显示裁剪后的区域。

原生js使用canvas实现图片格式webp/png/jpeg在线转换

javascript完成图片格式转换: 通过input上传图片,使用FileReader将文件读取到内存中。将图片转换为canvas,canvas.toDataURL()方法设置为我们需要的格式,最后将canvas转换为图片。

离屏Canvas — 使用Web Worker提高你的Canvas运行速度

现在因为有了离屏Canvas,你可以不用在你的主线程中绘制图像了!Canvas 是一个非常受欢迎的表现方式,同时也是WebGL的入口。它能绘制图形,图片,展示动画,甚至是处理视频内容

canvas高效绘制10万图形,你必须知道的高效绘制技巧

最近的一个客户项目中,简化的需求是绘制按照行列绘制很多个圆圈。需求看起来不难,上手就可以做,写两个for循环。,IT行业的知识更新越来越快,能够以不变应万变的人,就是拥有良好的学习力、创造力、判断力和思考力的人。这些能力会让你在变换万千的技术海洋中,屹立不倒,不被淹没。

利用canvas将网页元素生成图片并保存在本地

利用canvas将网页元素生成图片并保存在本地,首先引入三个文件,createElementNS() 方法可创建带有指定命名空间的元素节点。 createElementNS(ns,name) > createElementNS() 方法与 createElement() 方法相似

基于 HTML5 Canvas 的智能安防 SCADA 巡逻模块

随着大数据时代的来临,物联网的日益发展,原先的 SCADA 系统本身也在求新求变,从最开始的专业计算机和操作系统,到通用计算机和相关软件,再到现在基于 HTML5 Canvas 的新型组态开发,其应用的范围也从最初的电力

js用canvas实现简单的粒子运动

在写下合格粒子运动时要先清楚你的思路,不能一开始就盲目的开始写,首先先要确定思路然后在去一步步的实现,在写的过程要注意细节,要思考js有些知识是跟数学知识相关的要注意观察

Canvas 点线动画案例

canvas 画的圆不是圆,是椭圆。不要在style里指定 Canvas 的宽度,Canvas 画布的尺寸的大小和显示的大小是有很大的区别的,在 canvas 里面设置的是才是 Canvas 本身的大小。不要企图通过闭合现有路径来开始一条新路径

Canvas在移动端绘制模糊的原因与解决办法

由于一些移动端的兼容性原因,我们某个项目需要前端将pdf转换成在移动端页面可直接观看的界面。为了方便解决,我们采用了pdf.js这个插件,该插件可以将pdf转换成canvas绘制在页面上

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!