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-…) 上面內容若是寫的有什麼問題的話,歡迎你們指正🤞🤞🤞🤞

相關文章
相關標籤/搜索