fabric画图插件源代码重点难点分析

发布时间 2023-07-05 09:11:10作者: pzhu1

网上有些文章分析fabric的功能框架和使用方法,没有分析fabric重点底层源代码的,本文分析fabric底层源代码重点难点。

fabric拖拽处理流程分析:

绑定鼠标事件:
addOrRemove: function(functor, eventjsFunctor) { // functor=addListener/removeListener, eventjsFunctor='add'/'remove'
var canvasElement = this.upperCanvasEl,
eventTypePrefix = this._getEventPrefix();
functor(fabric.window, 'resize', this._onResize);
functor(canvasElement, eventTypePrefix + 'down', this._onMouseDown); // mousedown时绑定mousemove/mouseup
functor(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); // mouse hover,可以注释掉
functor(canvasElement, eventTypePrefix + 'out', this._onMouseOut);
functor(canvasElement, eventTypePrefix + 'enter', this._onMouseEnter);
functor(canvasElement, 'wheel', this._onMouseWheel);
functor(canvasElement, 'contextmenu', this._onContextMenu);
functor(canvasElement, 'dblclick', this._onDoubleClick);
functor(canvasElement, 'dragover', this._onDragOver);
functor(canvasElement, 'dragenter', this._onDragEnter);
functor(canvasElement, 'dragleave', this._onDragLeave);
functor(canvasElement, 'drop', this._onDrop);
if (!this.enablePointerEvents) {
functor(canvasElement, 'touchstart', this._onTouchStart, addEventOptions);
}
if (typeof eventjs !== 'undefined' && eventjsFunctor in eventjs) {
eventjs[eventjsFunctor](canvasElement, 'gesture', this._onGesture);
eventjs[eventjsFunctor](canvasElement, 'drag', this._onDrag);
eventjs[eventjsFunctor](canvasElement, 'orientation', this._onOrientationChange);
eventjs[eventjsFunctor](canvasElement, 'shake', this._onShake);
eventjs[eventjsFunctor](canvasElement, 'longpress', this._onLongPress);
}
},

_bindEvents: function() {
if (this.eventsBound) {
// for any reason we pass here twice we do not want to bind events twice.
return;
}
this._onMouseDown = this._onMouseDown.bind(this);
this._onTouchStart = this._onTouchStart.bind(this);
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseUp = this._onMouseUp.bind(this);
this._onTouchEnd = this._onTouchEnd.bind(this);
this._onResize = this._onResize.bind(this);
this._onGesture = this._onGesture.bind(this);
this._onDrag = this._onDrag.bind(this);
this._onShake = this._onShake.bind(this);
this._onLongPress = this._onLongPress.bind(this);
this._onOrientationChange = this._onOrientationChange.bind(this);
this._onMouseWheel = this._onMouseWheel.bind(this);
this._onMouseOut = this._onMouseOut.bind(this);
this._onMouseEnter = this._onMouseEnter.bind(this);
this._onContextMenu = this._onContextMenu.bind(this);
this._onDoubleClick = this._onDoubleClick.bind(this);
this._onDragOver = this._onDragOver.bind(this);
this._onDragEnter = this._simpleEventHandler.bind(this, 'dragenter');
this._onDragLeave = this._simpleEventHandler.bind(this, 'dragleave');
this._onDrop = this._simpleEventHandler.bind(this, 'drop');
this.eventsBound = true;
},

mouswDown事件handler:
_onMouseDown: function (e) {
this.__onMouseDown(e);
this._resetTransformEventData();
var canvasElement = this.upperCanvasEl,
eventTypePrefix = this._getEventPrefix();
removeListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions);
addListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp);
addListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions);
},

mouseMove事件handler:
_onMouseMove: function (e) { // hover和拖拽触发,可以取消hover触发
//console.log(e);
//return;
!this.allowTouchScrolling && e.preventDefault && e.preventDefault();
this.__onMouseMove(e); // 这是拖拽效果关键代码
},

