vue + fabricjs 实现简易画图板

由于公司须要用fabric.js这个框架,因此在学习fabric.js的时候作了这样的一个简易画图板的demo,主要功能有:画直线,画圆, 画矩形, 画贝塞尔曲线,侦测(就是判断鼠标是否是移动到了这个对象附近,若是是的话,吸附在对象上,我就作了贝塞尔曲线的侦测,由于直线侦测的思路与贝塞尔曲线差很少),镜像(目前就作了贝塞尔曲线的镜像),删除,调整直线长短,显示直线长度,修改贝塞尔曲线的弧度,位置等功能vue

开始

  1. 新建vue项目
  2. 在项目中安装fabric npm install fabric--save,将其引入到你的.vue文件夹中 import { fabric } from 'fabric',fabric 须要在.vue文件的 mounted()生命周期中使用
  3. 在中写一个<canvas id="main" width="1920" height="600" ref="cvs"></canvas>,而后在mounted中初始化画布,初始化分为如下步骤
  • 声明画布
let canvas = new fabric.Canvas("main", {
    backgroundColor: "rgb(100,100,200)"
});
复制代码
  • 肯定窗口与画布的位置:由于鼠标的位置是相对于整个屏幕来讲,可是咱们须要知道的位置是鼠标在画布上的相对位置,因此在页面初始化的时候,就肯定canvas在屏幕上的相对位置,经过鼠标的位置减去canvas距离上左的距离,从而算出鼠标在画布上的相对位置,因此咱们要用到offsetX, offsetY
    image.png
  • 肯定某些画板元素是否要被禁用或者开启
canvas.skipTargetFind = true; //画板元素不能被选中
 canvas.selection = false; //画板不显示选中
复制代码

image.png
这个是初始化完画布的样子,为了更加清晰,因此我这边加了一个颜色

画直线 && 修改直线

line.gif
这边的画直线,不是给你肯定的两个点而后显示这条直线就行,咱们是要像真的在画一条直线同样,一点点的画过去 画直线思路:

  1. 监听鼠标按下的事件,将这个点的x,y存在两个变量中(mouseFromX, mouseFromY)做为做为起始点
  2. 监听抬起鼠标的事件,将这个点的x,y存在两个变量中(mouseToX, mouseToY)做为终点,你可能会说,那不是就是两点肯定一条直线,怎么会有一点点画过去的效果???对,这个样子仍是不会出现咱们想要的效果,因此看第3步
  3. 监听移动鼠标的事件,将移动点的x,y存在两个变量中(mouseToX, mouseToY)做为终点,这个样子就能出现咱们想要的效果,可是,这个样子咱们又会出现另外一个问题,就是鼠标位置一直在移动,咱们会画出来好多好多的线,就像是从一个点发出无数条射线同样,解决这个问题,咱们能够看第4步
  4. 在移动的过程当中每次画下一条线时,都要删除上一条线就能够,不过这个样子也会致使你画下一条线的时候上一条线消失,抬起鼠标的时候,从新在画布上画上

==注意:咱们在划线的时候须要在线的两端画上两个小圆球,而且这两个圆球须要存这条直线的信息,直线也要存这两个圆球的信息,由于咱们到时候要修改直线长度位置之类的==git

修改直线思路:github

  1. 监听小球移动的事件
  2. 小球拖动,修改直线的一段的坐标

主要代码:npm

function mouseUpLine(options, canvas) {
    isMouseDown = false;
    mouseToX = options.e.offsetX;
    mouseToY = options.e.offsetY;
    canvas.add(line, point1, point2);
    let lineObj = { 'id': lineArray.length, 'detail': line, 'leftPoint': point1, 'rightPoint': point2 };
    lineArray.push(lineObj);
    return computedLineLength(mouseFromX, mouseFromY, mouseToX, mouseToY);
}

function lineData() {
    return lineArray;
}

