vue + fabricjs 实现简易画图板

帅比萌擦擦* 提交于 2020-05-09 14:07:26

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

开始

  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. 在移动的过程中每次画下一条线时,都要删除上一条线就可以,不过这个样子也会导致你画下一条线的时候上一条线消失,抬起鼠标的时候,重新在画布上画上

==注意:我们在划线的时候需要在线的两端画上两个小圆球,并且这两个圆球需要存这条直线的信息,直线也要存这两个圆球的信息,因为我们到时候要修改直线长度位置之类的==

修改直线思路:

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

主要代码:

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;
}
复制代码

画圆

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

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;
}
复制代码

画矩形

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

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曲线

主要代码:

// 鼠标移动
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-…) 上面内容如果写的有什么问题的话,欢迎大家指正🤞🤞🤞🤞

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!