__onMouseMove: function (e) { // hover和拖拽都执行,拖拽效果关键代码
console.log('onmousemove');
this._handleEvent(e, 'move:before'); // 对拖拽效果无影响
this._cacheTransformEventData(e); // find target,对拖拽效果无影响
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 {
console.log('_transformObject') // 拖拽时执行
this._transformObject(e); // 根据鼠标位置更新object属性
}
this._handleEvent(e, 'move'); // 对拖拽效果无影响
this._resetTransformEventData(); // 对拖拽效果无影响
},

_transformObject: function(e) { // 鼠标事件对象是canvas,并不是某个object,findTarget能构造transform.target
var pointer = this.getPointer(e),
transform = this._currentTransform; // pointer是鼠标坐标,_currentTransform是transform数据对象
transform.reset = false;
transform.target.isMoving = true;
transform.shiftKey = e.shiftKey;
transform.altKey = e[this.centeredKey];

this._performTransformAction(e, transform, pointer); // 按鼠标坐标计算object偏移,结果记录在transform(引用this._currentTransform)
transform.actionPerformed && this.requestRenderAll(); // 如果需要偏移则重新画图
},


_performTransformAction: function(e, transform, pointer) { // 拖拽时执行
var x = pointer.x,
y = pointer.y,
action = transform.action,
actionPerformed = false,
actionHandler = transform.actionHandler, // = false
// this object could be created from the function in the control handlers
options = {
target: transform.target,
e: e,
transform: transform,
pointer: pointer
};
if (action === 'drag') {
actionPerformed = this._translateObject(x, y); // 设置target的left/top跟随鼠标位置,如果需要偏移(target位置与鼠标位置不符)则返回true
if (actionPerformed) {
this._fire('moving', options); // 这是执行应用绑定mouse:move的handler,比如canvas.on('mouse:move', handler)
this.setCursor(options.target.moveCursor || this.moveCursor);
}
}
else if (actionHandler) { // 拖拽时为false
(actionPerformed = actionHandler(e, transform, x, y)) && this._fire(action, options);
}
transform.actionPerformed = transform.actionPerformed || actionPerformed;
},


_translateObject: function (x, y) {
var transform = this._currentTransform,
target = transform.target,
newLeft = x - transform.offsetX, // offset是鼠标在object或组选框里面相对于左上角的偏移量,鼠标-鼠标在object内部offset就是object新位置,拖拽过程中offset是固定值
newTop = y - transform.offsetY,
moveX = !target.get('lockMovementX') && target.left !== newLeft, // 有拖拽偏移
moveY = !target.get('lockMovementY') && target.top !== newTop;
moveX && target.set('left', newLeft); // 如果有拖拽偏移,是在这儿修改object的坐标
moveY && target.set('top', newTop);
return moveX || moveY; // true-有拖拽偏移
},

 

requestRenderAll: function () { // 重新画图
if (!this.isRendering) {
this.isRendering = fabric.util.requestAnimFrame(this.renderAndResetBound);
}
return this;
},

fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ {
initialize: function(el, options) {
this.renderAndResetBound = this.renderAndReset.bind(this);
}
}


renderAndReset: function() {
this.isRendering = 0;
this.renderAll(); // 有两个renderAll(),差不多,都是要执行renderCanvas(),实际是执行代码多一点的那个renderAll()
},


renderAll: function () { // mousedown和拖拽时触发
if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) {
this.clearContext(this.contextTop);
this.contextTopDirty = false;
}
if (this.hasLostContext) {
this.renderTopLayer(this.contextTop);
}
var canvasToDrawOn = this.contextContainer;
this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); // 遍历所有object
return this;
},


renderCanvas: function(ctx, objects) { // 拖拽时反复执行
var v = this.viewportTransform, path = this.clipPath; // canvas的transform如果没有修改过默认[1,0,0,1,0,0],如果按鼠标位置改过那是要变换canvas比如拖拽canvas
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]); // canvas变换一次,用viewportTransform值,这个值正常是默认值不会变,只有写代码变换整个canvas时才会变,比如拖拽canvas。
this._renderObjects(ctx, objects); // 重画每个object时会单独变换canvas
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, });
},