function ObjectMove(options, canvas) {
    var p = options.target;
    let lineLength = 0;
    if (p.line1) {
        p.line1.set({ x2: p.left, y2: p.top });
        lineLength = computedLineLength(p.line1.x1, p.line1.y1, p.line1.x2, p.line1.y2);
    }
    if (p.line2) {
        p.line2.set({ x1: p.left, y1: p.top });
        lineLength = computedLineLength(p.line2.x1, p.line2.y1, p.line2.x2, p.line2.y2);
    }
    canvas.renderAll();
    return lineLength;
}
// 画直线
function drawLine(mouseFromX, mouseFromY, mouseToX, mouseToY) {
    line = new fabric.Line([mouseFromX, mouseFromY, mouseToX, mouseToY], {
        fill: 'green',
        stroke: 'green', // 笔触颜色
        strokeWidth: 2, // 笔触宽度
        hasControls: false, // 选中时是否能够放大缩小
        hasRotatingPoint: false, // 选中时是否能够旋转
        hasBorders: false, // 选中时是否有边框
        selectable: false,
        evented: false
    });
    point1 = makeCircle(line.get('x2'), line.get('y2'), line, null);
    point2 = makeCircle(line.get('x1'), line.get('y1'), null, line);
    line.point1 = point1;
    line.point2 = point2;
    return line;
}
// 画球
function makeCircle(left, top, line1, line2) {
    var c = new fabric.Circle({
        left: left,
        top: top,
        strokeWidth: 2,
        radius: 6,
        fill: '#fff',
        stroke: '#666',
        originX: 'center',
        originY: 'center'
    });
    c.hasControls = c.hasBorders = false;

    c.line1 = line1;
    c.line2 = line2;

    return c;
}
复制代码

画圆

画圆主要代码:(思路比较简单,就不讲了) json

circle.gif

function makeCircle(left, top, r) {
  circleObj = new fabric.Circle({
    left: left,
    top: top,
    strokeWidth: 2,
    radius: r,
    fill: '#fff',
    stroke: '#666',
    originX: 'center',
    originY: 'center'
  });
  circleObj.hasControls = circleObj.hasBorders = false;
}
复制代码

画矩形

画矩形主要代码:(思路比较简单,就不讲了) canvas

rect.gif

function makeRect(left, top, width, height) {
  rectObj = new fabric.Rect({
    left: left,
    top: top,
    height: height,
    width: width,
    fill: 'white',
    stroke: '#666'
  });
  rectObj.hasControls = rectObj.hasBorders = false;
}
复制代码

画bezier曲线

画bezier曲线思路:(画这个东西有点麻烦,建议先百度bezier曲线的相关内容) 数组

bezier.gif

  1. 咱们使用三阶的bezier曲线去画,path通常是以'M 开始点x, 开始点y,C 1号控制点x, 1号控制点y,2号控制点x, 2号控制点y, 结束点x, 结束点y',演示图中,红色球,蓝色球为控制点,白色球为开始点和结束点,咱们统称为锚点
    image.png
  2. 为了保持第一条bezier曲线和第二条bezier曲线链接顺滑,因此第一条bezier曲线的2号控制点须要和第二条bezier曲线的1号控制点须要在一条直线上(我这边的处理时锚点为两个控制点的中点)
  3. 锚点和控制点有必定的联系,因此咱们在建立一个锚点的时候,就会同时建立两个控制点(并不会直线添加到画布上),而且控制点与锚点重合,锚点中须要存这两个控制点的信息,方便之后使用
  4. 要连续的画线,因此画第二条bezier曲线的时候,须要将前一条bezier曲线的结束点做为第二条bezier曲线的开始点
  5. 按空格结束划线

移动锚点和控制点的思路:框架

  1. 点击锚点,绘制存在锚点中的两个控制点
  2. 若是要更改bezier曲线的弧度,须要移动控制点时(好比说移动蓝色控制点),根据锚点为两个控制点的终点,画出另外一个没有移动的锚点(红色控制点),而且更新这两个控制点在这个锚点上的坐标信息
  3. 若是要移动锚点,则须要记录锚点移动的x, y,从而算出移动了多少,而且也要将该锚点上的控制点,增长或减小相应的移动距离
  4. 根据最新的坐标信息,从新绘制该条bezier曲线

主要代码:less

