Fabric.js 简介
我们先来看看官方的定义:
Fabric.js is a framework that makes it easy to work with HTML5 canvas element. It is an interactive object model on top of canvas element. It is also an SVG-to-canvas parser.
Fabric.js 是一个可以让 HTML5 Canvas 开发变得简单的框架。它是一种基于 Canvas 元素的可交互对象模型,也是一个 SVG 到 Canvas 的解析器(让SVG 渲染到 Canvas 上)。
Fabric.js 的代码不算多,源代码(不包括内置的三方依赖)大概 1.7 万行。最初是在 2010 年开发的,从源代码就可以看出来,都是很老的代码写法。没有构建工具,没有依赖,甚至没使用 ES6,代码中模块都是用 IIFE 的方式包装的。
但是这个并不影响我们学习它,相反正因为它没引入太多的概念,使用起来相当方便。不需要构建工具,直接在一个 HTML 文件中引入库文件就可以开发了,甚至官方都提供了一个 HTML 模板代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://rawgit.com/fabricjs/fabric.js/master/dist/fabric.js"></script>
</head>
<body>
<canvas id="c" width="300" height="300" style="border:1px solid #ccc"></canvas>
<script>
(function() {
var canvas = new fabric.Canvas('c');
})();
</script>
</body>
</html>
这就够了,不是吗?
使用场景
从它的官方定义可以看出来,它是一个用 Canvas 实现的对象模型。如果你需要用 HTML Canvas 来绘制一些东西,并且这些东西可以响应用户的交互,比如:拖动、变形、旋转等操作。那用 fabric.js 是非常合适的,因为它内部不仅实现了 Canvas 对象模型,还将一些常用的交互操作封装好了,可以说是开箱即用。
内部集成的主要功能如下:
Canvas 开发原理
如果你之前没有过 Canvas 的相关开发经验(只有 Javascript 网页开发经验),刚开始入门会觉得不好懂,不理解 Canvas 开发的逻辑。这个很正常,因为这表示你正在从传统的 Javascript 开发转到图形图像 GUI 图形图像、动画开发。虽然语言都是 Javascript 但是开发理念和用到的编程范式完全不同。
这两种开发方式各有各的优势,比如:
有的功能在 HTML 里一行代码就能实现的功能放到 Canvas 中需要成千行的代码去实现。比如:textarea, contenteditable
相反,有的功能在 Canvas 里面只需要一行代码实现的,使用 HTML 却几乎无法实现。比如:截图、录制
Canvas 开发的本质其实很简单,想像下面这种少儿画板:
Canvas 的渲染过程就是不断的在画板(Canvas)上面擦了画,画了擦。
动画就更简单了,只要渲染帧率超过人眼能识别的帧率(60fps)即可:
<canvas id="canvas" width="500" height="500" style="border:1px solid black"></canvas>
<script>
var canvas = document.getElementById("canvas")
var ctx = canvas.getContext('2d');
var left = 0
setInterval(function() {
ctx.clearRect(0, 0, 500, 500);
ctx.fillRect(left++, 100, 100, 100);
}, 1000 / 60)
</script>
当然你也可以用requestAnimationFrame
,不过这不是我想说明的重点。
Fabric.js 源码解析
模块结构图
fabric.js 的模块我大概画了个图,方便理解。
基本原理
fabric.js 在初始化的时候会将你指定的 Canvas 元素(叫做 lowerCanvas)外面包裹上一层 div 元素,然后内部会插入另外一个上层的 Canvas 元素(叫做 upperCanvas),这两个 Canvas 有如下区别:
内部叫法 | 文件路径 | 作用 |
---|
upperCanvas | src/canvas.class.js | 上层画布,只处理 分组选择,事件绑定 |
lowerCanvas | src/static_canvas.class.js | 真正 绘制 元素对象(Object)的画布 |
核心模块详解
上图中,灰色的模块对于理解 fabric.js 核心工作原理没多大作用,可以不看。其它核心模块我按自己的理解来解释一下。
所有模块都被挂载到一个 fabric 的命名空间上面,都可以用fabric.XXX
的形式访问。
fabric.util
工具包
工具包中一个最重要的方法是createClass
,它可以用来创建一个类。我们来看看这个方法:
function createClass() {
var parent = null,
properties = slice.call(arguments, 0);
if (typeof properties[0] === 'function') {
parent = properties.shift();
}
function klass() {
this.initialize.apply(this, arguments);
}
// 关联父子类之间的关系
klass.superclass = parent;
klass.subclasses = [];
if (parent) {
Subclass.prototype = parent.prototype;
klass.prototype = new Subclass();
parent.subclasses.push(klass);
}
// ...
}
为什么不用 ES 6 的类写法呢?主要是因为这个库写的时候 ES 6 还没出来。作者沿用了老式的基于Javascript prototype 实现的类继承的写法,这个方法封装了类的继承、构造方法、父子类之前的关系等功能。注意klass.superclass
和klass.subclasses
这两行,后面会讲到。
添加这两个引用关系后,我们就可以在 JS 运行时动态获取类之间的关系,方便后续序列化及反序列化操作,这种做法类似于其它编程语言中的反射机制,可以让你在代码运行的时候动态的构建、操作对象
initialize()
方法(构造函数)会在类被 new 出来的时候自动调用:
function klass()
{
this.initialize.apply(this, arguments);
}
fabric 通用类
fabric.Canvas
类
上层画布类,如上面表格所述,它并不渲染对象。它只来处理与用户交互的逻辑。 比如:全局事件绑定、快捷键、鼠标样式、处理多(分组)选择逻辑。
我们来看看这个类初始化时具体干了些什么。
fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, {
initialize: function (el, options) {
options || (options = {});
this.renderAndResetBound = this.renderAndReset.bind(this);
this.requestRenderAllBound = this.requestRenderAll.bind(this);
this._initStatic(el, options);
this._initInteractive();
this._createCacheCanvas();
},
// ...
})
注意:由于createClass
中第一个参数是StaticCanvas
,所以我们可以知道 Canvas 的父类是StaticCanvas
。
从构造方法initialize
中我们可以看出:
只有_initInteractive
和_createCacheCanvas
是 Canvas 类自己的方法,renderAndResetBound
,requestRenderAllBound
,_initStatic
都继承自父类StaticCanvas
这个类的使用也很简单,做为 fabric.js 程序的入口,我们只需要 new 出来即可:
// c 就是 HTML 中的 canvas 元素 id
const canvas = new fabric.Canvas("c", { /* 属性 */ })
fabric.StaticCanvas
类
fabric 的核心类,控制着 Canvas 的渲染操作,所有的画布对象都必须在它上面绘制出来。我们从构造函数中开始看:
fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, {
initialize: function (el, options) {
options || (options = {});
this.renderAndResetBound = this.renderAndReset.bind(this);
this.requestRenderAllBound = this.requestRenderAll.bind(this);
this._initStatic(el, options);
},
})
注意:StaticCanvas 不仅继承了fabric.CommonMethods
中的所有方法,还继承了fabric.Observable
和fabric.Collection
,而且它的实现方式很 Javascript,在 StaticCanvas.js 最下面一段:
extend(fabric.StaticCanvas.prototype, fabric.Observable);
extend(fabric.StaticCanvas.prototype, fabric.Collection);
fabric.js 的画布渲染原理
requestRenderAll()
方法
从下面的代码可以看出来,这个方法的主要任务就是不断调用renderAndResetBound
方法,renderAndReset
方法会最终调用renderCanvas
来实现绘制。
requestRenderAll: function () {
if (!this.isRendering) {
this.isRendering = fabric.util.requestAnimFrame(this.renderAndResetBound);
}
return this;
}
renderCanvas()
方法
renderCanvas 方法中代码比较多:
renderCanvas: function(ctx, objects) {
var v = this.viewportTransform, path = this.clipPath;
this.cancelRequestedRender();
this.calcViewportBoundaries();
this.clearContext(ctx);
fabric.util.setImageSmoothing(ctx, this.imageSmoothingEnabled);
this.fire('before:render', {ctx: ctx,});
this._renderBackground(ctx);
ctx.save();
//apply viewport transform once for all rendering process
ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
this._renderObjects(ctx, objects);
ctx.restore();
if (!this.controlsAboveOverlay && this.interactive) {
this.drawControls(ctx);
}
if (path) {
path.canvas = this;
// needed to setup a couple of variables
path.shouldCache();
path._transformDone = true;
path.renderCache({forClipping: true});
this.drawClipPathOnCanvas(ctx);
}
this._renderOverlay(ctx);
if (this.controlsAboveOverlay && this.interactive) {
this.drawControls(ctx);
}
this.fire('after:render', {ctx: ctx,});
}
我们删掉一些不重要的,精简一下,其实最主要的代码就两行:
renderCanvas: function(ctx, objects) {
this.clearContext(ctx);
this._renderObjects(ctx, objects);
}
clearContext 里面会调用 canvas 上下文的clearRect
方法来清空画布:
ctx.clearRect(0, 0, this.width, this.height)
_renderObjects
就是遍历所有的objects
调用它们的render()
方法,把自己绘制到画布上去:
for (i = 0, len = objects.length; i < len; ++i) {
objects[i] && objects[i].render(ctx);
}
现在你是不是明白了文章最开始那段setInterval
实现的 Canvas 动画原理了?
fabric 形状类
fabric.Object
对象根类型
虽然我们已经明白了 canvas 的绘制原理,但是一个对象(2d元素)到底是怎么绘制到 canvas 上去的,它们的移动怎么实现的?具体细节我们还不是很清楚,这就要从fabric.Object
根类型看起了。
由于 fabric 中的 2d 元素都是以面向对象的形式实现的,所以我画了一张内部类之间的继承关系,可以清楚的看出它们之间的层次结构:
不像传统的 UML 类图那样,这个图看起来还稍有点乱,因为 fabric.js 内部实现的是多重继承,或者说类似于 mixin 的一种混入模式实现的继承。
从图中我们可以得出以下几点:
底层 StaticCanvas 继承了Collection
对象和Observable
对象,这就意味着 StaticCanvas 有两种能力:
所有的 2d 形状(如:矩形、圆、线条、文本)都继承了Object
类。Object 有的属性、方法,所有的 2d 形状都会有
所有的 2d 形状都具有自定义事件发布/订阅的能力
Object 类常用属性
下面的注释中,边角控制器是 fabric.js 内部集成的用户与对象交互的一个手柄,当某个对象处于激活状态的时候,手柄会展示出来。如下图所示:
常用属性解释:
// 对象的类型(矩形,圆,路径等),此属性被设计为只读,不能被修改。修改后 fabric 的一些部分将不能正常使用。
type: 'object',
// 对象变形的水平中心点的位置(左,右,中间)
// 查看 http://jsfiddle.net/1ow02gea/244/ originX/originY 在分组中的使用案例
originX: 'left',
// 对象变形的垂直中心点的位置(上,下,中间)
// 查看 http://jsfiddle.net/1ow02gea/244/ originX/originY 在分组中的使用案例
originY: 'top',
// 对象的顶部位置,默认**相对于**对象的上边沿,你可以通过设置 originY={top/center/bottom} 改变它的参数参考位置
top: 0,
// 对象的左侧位置,默认**相对于**对象的左边沿,你可以通过设置 originX={top/center/bottom} 改变它的参数参考位置
left: 0,
// 对象的宽度
width: 0,
// 对象的高度
height: 0,
// 对象水平缩放比例(倍数:1.5)
scaleX: 1,
// 对象水平缩放比例(倍数:1.5)
scaleY: 1,
// 是否水平翻转渲染
flipX: false,
// 是否垂直翻转渲染
flipY: false,
// 透明度
opacity: 1,
// 对象旋转角度(度数)
angle: 0,
// 对象水平倾斜角度(度数)
skewX: 0,
// 对象垂直倾斜角度(度数)
skewY: 0,
// 对象的边角控制器大小(像素)
cornerSize: 13,
// 当检测到 touch 交互时对象的边角控制器大小
touchCornerSize: 24,
// 对象边角控制器是否透明(不填充颜色),默认只保留边框、线条
transparentCorners: true,
// 鼠标 hover 到对象上时鼠标形状
hoverCursor: null,
// 鼠标拖动对象时鼠标形状
moveCursor: null,
// 对象本身与边角控制器之间的间距(像素)
padding: 0,
// 对象处于活动状态下边角控制器**包裹对象的边框**颜色
borderColor: 'rgb(178,204,255)',
// 指定边角控制器**包裹对象的边框**虚线边框的模式元组(hasBorder 必须为 true)
// 第一个元素为实线,第二个为空白
borderDashArray: null,
// 对象处于活动状态下边角控制器颜色
cornerColor: 'rgb(178,204,255)',
// 对象处于活动状态且 transparentCorners 为 false 时边角控制器本身的边框颜色
cornerStrokeColor: null,
// 边角控制器的样式,正方形或圆形
cornerStyle: 'rect',
// 指定边角控制器本身的虚线边框的模式元组(hasBorder 必须为 true)
// 第一个元素为实线,第二个为空白
cornerDashArray: null,
// 如果为真,通过边角控制器来对对象进行缩放会以对象本身的中心点为准
centeredScaling: false,
// 如果为真,通过边角控制器来对对象进行旋转会以对象本身的中心点为准
centeredRotation: true,
// 对象的填充颜色
fill: 'rgb(0,0,0)',
// 填充颜色的规则:nonzero 或者 evenodd
// @see https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/fill-rule
fillRule: 'nonzero',
// 对象的背景颜色
backgroundColor: '',
// 可选择区域被选择时(对象边角控制器区域),层级低于对象背景颜色
selectionBackgroundColor: '',
// 设置后,对象将以笔触的方式绘制,此属性值即为笔触的颜色
stroke: null,
// 笔触的大小
strokeWidth: 1,
// 指定笔触虚线的模式元组(hasBorder 必须为 true)
// 第一个元素为实线,第二个为空白
strokeDashArray: null,
Object 类常用方法
drawObject()
对象的绘制方法
drawObject()
方法内部会调用_render()
方法,但是在fabric.Object
基类中它是个空方法。这意味着对象具体的绘制方法需要子类去实现。即子类需要重写父类的空_render()
方法。
_onObjectAdded()
对象被添加到 Canvas 事件
这个方法非常重要,只要当一个对象被添加到 Canvas 中的时候,对象才可以具有 Canvas 的引用上下文,对象的一些常用方法才能起作用。比如:Object.center()
方法,调用它可以让一个对象居中到画布中央。下面这段代码可以实现这个功能:
const canvas = new fabric.Canvas("canvas", {
width: 500, height: 500,
})
const box = new fabric.Rect({
left: 10, top: 10,
width: 100, height: 100,
})
console.log(box.top, box.left) // => 10, 10
box.center()
console.log(box.top, box.left) // => 10, 10
canvas.add(box)
但是你会发现 box 并没有被居中,这就是因为:当一个对象(box)还没被添加到 Canvas 中的时候,对象上面还不具有 Canvas 的上下文,所以调用的对象并不知道应该在哪个 Canvas 上绘制。我们可以看下center()
方法的源代码:
center: function () {
this.canvas && this.canvas.centerObject(this);
return this;
},
正如上面所说,没有 canvas 的时候是不会调用到canvas.centerObject()
方法,也就实现不了居中。
所以解决方法也很简单,调换下 center() 和 add() 方法的先后顺序就好了:
const canvas = new fabric.Canvas("canvas", {
width: 500, height: 500,
})
const box = new fabric.Rect({
left: 10, top: 10,
width: 100, height: 100,
})
canvas.add(box)
console.log(box.top, box.left) // => 10, 10
box.center()
console.log(box.top, box.left) // => 199.5, 199.5
「为什么不是 200,而是 199.5」—— 好问题,但是我不准备讲这个。有兴趣可以自己研究 下。
toObject()
对象的序列化
正向的把对象序列化是很简单的,只需要把你关注的对象上的属性拼成一个 JSON 返回即可 :
toObject: function(propertiesToInclude) {
var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS,
object = {
type: this.type,
version: fabric.version,
originX: this.originX,
originY: this.originY,
left: toFixed(this.left, NUM_FRACTION_DIGITS),
top: toFixed(this.top, NUM_FRACTION_DIGITS),
width: toFixed(this.width, NUM_FRACTION_DIGITS),
height: toFixed(this.height, NUM_FRACTION_DIGITS),
// 省略其它属性
};
return object;
},
当调用对象的toJSON()
方法时会使用JSON.stringify(toObject())
来将对象的属性转换成 JSON 字符串
fromObject()
对象的反序列化
fromObject()
是 Object 的子类需要实现的反序列化方法,通常会调用 Object 类的默认方法_fromObject()
fabric.Object._fromObject = function(className, object, callback, extraParam) {
var klass = fabric[className];
object = clone(object, true);
fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) {
if (typeof patterns[0] !== 'undefined') {
object.fill = patterns[0];
}
if (typeof patterns[1] !== 'undefined') {
object.stroke = patterns[1];
}
fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) {
object.clipPath = enlivedProps[0];
var instance = extraParam ? new klass(object[extraParam], object) : new klass(object);
callback && callback(instance);
});
});
};
这段代码做了下面一些事情:
通过类名(className 在Object
的子类fromObject
中指定)找到挂载在fabric
命名空间上的对象的所属类
深拷贝当前对象,避免操作过程对修改源对象
处理、修正对象的一些特殊属性,比如:fill, stroke, clipPath 等
用所属类按新的对象属性构建一个新的对象实例(instance),返回给回调函数
噫,好像不对劲?反序列化入参不得是个 JSON 字符串吗。是的,不过 fabric.js 中并没有在 Object 类中提供这个方法,这个自己实现也很简单,将目标 JSON 字符串 parse 成 普通的 JSON 对象传入即可。
Canvas 类上面到是有一个画布整体反序列化的方法:loadfromJSON()
,它做的事情就是把一段静态的 JSON 字符串转成普通对象后传给每个具体的对象,调用对象上面的fromObject()
方法,让对象具有真正的渲染方法,再回绘到 Canvas 上面。
序列化主要用于 持久存储
,反序列化则主要用于将持久存储的静态内容转换为 Canvas 中可操作的 2d 元素,从而可以实现将某个时刻画布上的状态还原的目的
如果你的存储够用的话,甚至可以将整个在 Canvas 上的绘制过程进行录制/回放
一些绘制过程中常见的功能也是通过序列化/反序列化来实现的,比如:撤销/重做
fabric 混入类
混入类(mixin)通常用来给对象添加额外的方法,通常这些方法和画布关系不大,比如:一些无参方法,事件绑定等。通常混入类会通过调用fabric.util.object.extend()
方法来给对象的 prototype 上添加额外的方法。
fabric.js 的事件绑定
混入类里面有一个很重要的文件:canvas_event.mixin.js
,它的作用有以下几种:
为上层 Canvas 绑定原生浏览器事件
在合适的时机触发自定义事件
使用第三方库(event.js)绑定、模拟移动端手势操作事件
fabric.js 的鼠标移动(__onMouseMove())事件
__onMouseMove()
可以说是一个核心事件,对象的变换基本上都要靠它来计算距离才能实 现,我们来看看它是如何实现的
__onMouseMove: function (e) {
this._handleEvent(e, 'move:before');
this._cacheTransformEventData(e);
var target, pointer;
if (this.isDrawingMode) {
this._onMouseMoveInDrawingMode(e);
return;
}
if (!this._isMainEvent(e)) {
return;
}
var groupselector = this._groupselector;
// We initially clicked in an empty area, so we draw a box for multiple selection
if (groupselector) {
pointer = this._pointer;
groupselector.left = pointer.x - groupselector.ex;
groupselector.top = pointer.y - groupselector.ey;
this.renderTop();
}
else if (!this._currentTransform) {
target = this.findTarget(e) || null;
this._setCursorfromEvent(e, target);
this._fireOverOutEvents(target, e);
}
else {
this._transformObject(e);
}
this._handleEvent(e, 'move');
this._resetTransformEventData();
},
注意看源码的时候要把握到重点,一点不重要的就先忽略,比如:缓存处理、状态标识。我们只看最核心的部分,上面这段代码里面显然_transformObject()
才是一个核心方法,我们深入学习下。
/**
* 对对象进行转换(变形、旋转、拖动)动作,e 为当前鼠标的 mousemove 事件,
* **transform** 表示要进行转换的对象(mousedown 时确定的)在 `_setupCurrentTransform()` 中封装过,
* 可以理解为对象 **之前** 的状态,再调用 transform 对象中对应的 actionHandler
* 来操作画布中的对象,`_performTransformAction()` 可以对 action 进行检测,如果对象真正发生了变化
* 才会触发最终的渲染方法 requestRenderAll()
* @private
* @param {Event} e 鼠标的 mousemove 事件
*/
_transformObject: function(e) {
var pointer = this.getPointer(e),
transform = this._currentTransform;
transform.reset = false;
transform.shiftKey = e.shiftKey;
transform.altKey = e[this.centeredKey];
this._performTransformAction(e, transform, pointer);
transform.actionPerformed && this.requestRenderAll();
},
我已经把注释添加上了,主要的代码实现其实是在_performTransformAction()
中实现的。
_performTransformAction: function(e, transform, pointer) {
var x = pointer.x,
y = pointer.y,
action = transform.action,
actionPerformed = false,
actionHandler = transform.actionHandler;
// actionHandle 是被封装在 controls.action.js 中的处理器
if (actionHandler) {
actionPerformed = actionHandler(e, transform, x, y);
}
if (action === 'drag' && actionPerformed) {
transform.target.isMoving = true;
this.setCursor(transform.target.moveCursor || this.moveCursor);
}
transform.actionPerformed = transform.actionPerformed || actionPerformed;
},
这里的transform对象是设计得比较精妙的地方,它封装了对象操作的几种不同的类型,每种类型对应的有不同的动作处理器(actionHandler),transform 对象就充当了一 种对于2d元素进行操作的上下文,这样设计可以得得事件绑定和处理逻辑分离,代码具有更高的内聚性。
我们再看看上面注释中提到的_setupCurrentTransform()
方法,一次 transform 开始与结束正好对应着鼠标的按下(onMouseDown)与松开(onMouseUp)两个事件。
我们可以从onMouseDown()
事件中顺藤摸瓜,找到构造 transform 对象的地方:
_setupCurrentTransform: function (e, target, alreadyselected) {
var pointer = this.getPointer(e), corner = target.__corner,
control = target.controls[corner],
actionHandler = (alreadyselected && corner)
? control.getActionHandler(e, target, control)
: fabric.controlsUtils.dragHandler,
transform = {
target: target,
action: action,
actionHandler: actionHandler,
corner: corner,
scaleX: target.scaleX,
scaleY: target.scaleY,
skewX: target.skewX,
skewY: target.skewY,
};
// transform 上下文对象被构造的地方
this._currentTransform = transform;
this._beforeTransform(e);
},
control.getActionHandler
是动态从default_controls.js
中按边角的类型获取的:
边角类型 | 控制位置 | 动作处理器(actionHandler) | 作用 |
---|
ml | 左中 | scalingXOrSkewingY | 横向缩放或者纵向扭曲 |
mr | 右中 | scalingXOrSkewingY | 横向缩放或者纵向扭曲 |
mb | 下中 | scalingYOrSkewingX | 纵向缩放或者横向扭曲 |
mt | 上中 | scalingYOrSkewingX | 纵向缩放或者横向扭曲 |
tl | 左上 | scalingEqually | 等比缩放 |
tr | 右上 | scalingEqually | 等比缩放 |
bl | 左下 | scalingEqually | 等比缩放 |
br | 右下 | scalingEqually | 等比缩放 |
mtr | 中上变形 | controlsUtils.rotationWithSnapping | 旋转 |
对照上面的边角控制器图片更好理解。
这里我想多说一点,一般来讲,像这种上层的交互功能,做为一个 Canvas 库通常是不会封装好的。 但是 fabric.js 却帮我们做好了,这也验证了它自己定义里面的一个关键词:** 可交互的**,正是因为它通过边角控制器封装了可见的对象操作,才使得 Canvas 对象可以与用户进行交互。我们普通开发者不需要关心细节,配置一些通用参数就能实现功能。
fabric.js 的自定义事件
fabric.js 中内置了很多自定义事件,这些事件都是我们常用的,非原子事件。对于日常开发来说非常方便。
对象上的 24 种事件
object:added
object:removed
object:selected
object:deselected
object:modified
object:modified
object:moved
object:scaled
object:rotated
object:skewed
object:rotating
object:scaling
object:moving
object:skewing
object:mousedown
object:mouseup
object:mouseover
object:mouseout
object:mousewheel
object:mousedblclick
object:dragover
object:dragenter
object:dragleave
object:drop
画布上的 5 种事件
before:render
after:render
canvas:cleared
object:added
object:removed
明白了上面这几个核心模块的工作原理,再使用 fabric.js 来进行 Canvas 开发就能很快入门, 实际上 Canvas 开发并不难,难的是编程思想和方式的转变。
几个需要注意的地方
fabric.js 源码没有使用 ES 6,没使用 Typescript,所以在看代码的时候还是很不方便的,推荐使用 jetbrains 家的 IDE:IntelliJ IDEA 或 Webstorm 都是支持对 ES 6 以下的 Javascript 代码进行 静态分析的,可以使用跳转到定义、调用层级等功能,看源代码会很方便。
fabric.js 源码中很多地方用到 Canvas 的 save() 和 restore() 方法,可以查看这个链接了解更多:查看。
如果你之前从来没有接触过 Canvas 开发,那我建议去看看 bilibili 上萧井陌录的一节的关于入门游戏开发的视频教程,不要一开始就去学习 Canvas 的 API,先了解概念原理性的东西,最后再追求细节。
该文章在 2023/5/23 11:56:36 编辑过