_renderObjects: function(ctx, objects) { // objects不包含组选对象,组选对象里面的object的left/top是相对值,否则是原始值
var i, len;
for (i = 0, len = objects.length; i < len; ++i) {
objects[i] && objects[i].render(ctx); // 每个object执行fabric object的通用render方法,再调用每个object的_render()重画
}
},

render: function(ctx) { // object通用render函数,拖拽时执行
// do not render if width/height are zeros or object is not visible
if (this.isNotVisible()) {
return;
}
if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) {
return;
}
ctx.save();
this._setupCompositeOperation(ctx);
this.drawSelectionBackground(ctx);
this.transform(ctx); // ctx.transform(用计算出来的matrix数据),根据objcet的属性变化用ctx.transform()变换canvas的坐标系,也就是针对被拖动的object变换canvas的坐标系跟随鼠标拖拽变化
this._setOpacity(ctx);
this._setShadow(ctx, this);

if (this.shouldCache()) { // 初始化时,以及选取拖拽都会执行到这步,shouldCache()设置并返回this.ownCaching=true
this.renderCache(); // 如果用cache,就不在目标canvas用_render()重画,只需变换transform,下一步执行drawImage()实现重画
this.drawCacheOnCanvas(ctx);// 复制每个object的私有cacheCanvas到目标canvas实现重画
}
else { // 初始化时,以及选取拖拽都不会执行到这步,禁止object cache之后会执行
this._removeCacheCanvas();
this.dirty = false;
this.drawObject(ctx); // 会调object的specific画图函数object._render()在目标canvas重画
if (this.objectCaching && this.statefullCache) {
this.saveState({ propertySet: 'cacheProperties' });
}
}
ctx.restore();
},


renderCache: function(options) {
options = options || {};
if (!this._cacheCanvas) {
this._createCacheCanvas();
}
if (this.isCacheDirty()) { // 拖拽时会执行到这儿,但dirty=false不执行下面代码重新画图,拖拽开始和结束时有时会执行下面,拖拽重画不在这儿处理
console.log('now execute drawObject')
this.statefullCache && this.saveState({ propertySet: 'cacheProperties' }); // statefullCache=false,不会执行saveState()
this.drawObject(this._cacheContext, options.forClipping); // 在cache canvas重画,有两个drawObject(),执行14846位置的drawObject -> this._render()重新画图
this.dirty = false;
}
},


drawObject: function(ctx, forClipping) { // ctx可以是目标canvas或cache canvas
var originalFill = this.fill, originalStroke = this.stroke;
if (forClipping) {
this.fill = 'black';
this.stroke = '';
this._setClippingProperties(ctx);
}
else {
this._renderBackground(ctx);
this._setStrokeStyles(ctx, this);
this._setFillStyles(ctx, this);
}
this._render(ctx);
this._drawClipPath(ctx);
this.fill = originalFill;
this.stroke = originalStroke;
},

在fabric object通用render方法中,执行完this.renderCache()之后执行下面的方法:
drawCacheOnCanvas: function(ctx) { // 拖拽时执行,ctx是目标画布,裁剪复制cache canvas到目标canvas,
ctx.scale(1 / this.zoomX, 1 / this.zoomY);
//document.body.appendChild(this._cacheCanvas); // 显示观察cache canvas,删除之后会随时创建
// 拖拽时被拖拽的object(this)的left/top在不断变化,其它属性无变化,之前已经根据left/top的变化对canvas进行了transform变换
ctx.drawImage(this._cacheCanvas, -this.cacheTranslationX, -this.cacheTranslationY); // 固定值128.5,cache canvas width/height=256px,这个实现了object重画
},

当拖拽时并没有调用object._render()方法重新画图,而是用ctx.drawImage()实现重新画图,之前已经根据object的left/top变化对ctx进行了transform变换,也就是下面这句:
this.transform(ctx)