// 鼠标移动
function bezierMouseMove(options, canvas) {
    if (!anchorArr.length) return;
    let point = { left: options.e.offsetX, top: options.e.offsetY };
    if (!isMouseDown) {
        // isFinish = false;
        canvas.remove(temBezier, temAnchor);
        let anchor = anchorArr[anchorArr.length - 1];
        makeBezier(anchor, anchor.nextConP, anchor.nextConP, point);
        let startCon = makeBezierConP(point.left, point.top, 'red');
        temAnchor = makeBezierAnchor(point.left, point.top, startCon, startCon);
        canvas.add(temBezier, temAnchor);
    } else {
        if (anchorArr.length > 1) {
            canvas.remove(temBezier);
            // 开始点
            let preAnchor = anchorArr[anchorArr.length - 2];
            // 结束点
            currentAnchor = anchorArr[anchorArr.length - 1];
            // 鼠标位置为当前锚点的后控制点
            let currentPreContrl = { left: point.left, top: point.top };
            let currentNextContrl = { left: 2 * currentAnchor.left - point.left, top: 2 * currentAnchor.top - point.top };
            // 每次画都是数组中的数组的最后一个点和倒数第二个点为bezier的第一个点个最后一个点
            makeBezier(preAnchor, preAnchor.nextConP, currentAnchor.preConP, currentAnchor);
            canvas.add(temBezier);
            temCanvas = canvas;
            // 更新当前锚点的后控制点
            currentAnchor.preConP = currentNextContrl;
            currentAnchor.nextConP = currentPreContrl;
            currentAnchor.preConP.name = 'preAnchor';
            currentAnchor.nextConP.name = 'nextAnchor';
        }
    }
}
// 移动控制点
function changeControl(options, canvas) {
    console.log(options);
    clickPostion = { 'left': options.transform.original.left, 'top': options.transform.original.top };
    if (!targetAnchor) return;
    let controlPoint = options.target;
    let whichBezier = bezierArray[targetAnchor.lineName];
    // console.log(targetAnchor);
    let point = { 'left': options.e.offsetX, 'top': options.e.offsetY };
    // 经过控制点的颜色,肯定点击的是前控制点仍是后控制点
    if (controlPoint.fill === 'red') {
        // 改变先后控制点的坐标
        targetAnchor.preConP.left = point.left;
        targetAnchor.preConP.top = point.top;
        targetAnchor.nextConP.left = targetAnchor.left * 2 - point.left;
        targetAnchor.nextConP.top = targetAnchor.top * 2 - point.top;
        // 从新绘制控制点
        canvas.remove(preContPoint, nextContPoint);
        preContPoint = makeBezierConP(targetAnchor.preConP.left, targetAnchor.preConP.top, 'red');
        nextContPoint = makeBezierConP(targetAnchor.nextConP.left, targetAnchor.nextConP.top, 'blue');
        canvas.add(preContPoint, nextContPoint);
        // console.log(whichBezier.detail[targetAnchor.id]);
    } else if (controlPoint.fill === 'blue') {
        targetAnchor.preConP.left = targetAnchor.left * 2 - point.left;
        targetAnchor.preConP.top = targetAnchor.top * 2 - point.top;
        targetAnchor.nextConP.left = point.left;
        targetAnchor.nextConP.top = point.top;
        canvas.remove(preContPoint, nextContPoint);
        preContPoint = makeBezierConP(targetAnchor.preConP.left, targetAnchor.preConP.top, 'red');
        nextContPoint = makeBezierConP(targetAnchor.nextConP.left, targetAnchor.nextConP.top, 'blue');
        canvas.add(preContPoint, nextContPoint);
    } else if (controlPoint.fill === 'white') {
        console.log(clickPostion);
        let moveLeft = point.left - clickPostion.left;
        let moveTop = point.top - clickPostion.top;
        // console.log(moveTop, moveLeft, targetAnchor.preConP.left);
        targetAnchor.preConP.left = targetAnchor.preConP.left + moveLeft - lastMoveLeft;
        targetAnchor.preConP.top = targetAnchor.preConP.top + moveTop - lastMoveTop;
        targetAnchor.nextConP.left = targetAnchor.nextConP.left + moveLeft - lastMoveLeft;
        targetAnchor.nextConP.top = targetAnchor.nextConP.top + moveTop - lastMoveTop;
        canvas.remove(preContPoint, nextContPoint);
        preContPoint = makeBezierConP(targetAnchor.preConP.left, targetAnchor.preConP.top, 'red');
        nextContPoint = makeBezierConP(targetAnchor.nextConP.left, targetAnchor.nextConP.top, 'blue');
        canvas.add(preContPoint, nextContPoint);
        lastMoveLeft = moveLeft;
        lastMoveTop = moveTop;
    }
    // console.log('改变过', targetAnchor, bezierArray);
    // 更新当前条bezier曲线的当前锚点信息
    bezierArray[targetAnchor.lineName].detail[targetAnchor.id] = targetAnchor;
    // 针对于最后一个点, 由于没有当前选中点的后一个锚点
    if (whichBezier.detail[targetAnchor.id + 1]) {
        canvas.remove(whichBezier.segmentBezier[targetAnchor.id]);
        // 画当前选中锚点的后一条bezier曲线 参数:当前选中的锚点,当前点选中锚点的后控制点, 当前选中锚点的后一个锚点的前控制点,当前选中锚点的后一个锚点
        newNextBezier = makeBezier(whichBezier.detail[targetAnchor.id], whichBezier.detail[targetAnchor.id].nextConP, whichBezier.detail[targetAnchor.id + 1].preConP, whichBezier.detail[targetAnchor.id + 1]);
        // 更新当前选中锚点的后一条bezier曲线
        whichBezier.segmentBezier[targetAnchor.id] = newNextBezier;
        canvas.add(whichBezier.segmentBezier[targetAnchor.id]);
    }
    // 针对于开始点, 由于没有当前选中点的前一个锚点
    if (whichBezier.detail[targetAnchor.id - 1]) {
        canvas.remove(whichBezier.segmentBezier[targetAnchor.id - 1]);
        // 画当前选中锚点的前一条bezier曲线 参数:当前选中锚点的前一个锚点, 当前选中锚点的前一个锚点的后控制点,当前选中锚点的前控制点, 当年前选中的锚点
        newPreBezier = makeBezier(whichBezier.detail[targetAnchor.id - 1], whichBezier.detail[targetAnchor.id - 1].nextConP, whichBezier.detail[targetAnchor.id].preConP, whichBezier.detail[targetAnchor.id]);
        // 更新当前选中锚点的前一条bezier曲线
        whichBezier.segmentBezier[targetAnchor.id - 1] = newPreBezier;
        canvas.add(whichBezier.segmentBezier[targetAnchor.id - 1]);
    }
}
// 建立锚点
function makeBezierAnchor(left, top, preConP, nextConP) {
    var c = new fabric.Circle({
        left: left,
        top: top,
        strokeWidth: 2,
        radius: 6,
        fill: 'white',
        stroke: '#666',
        originX: 'center',
        originY: 'center'
    });

    c.hasBorders = c.hasControls = false;
    // preConP是上一条线的控制点nextConP是下一条线的控制点
    c.preConP = preConP;
    c.nextConP = nextConP;
    c.name = 'anchor';
    c.lineName = bezierArray.length;
    c.id = anchorArr.length;
    return c;
}
// 按空格键结束画图
function keyDown(event) {
    if (event && event.keyCode === 32) {
        temCanvas.remove(temAnchor, temBezier, preContPoint, nextContPoint);
        segmentBezierArr.forEach(element => {
            element.belongToId = bezierArray.length;
        });
        bezierArray.push({ id: bezierArray.length, 'detail': anchorArr, 'segmentBezier': segmentBezierArr });
        anchorArr.forEach(item => {
            temCanvas.bringToFront(item);
        });
        temBezier = null;
        temAnchor = null;
        currentAnchor = null;
        preContPoint = null;
        nextContPoint = null;
        isMouseDown = false;
        anchorArr = [];
        segmentBezierArr = [];
        console.log(bezierArray);
        // isFinish = true;
    }
}
复制代码

删除

delete.gif
思路:fabric 提供了getActiveObject() 获取选中的对象这个接口,因此只要获取到这个对象,而后canvas.remove(这个对象)就行

主要代码:学习

canvas.skipTargetFind = false;
  if (canvas.getActiveObject() && canvas.getActiveObject().belongToId === undefined) {
    canvas.remove(canvas.getActiveObject().point1);
    canvas.remove(canvas.getActiveObject().point2);
    canvas.remove(canvas.getActiveObject());
  }
  if (canvas.getActiveObject() && canvas.getActiveObject().belongToId !== undefined) {
    deleteBezier(options, canvas);
  }
复制代码

侦测

detect.gif
思路:(我这边只作了曲线的侦测,直线侦测与曲线侦测相似)

  1. 咱们知道三阶bezier曲线的方程,根据方程取得曲线上的100个或者1000个点
  2. 咱们设一个靠近的最短值,若是鼠标靠近曲线上点的距离小于这个最短值,那么就以那个点为圆心画一个空心球,这样就会出现一个侦测的效果
  3. 可是这也存在一个问题,就是小于这个最短值的点有好多个的话,就会画出好多个球,因此咱们要找离鼠标最近的那个点画球就能够了
/** * 三阶贝塞尔曲线方程 * B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1] * @param t 曲线长度比例 * @param p0 起始点 * @param p1 控制点1 * @param p2 控制点2 * @param p3 终止点 * @return t对应的点 */
    CalculateBezierPointForCubic : function ( t, p0, p1, p2, p3) {
        var point = cc.p( 0, 0 );
        var temp = 1 - t;
        point.x = p0.x * temp * temp * temp + 3 * p1.x * t * temp * temp + 3 * p2.x * t * t * temp + p3.x * t * t * t;
        point.y = p0.y * temp * temp * temp + 3 * p1.y * t * temp * temp + 3 * p2.y * t * t * temp + p3.y * t * t * t;
        return point;
    }