从cache canvas复制过来的object的位置按canvas变换transform之后的坐标系重画,就跟随鼠标拖拽改变了位置。

fabric最难看懂的代码也就是在这个环节,它按object变换canvas的transform之后并没有执行object._render()重画object,而是用drawImage()方法实现了object重画,
初始化时用object._render()方法画图,移动时不用,拖拽时从cache复制object提高了效率和性能,不用每次都执行object._render()把object重画一遍,非常高超。


fabric代码中有一段计算鼠标位置是否在一个object范围内的代码,用了特殊方法,分析如下:
// 用复杂算法计算鼠标坐标是否在一个矩形区域范围内
var lines = { // 矩形坐标
bottomline: {
d: {x:10,y:60}, // 左
o: {x:60,y:60} // 右
},
leftline: {
d: {x:10,y:60}, // 下
o: {x:10,y:10}, // 上
},
rightline: {
d: {x:60,y:60}, // 下
o: {x:60,y:10} // 上
},
topline: {
d: {x:60,y:10}, // 右
o: {x:10,y:10} // 左
}
};

var point = {x:20,y:20}; // 鼠标坐标

var xcount = findCrossPoints(point,lines);
console.log(xcount);
if (xcount !== 0 && xcount % 2 === 1) {
console.log('point在lines中')
}else{
console.log('point不在lines中')
}

function findCrossPoints(point, lines) {
var b1, b2, a1, a2, xi, // yi,
xcount = 0,
iLine;

for (var lineKey in lines) { // per line
iLine = lines[lineKey];
// optimisation 1: line below point. no cross
if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) { // line在point上面,如果point在矩形下方,到这步均不处理
continue;
}
// optimisation 2: line above point. no cross
if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) { // line在point下面,如果point在矩形上方,到这步均不处理
continue;
}
// optimisation 3: vertical line case(point与矩形在水平方向有交叉,要看垂直方向有无交叉,xcount值代表有无交叉,=1有交叉,=2无交叉)
if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) { // 竖线在point右侧记一次,如果有两次,则矩形在point右侧
xi = iLine.o.x;
// yi = point.y;
// dont count xi < point.x cases
if (xi >= point.x) { // 竖线在point右侧则记录一次
xcount += 1;
}
// optimisation 4: specific for square images
if (xcount === 2) { // 两条竖线都在point右侧(矩形在point右侧),如果一条竖线在point右侧,则point在矩形内。
break;
}
}
// calculate the intersection point
else { // 不是point右侧的竖线会执行这步,比如point左侧的竖线
// b1 = 0;
// b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); // ?
// a1 = point.y - b1 * point.x;
// a2 = iLine.o.y - b2 * iLine.o.x;

// xi = -(a1 - a2) / (b1 - b2); // ?
//xi=NaN;
// yi = a1 + b1 * xi;
}

}
return xcount;
}

它这种计算法也是成立的,有点复杂,其实用普通计算方法即可,另外最后一段复杂计算代码是垃圾,故弄玄虚,根本没用。

fabric为每种object构造了一个主类class,所有object的类class都是类似的,都继承了fabric Object的主类class,以矩形object为例:
fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ {
function createClass() {
function klass() {
this.initialize.apply(this, arguments);
}

调用方法:
var box1 = new fabric.Rect({ // 调fabric画多边形,box1是fabric klass实例
left: 10, top: 10,
width: 50, height: 50,fill: '#999'
})
canvasTest.add(box1)


这属于常规对象编程技术,没有什么特别的,所有插件的对象编程方法都差不多,fabric这个插件的难点还是在于canvas鼠标事件的相关处理。
一般在开发中不会涉及到canvas鼠标事件,只涉及html元素的鼠标事件,fabric是基于canvas的应用典范,代码非常高超,有些代码拖拽时不执行,
不知道有什么用,有些代码看不懂,不知道是干什么用的,非常深奥。

 

本文到此结束,欢迎批评指正。