复制代码

主要代码:

function mouseMove(options, canvas) {
    let point = { 'x': options.e.offsetX, 'y': options.e.offsetY };
    let min = Infinity;
    linePostionArr.forEach(item => {
        let len = computedMin(point, item);
        if (len < minDetect && min > len) {
            min = len;
            minPoint = item;
        }
    });
    if (!minPoint) return;
    // console.log(minPoint);
    let l = computedMin(point, minPoint);
    if (l < minDetect) {
        canvas.remove(detectPoint);
        detectPoint = makePoint(minPoint.x, minPoint.y);
        canvas.add(detectPoint);
    } else {
        canvas.remove(detectPoint);
    }
}
复制代码

镜像

mirror.gif
思路:(目前只作了bezier曲线的镜像,直线相似)

  1. 咱们当时画beizer曲线时,会把全部的锚点存在一个数组中
  2. 计算出锚点/控制点与咱们画的直线的方程的垂足
  3. 将这个垂足做为中点,画出对称的锚点/控制点
  4. 根据对称点,画出镜像bezier曲线
    image.png

主要代码:mirror.js

// 返回中点位置
function intersectionPoint(x1, y1, x2, y2, point) {
    let linek = (y2 - y1) / (x2 - x1);
    let b1 = y1 - linek * x1;
    let verticalk = -1 / linek;
    let b2 = point.top - verticalk * point.left;
    let x = (b2 - b1) / (linek - verticalk);
    let y = (linek * linek * b2 + b1) / (linek * linek + 1);
    return { 'left': x, 'top': y };
}
// 修改点的坐标并存入新的数组
function SymmetricalPoint(mirrorArray) {
    mirrorArray.forEach((item, index) => {
        console.log(index, item);
        let centerPoint = intersectionPoint(mouseFromX, mouseFromY, mouseToX, mouseToY, item);
        // console.log('我是锚点中心点', centerPoint);
        let startPoint = computedSymmetricalPoint(centerPoint.left, centerPoint.top, item.left, item.top);
        item.left = startPoint.left;
        item.top = startPoint.top;
        let centerPointPre = intersectionPoint(mouseFromX, mouseFromY, mouseToX, mouseToY, item.preConP);
        // console.log('我是前控制点中心点', index, centerPointPre);
        let preControl = computedSymmetricalPoint(centerPointPre.left, centerPointPre.top, item.preConP.left, item.preConP.top);
        // item.preConP.set({ 'left': preControl.left, 'top': preControl.top});
        let newItem = Object.assign({}, item.preConP);
        newItem.left = preControl.left;
        newItem.top = preControl.top;
        item.preConP = newItem;
        // console.log('看下控制点是否改变', item.preConP);
        let centerPointNext = intersectionPoint(mouseFromX, mouseFromY, mouseToX, mouseToY, item.nextConP);
        // console.log('我是后控制点中心点',index, centerPointNext);
        let nextControl = computedSymmetricalPoint(centerPointNext.left, centerPointNext.top, item.nextConP.left, item.nextConP.top);
        item.nextConP.left = nextControl.left;
        item.nextConP.top = nextControl.top;
        mirrorPointArr.push(item);
    });
    // console.log('---查看加工后的mirrorPointArr------', mirrorPointArr);
}
// 计算对称点
function computedSymmetricalPoint(cLeft, cTop, xLeft, xTop) {
    // console.log(cLeft, cTop, xLeft, xTop);
    let left = 2 * cLeft - xLeft;
    let top = 2 * cTop - xTop;
    let point = { 'left': left, 'top': top };
    return point;
}
复制代码

==注意:mirror.js是只针对锚点存在数组中这种储存方式,因此这个js文件只能镜像bezier曲线,可是若是你能将画出来的曲线或者直线路径存储为 'M 495 105 C 495 105 707 204 619 302 C 531 400 531 400 516 492 L 200 200 L 500 500' 这种类型,能够直接用mirrorPath.js文件进行镜像,这个能够将无论直线曲线或者其余类型都能成功镜像==

一些经常使用API

对象:

fabric.Circle 圆 fabric.Ellipse 椭圆 fabric.Line 直线 fabric.Polygon fabric.Polyline fabric.Rect 矩形 fabric.Triangle 三角形

方法:

add(object) 添加 insertAt(object,index) 添加 remove(object) 移除 forEachObject 循环遍历 getObjects() 获取全部对象 item(int) 获取子项 isEmpty() 判断是否空画板 size() 画板元素个数 contains(object) 查询是否包含某个元素 fabric.util.cos fabric.util.sin fabric.util.drawDashedLine 绘制虚线 getWidth() setWidth() getHeight() clear() 清空 renderAll() 重绘 requestRenderAll() 请求从新渲染 rendercanvas() 重绘画板 getCenter().top/left 获取中心坐标 toDatalessJSON() 画板信息序列化成最小的json toJSON() 画板信息序列化成json moveTo(object,index) 移动 dispose() 释放 setCursor() 设置手势图标 getSelectionContext()获取选中的context getSelectionElement()获取选中的元素 getActiveObject() 获取选中的对象 getActiveObjects() 获取选中的多个对象 discardActiveObject()取消当前选中对象 isType() 图片的类型 setColor(color) = canvas.set("full",""); rotate() 设置旋转角度 setCoords() 设置坐标

事件:

object:added object:removed object:modified object:rotating object:scaling object:moving object:selected 这个方法v2已经废弃,使用selection:created替代,多选不会触发 before:selection:cleared selection:cleared selection:updated selection:created path:created mouse:down mouse:move mouse:up mouse:over mouse:out mouse:dblclick

经常使用属性:

canvas.isDrawingMode = true; 能够自由绘制 canvas.selectable = false; 控件不能被选择,不会被操做 canvas.selection = true; 画板显示选中 canvas.skipTargetFind = true; 整个画板元素不能被选中 canvas.freeDrawingBrush.color = "#E34F51" 设置自由绘画笔的颜色 freeDrawingBrush.width 自由绘笔触宽度

IText的方法:

selectAll() 选择所有 getSelectedText() 获取选中的文本 exitEditing() 退出编辑模式

结束

若是须要看源码的话,能够点击👉[项目github地址]:(github.com/JZHEY/Draw-…) 上面内容若是写的有什么问题的话,欢迎你们指正🤞🤞🤞🤞