1. 程式人生 > 其它 >實現簽字功能

實現簽字功能

封裝元件,實現簽字畫板功能

<template>
  <div class="home">
    <div class="btnwrap">
      <div
        @click="showStrokeColorPicker = !showStrokeColorPicker"
        class="btn-color"
        ref="strokeColor"
      >
        <Sketch-picker
          v-if="showStrokeColorPicker"
          class
="color-picker" :value="strokeColor" @input="updateStrokeColor" /> </div> <span class="color-label" :style="{ color: strokeColor }">線條顏色</span> <!-- <div @click="showFillColorPicker = !showFillColorPicker" class="btn-color" ref="fillColor" > <Sketch-picker v-if="showFillColorPicker" class="color-picker" :value="fillColor" @input="updateFillColor" /> </div> <span class="color-label" :style="{ color: strokeColor }">填充顏色</span>
--> <div v-show="true" @click="showBgColorPicker = !showBgColorPicker" class="btn-color" ref="bgColor" > <Sketch-picker v-if="showBgColorPicker" class="color-picker" :value="bgColor" @input="updateBgColor"
/> </div> <span class="color-label" :style="{ color: strokeColor }">畫布顏色</span> <div class="brushWidth"> <label :style="{ color: strokeColor }">線條大小:{{ lineSize }}</label> <input type="range" name="vol" min="1" max="100" v-model="lineSize" /> </div> <!-- <div class="brushWidth"> <label :style="{ color: strokeColor }">文字大小:{{ fontSize }}</label> <input type="range" name="vol" min="18" max="50" v-model="fontSize" /> </div> --> <div class="btnList"> <div @click="tapToolBtn('brush')" :class="{ active: selectTool === 'brush' }" class="btn-tool" > <i class="iconfont icon-noun__cc"></i> </div> <!-- <div @click="tapToolBtn('line')" :class="{ active: selectTool === 'line' }" class="btn-tool" > <i class="iconfont icon-jurassic_line"></i> </div> --> <!-- <div @click="tapToolBtn('rect')" :class="{ active: selectTool === 'rect' }" class="btn-tool" > <i class="iconfont icon-juxing"></i> </div> <div @click="tapToolBtn('circle')" :class="{ active: selectTool === 'circle' }" class="btn-tool" > <i class="iconfont icon-yuanxingweixuanzhong"></i> </div> --> <!-- <div @click="tapToolBtn('text')" :class="{ active: selectTool === 'text' }" class="btn-tool" > <i class="iconfont icon-xingzhuang-wenzi"></i> </div> --> <div @click="tapToolBtn('eraser')" :class="{ active: selectTool === 'eraser' }" class="btn-tool" > <i class="iconfont icon-xiangpi"></i> </div> <!-- <div @click="tapToolBtn('move')" :class="{ active: selectTool === 'move' }" class="btn-tool" > <i class="iconfont icon-24gl-move"></i> </div> --> <!-- <div @click="tapToolBtn('select')" :class="{ active: selectTool === 'select' }" class="btn-tool" > <i class="iconfont icon-xuanzhong"></i> </div> --> <!-- <div @click="tapScaleBtn(-1)" class="btn-tool"> <i class="iconfont icon-suoxiao"></i> </div> <div @click="tapScaleBtn(1)" class="btn-tool"> <i class="iconfont icon-fangda"></i> </div> --> <!-- <div @click="tapHistoryBtn(-1)" class="btn-tool"> <i class="iconfont icon-fanhuishangyibu-"></i> </div> <div @click="tapHistoryBtn(1)" class="btn-tool"> <i class="iconfont icon-fanhuixiayibu-"></i> </div> --> <div @click="tapClearBtn()" class="btn-tool"> <i class="iconfont icon-qingkong"></i> </div> <div @click="tapSaveBtn()" class="btn-tool"> <i class="iconfont icon-baocun_o"></i> </div> <!-- <div @click="tapDownBtn()" class="btn-tool"> <i class="iconfont icon-xiazai"></i> </div> height:260px;--> </div> </div> <canvas class="canvas" ref="canvas" :style="{ height: height }"></canvas> </div> </template> <script> import { fabric } from "fabric"; import "./eraser_brush.mixin.js"; import { Sketch } from "vue-color"; // import { mapGetters } from "vuex"; export default { components: { "Sketch-picker": Sketch, }, computed: { // ...mapGetters(["hammerType"]) }, prop: { height: { type: String, required: false, default: "260px", }, }, data() { return { canvas: null, // fabric canvas物件 strokeColor: "#000000", // 線框色 showStrokeColorPicker: false, // 是否顯示 線框色選擇器 fillColor: "rgba(0,0,0,0)", // 填充色 showFillColorPicker: false, // 是否顯示 填充色選擇器 bgColor: "#ffffff", // 背景色 showBgColorPicker: false, // 是否顯示 背景色選擇器 lineSize: 10, // 線條大小 (線條 and 線框) fontSize: 18, // 字型大小 selectTool: "", // 當前使用者選擇的繪圖工具 畫筆:brush 直線:line 矩形:rect 圓形 circle 文字 text mouseFrom: {}, // 滑鼠繪製起點 mouseTo: {}, // 滑鼠繪製重點 drawingObject: null, // 儲存滑鼠未鬆開時使用者繪製的臨時影象 textObject: null, // 儲存使用者建立的文字物件 isDrawing: false, // 當前是否正在繪製圖形(畫筆,文字模式除外) stateArr: [], // 儲存畫布的操作記錄 stateIdx: 0, // 當前操作步數 isRedoing: false, // 當前是否在執行撤銷或重做操作 }; }, watch: { // hammerType: { // handler(val, oldVal) { // if (val === "top") { // this.initCanvas(); // // 初始化 畫布 // // 預設開啟畫筆模式 // this.tapToolBtn("brush"); // // 初始化 畫布 事件 // this.initCanvasEvent(); // } // } // }, // 監聽線條大小變化 lineSize() { this.canvas.freeDrawingBrush.width = parseInt(this.lineSize, 10); this.lineSize = parseInt(this.lineSize, 10); }, // 監聽背景色變化 bgColor() { this.canvas.setBackgroundColor(this.bgColor, undefined, { erasable: false, }); this.canvas.renderAll(); }, }, mounted() { this.initCanvas(); // 初始化 畫布 // 預設開啟畫筆模式 this.tapToolBtn("brush"); // 初始化 畫布 事件 this.initCanvasEvent(); }, methods: { // 監聽線框色選擇器 顏色選擇 updateStrokeColor(val) { // 儲存使用者選擇的線框色 this.strokeColor = val.hex; // 修改當前選擇的顏色指示 this.$refs.strokeColor.style.backgroundColor = this.strokeColor; this.tapToolBtn(); this.tapToolBtn("brush"); }, // 監聽填充色選擇器 顏色選擇 updateFillColor(val) { // 儲存使用者選擇的線框色 // this.fillColor = val.hex; // 修改當前選擇的顏色指示 // this.$refs.fillColor.style.backgroundColor = this.fillColor; }, // 監聽背景色選擇器 顏色選擇 updateBgColor(val) { // 儲存使用者選擇的背景色 this.bgColor = val.hex; this.$refs.bgColor.style.backgroundColor = this.bgColor; }, // 初始化畫布 initCanvas() { this.$refs.canvas.width = this.$refs.canvas.offsetWidth; this.$refs.canvas.height = this.$refs.canvas.offsetHeight; // 初始化線框色 與 指示器 this.$refs.strokeColor.style.backgroundColor = this.strokeColor; // 初始化填充色 與 指示器 // this.$refs.fillColor.style.backgroundColor = this.fillColor; // 初始化背景色 與 指示器 this.$refs.bgColor.style.backgroundColor = this.bgColor; // 初始化 fabric canvas物件 if (!this.canvas) { this.canvas = new fabric.Canvas(this.$refs.canvas, {}); // 設定畫布背景色 (背景色需要這樣設定,否則拓展的橡皮功能會報錯) this.canvas.setBackgroundColor(this.bgColor, undefined, { erasable: false, }); // 設定背景色不受縮放與平移的影響 this.canvas.set("backgroundVpt", false); // 禁止使用者進行組選擇 this.canvas.selection = false; this.canvas.isDrawingMode = true; // 設定當前滑鼠停留在 this.canvas.hoverCursor = "default"; // 重新渲染畫布 this.canvas.renderAll(); // 記錄畫布原始狀態 this.stateArr.push(JSON.stringify(this.canvas)); this.stateIdx = 0; } }, // 初始化畫布事件 initCanvasEvent() { // 操作型別集合 const toolTypes = ["line", "rect", "circle", "text", "move"]; // 監聽滑鼠按下事件 this.canvas.on("mouse:down", (options) => { this.showBgColorPicker = false; this.showStrokeColorPicker = false; if (this.selectTool !== "text" && this.textObject) { // 如果當前存在文字物件,並且不是進行新增文字操作 則 退出編輯模式,並刪除臨時的文字物件 // 將當前文字物件退出編輯模式 this.textObject.exitEditing(); this.textObject.set("backgroundColor", "rgba(0,0,0,0)"); if (this.textObject.text === "") { this.canvas.remove(this.textObject); } this.canvas.renderAll(); this.textObject = null; } // 判斷當前是否選擇了集合中的操作 if (toolTypes.indexOf(this.selectTool) !== -1) { // 記錄當前滑鼠的起點座標 (減去畫布在 x y軸的偏移,因為畫布左上角座標不一定在瀏覽器的視窗左上角) this.mouseFrom.x = options.e.clientX - this.canvas._offset.left; this.mouseFrom.y = options.e.clientY - this.canvas._offset.top; // 判斷當前選擇的工具是否為文字 if (this.selectTool === "text") { // 文字工具初始化 this.initText(); } else { // 設定當前正在進行繪圖 或 移動操作 this.isDrawing = true; } } }); // 監聽滑鼠移動事件 this.canvas.on("mouse:move", (options) => { // 如果當前正在進行繪圖或移動相關操作 if (this.isDrawing) { // 記錄當前滑鼠移動終點座標 (減去畫布在 x y軸的偏移,因為畫布左上角座標不一定在瀏覽器的視窗左上角) this.mouseTo.x = options.e.clientX - this.canvas._offset.left; this.mouseTo.y = options.e.clientY - this.canvas._offset.top; switch (this.selectTool) { case "line": // 當前繪製直線,初始化直線繪製 this.initLine(); break; case "rect": // 初始化 矩形繪製 this.initRect(); break; case "circle": // 初始化 繪製圓形 this.initCircle(); break; case "move": // 初始化畫布移動 this.initMove(); } } }); // 監聽滑鼠鬆開事件 this.canvas.on("mouse:up", () => { // 如果當前正在進行繪圖或移動相關操作 if (this.isDrawing) { // 清空滑鼠移動時儲存的臨時繪圖物件 this.drawingObject = null; // 重置正在繪製圖形標誌 this.isDrawing = false; // 清空滑鼠儲存記錄 this.resetMove(); // 如果當前進行的是移動操作,滑鼠鬆開重置當前視口縮放係數 if (this.selectTool === "move") { this.canvas.setViewportTransform(this.canvas.viewportTransform); } } }); // 監聽畫布渲染完成 this.canvas.on("after:render", () => { if (!this.isRedoing) { // 當前不是進行撤銷或重做操作 // 在繪畫時會頻繁觸發該回調,所以間隔1s記錄當前狀態 if (this.recordTimer) { clearTimeout(this.recordTimer); this.recordTimer = null; } this.recordTimer = setTimeout(() => { this.stateArr.push(JSON.stringify(this.canvas)); this.stateIdx++; }, 100); } else { // 當前正在執行撤銷或重做操作,不記錄重新繪製的畫布 this.isRedoing = false; } }); }, // 初始化畫筆工具 initBruch() { // 設定繪畫模式畫筆型別為 鉛筆型別 this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas); // 設定畫布模式為繪畫模式 this.canvas.isDrawingMode = true; // 設定繪畫模式 畫筆顏色與畫筆線條大小 this.canvas.freeDrawingBrush.color = this.strokeColor; this.canvas.freeDrawingBrush.width = parseInt(this.lineSize, 10); }, // 初始化 繪製直線 initLine() { // 根據儲存的滑鼠起始點座標 建立直線物件 const canvasObject = new fabric.Line( [ this.getTransformedPosX(this.mouseFrom.x), this.getTransformedPosY(this.mouseFrom.y), this.getTransformedPosX(this.mouseTo.x), this.getTransformedPosY(this.mouseTo.y), ], { fill: this.fillColor, stroke: this.strokeColor, strokeWidth: this.lineSize, } ); // 繪製 圖形物件 this.startDrawingObject(canvasObject); }, // 初始化 繪製矩形 initRect() { // 計算矩形長寬 const left = this.getTransformedPosX(this.mouseFrom.x); const top = this.getTransformedPosY(this.mouseFrom.y); const width = this.mouseTo.x - this.mouseFrom.x; const height = this.mouseTo.y - this.mouseFrom.y; // 建立矩形 物件 const canvasObject = new fabric.Rect({ left: left, top: top, width: width, height: height, stroke: this.strokeColor, fill: this.fillColor, strokeWidth: this.lineSize, }); // 繪製矩形 this.startDrawingObject(canvasObject); }, // 初始化繪製圓形 initCircle() { const left = this.getTransformedPosX(this.mouseFrom.x); const top = this.getTransformedPosY(this.mouseFrom.y); // 計算圓形半徑 const radius = Math.sqrt( (this.getTransformedPosX(this.mouseTo.x) - left) * (this.getTransformedPosY(this.mouseTo.x) - left) + (this.getTransformedPosX(this.mouseTo.y) - top) * (this.getTransformedPosY(this.mouseTo.y) - top) ) / 2; // 建立 原型物件 const canvasObject = new fabric.Circle({ left: left, top: top, stroke: this.strokeColor, fill: this.fillColor, radius: radius, strokeWidth: this.lineSize, }); // 繪製圓形物件 this.startDrawingObject(canvasObject); }, // 初始化文字工具 initText() { if (!this.textObject) { // 當前不存在繪製中的文字物件 // 建立文字物件 this.textObject = new fabric.Textbox("", { left: this.getTransformedPosX(this.mouseFrom.x), top: this.getTransformedPosY(this.mouseFrom.y), fontSize: this.fontSize, fill: this.strokeColor, hasControls: false, editable: true, width: 30, backgroundColor: "#fff", selectable: false, }); this.canvas.add(this.textObject); // 文字開啟編輯模式 this.textObject.enterEditing(); // 文字編輯框獲取焦點 this.textObject.hiddenTextarea.focus(); } else { // 將當前文字物件退出編輯模式 this.textObject.exitEditing(); this.textObject.set("backgroundColor", "rgba(0,0,0,0)"); if (this.textObject.text === "") { this.canvas.remove(this.textObject); } this.canvas.renderAll(); this.textObject = null; } }, // 初始化橡皮擦功能 initEraser() { this.canvas.freeDrawingBrush = new fabric.EraserBrush(this.canvas); this.canvas.freeDrawingBrush.width = parseInt(this.lineSize, 10); this.canvas.isDrawingMode = true; }, // 初始化畫布移動 initMove() { var vpt = this.canvas.viewportTransform; vpt[4] += this.mouseTo.x - this.mouseFrom.x; vpt[5] += this.mouseTo.y - this.mouseFrom.y; this.canvas.requestRenderAll(); this.mouseFrom.x = this.mouseTo.x; this.mouseFrom.y = this.mouseTo.y; }, // 繪製圖形 startDrawingObject(canvasObject) { // 禁止使用者選擇當前正在繪製的圖形 canvasObject.selectable = false; // 如果當前圖形已繪製,清除上一次繪製的圖形 if (this.drawingObject) { this.canvas.remove(this.drawingObject); } // 將繪製物件新增到 canvas中 this.canvas.add(canvasObject); // 儲存當前繪製的圖形 this.drawingObject = canvasObject; }, // 清空滑鼠移動記錄 (起點 與 終點) resetMove() { this.mouseFrom = {}; this.mouseTo = {}; }, // 繪圖工具點選選擇 tapToolBtn(tool) { if (this.selectTool === tool) return; // 儲存當前選中的繪圖工具 this.selectTool = tool; // 選擇任何工具前進行一些重置工作 // 禁用畫筆模式 this.canvas.isDrawingMode = false; this.canvas.selection = false; // 禁止圖形選擇編輯 const drawObjects = this.canvas.getObjects(); if (drawObjects.length > 0) { drawObjects.map((item) => { item.set("selectable", false); }); } if (this.selectTool === "brush") { // 如果使用者選擇的是畫筆工具,直接初始化,無需等待使用者進行滑鼠操作 this.initBruch(); } else if (this.selectTool === "eraser") { // 如果使用者選擇的是橡皮擦工具,直接初始化,無需等待使用者進行滑鼠操作 this.initEraser(); } else if (this.selectTool === "select") { this.canvas.selection = true; this.canvas.isDrawingMode = false; if (drawObjects.length > 0) { drawObjects.map((item) => { item.set("selectable", true); }); } } }, // 縮放按鈕點選 tapScaleBtn(flag) { // flag -1 縮小 1 放大 let zoom = this.canvas.getZoom(); if (flag > 0) { // 放大 zoom *= 1.1; } else { // 縮小 zoom *= 0.9; } // zoom 不能大於 20 不能小於0.01 zoom = zoom > 20 ? 20 : zoom; zoom = zoom < 0.01 ? 0.01 : zoom; this.canvas.setZoom(zoom); }, // 撤銷重做按鈕點選 tapHistoryBtn(flag) { this.isRedoing = true; const stateIdx = this.stateIdx + flag; // 判斷是否已經到了第一步操作 if (stateIdx < 0) return; // 判斷是否已經到了最後一步操作 if (stateIdx >= this.stateArr.length) return; if (this.stateArr[stateIdx]) { this.canvas.loadFromJSON(this.stateArr[stateIdx]); if (this.canvas.getObjects().length > 0) { this.canvas.getObjects().forEach((item) => { item.set("selectable", false); }); } this.stateIdx = stateIdx; } }, // 監聽畫布重新繪製 tapClearBtn() { this.$confirm("此操作將清空畫布, 是否繼續?", "提示", { confirmButtonText: "確定", cancelButtonText: "取消", type: "warning", }) .then(() => { const children = this.canvas.getObjects(); if (children.length > 0) { this.canvas.remove(...children); } }) .catch(() => {}); }, tapClearFun() { const children = this.canvas.getObjects(); if (children.length > 0) { this.canvas.remove(...children); } }, // 儲存按鈕點選 tapSaveBtn() { this.canvas.clone((cvs) => { //遍歷所有對物件,獲取最小座標,最大座標 let top = 0; let left = 0; let width = this.canvas.width; let height = this.canvas.height; var objects = cvs.getObjects(); if (objects.length > 0) { var rect = objects[0].getBoundingRect(); var minX = rect.left; var minY = rect.top; var maxX = rect.left + rect.width; var maxY = rect.top + rect.height; for (var i = 1; i < objects.length; i++) { rect = objects[i].getBoundingRect(); minX = Math.min(minX, rect.left); minY = Math.min(minY, rect.top); maxX = Math.max(maxX, rect.left + rect.width); maxY = Math.max(maxY, rect.top + rect.height); } top = minY - 100; left = minX - 100; width = maxX - minX + 200; height = maxY - minY + 200; cvs.sendToBack( new fabric.Rect({ left, top, width, height, stroke: "rgba(0,0,0,0)", fill: this.bgColor, strokeWidth: 0, }) ); } const dataURL = cvs.toDataURL({ format: "png", multiplier: cvs.getZoom(), left, top, width, height, }); // var file = this.dataURLtoFile(dataURL, "index.png"); this.$emit("sendImg", dataURL); // this.tapClearBtn(); const children = this.canvas.getObjects(); if (children.length > 0) { this.canvas.remove(...children); } }); }, // 下載按鈕點選 tapDownBtn() { this.canvas.clone((cvs) => { //遍歷所有對物件,獲取最小座標,最大座標 let top = 0; let left = 0; let width = this.canvas.width; let height = this.canvas.height; var objects = cvs.getObjects(); if (objects.length > 0) { var rect = objects[0].getBoundingRect(); var minX = rect.left; var minY = rect.top; var maxX = rect.left + rect.width; var maxY = rect.top + rect.height; for (var i = 1; i < objects.length; i++) { rect = objects[i].getBoundingRect(); minX = Math.min(minX, rect.left); minY = Math.min(minY, rect.top); maxX = Math.max(maxX, rect.left + rect.width); maxY = Math.max(maxY, rect.top + rect.height); } top = minY - 100; left = minX - 100; width = maxX - minX + 200; height = maxY - minY + 200; cvs.sendToBack( new fabric.Rect({ left, top, width, height, stroke: "rgba(0,0,0,0)", fill: this.bgColor, strokeWidth: 0, }) ); } const dataURL = cvs.toDataURL({ format: "png", multiplier: cvs.getZoom(), left, top, width, height, }); const link = document.createElement("a"); link.download = "canvas.png"; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); }, dataURLtoFile(dataurl, filename) { // 將base64轉換為檔案 var arr = dataurl.split(","); var mime = arr[0].match(/:(.*?);/)[1]; var bstr = atob(arr[1]); var n = bstr.length; var u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new File([u8arr], filename, { type: mime, }); }, // 計算畫布移動之後的x座標點 getTransformedPosX(x) { const zoom = Number(this.canvas.getZoom()); return (x - this.canvas.viewportTransform[4]) / zoom; }, getTransformedPosY(y) { const zoom = Number(this.canvas.getZoom()); return (y - this.canvas.viewportTransform[5]) / zoom; }, }, }; </script> <style lang="scss" scoped> .home { overflow: hidden; height: 100%; width: 100%; position: relative; .btnwrap { position: absolute; bottom: 80px; z-index: 40; width: 100%; height: 50px; display: flex; align-items: center; justify-content: center; .btnList { position: absolute; width: 100%; display: flex; justify-content: center; bottom: -60px; } .btn-color { width: 40px; height: 40px; position: relative; border: 1px solid #999; margin-left: 20px; .color-picker { position: absolute; left: 0; bottom: 40px; z-index: 1000; } } .color-label { padding-left: 4px; } .brushWidth { margin-left: 30px; display: flex; label { display: block; width: 100px; } } .btn-tool { margin: 10px 20px 0; padding-bottom: 10px; color: #a6a6a7; i { font-size: 30px; } &:hover { cursor: pointer; color: #333; } &.active { color: #333; border-bottom: 2px solid #3291ff; } } } .canvas { height: 100%; width: 100%; border: 1px solid #d3d3d3; } } </style>

 JS部分

/* eslint-disable */
(function() {
  /** ERASER_START */
  var __setBgOverlayColor = fabric.StaticCanvas.prototype.__setBgOverlayColor;
  var ___setBgOverlay = fabric.StaticCanvas.prototype.__setBgOverlay;
  var __setSVGBgOverlayColor =
    fabric.StaticCanvas.prototype._setSVGBgOverlayColor;
  fabric.util.object.extend(fabric.StaticCanvas.prototype, {
    backgroundColor: undefined,
    overlayColor: undefined,
    /**
     * Create Rect that holds the color to support erasing
     * patches {@link CommonMethods#_initGradient}
     * @private
     * @param {'bakground'|'overlay'} property
     * @param {(String|fabric.Pattern|fabric.Rect)} color Color or pattern or rect (in case of erasing)
     * @param {Function} callback Callback to invoke when color is set
     * @param {Object} options
     * @return {fabric.Canvas} instance
     * @chainable true
     */
    __setBgOverlayColor: function(property, color, callback, options) {
      if (color && color.isType && color.isType("rect")) {
        // color is already an object
        this[property] = color;
        color.set(options);
        callback && callback(this[property]);
      } else {
        var _this = this;
        var cb = function() {
          _this[property] = new fabric.Rect(
            fabric.util.object.extend(
              {
                width: _this.width,
                height: _this.height,
                fill: _this[property]
              },
              options
            )
          );
          callback && callback(_this[property]);
        };
        __setBgOverlayColor.call(this, property, color, cb);
        //  invoke cb in case of gradient
        //  see {@link CommonMethods#_initGradient}
        if (color && color.colorStops && !(color instanceof fabric.Gradient)) {
          cb();
        }
      }

      return this;
    },

    setBackgroundColor: function(backgroundColor, callback, options) {
      return this.__setBgOverlayColor(
        "backgroundColor",
        backgroundColor,
        callback,
        options
      );
    },

    setOverlayColor: function(overlayColor, callback, options) {
      return this.__setBgOverlayColor(
        "overlayColor",
        overlayColor,
        callback,
        options
      );
    },

    /**
     * patch serialization - from json
     * background/overlay properties could be objects if parsed by this mixin or could be legacy values
     * @private
     * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor)
     * @param {(Object|String)} value Value to set
     * @param {Object} loaded Set loaded property to true if property is set
     * @param {Object} callback Callback function to invoke after property is set
     */
    __setBgOverlay: function(property, value, loaded, callback) {
      var _this = this;

      if (
        (property === "backgroundColor" || property === "overlayColor") &&
        value &&
        typeof value === "object" &&
        value.type === "rect"
      ) {
        fabric.util.enlivenObjects([value], function(enlivedObject) {
          _this[property] = enlivedObject[0];
          loaded[property] = true;
          callback && callback();
        });
      } else {
        ___setBgOverlay.call(this, property, value, loaded, callback);
      }
    },

    /**
     * patch serialization - to svg
     * background/overlay properties could be objects if parsed by this mixin or could be legacy values
     * @private
     */
    _setSVGBgOverlayColor: function(markup, property, reviver) {
      var filler = this[property + "Color"];
      if (filler && filler.isType && filler.isType("rect")) {
        var excludeFromExport =
          filler.excludeFromExport ||
          (this[property] && this[property].excludeFromExport);
        if (filler && !excludeFromExport && filler.toSVG) {
          markup.push(filler.toSVG(reviver));
        }
      } else {
        __setSVGBgOverlayColor.call(this, markup, property, reviver);
      }
    },

    /**
     * @private
     * @param {CanvasRenderingContext2D} ctx Context to render on
     * @param {string} property 'background' or 'overlay'
     */
    _renderBackgroundOrOverlay: function(ctx, property) {
      var fill = this[property + "Color"],
        object = this[property + "Image"],
        v = this.viewportTransform,
        needsVpt = this[property + "Vpt"];
      if (!fill && !object) {
        return;
      }
      if (fill || object) {
        ctx.save();
        if (needsVpt) {
          ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
        }
        fill && fill.render(ctx);
        object && object.render(ctx);
        ctx.restore();
      }
    }
  });

  var _toObject = fabric.Object.prototype.toObject;
  var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup;
  fabric.util.object.extend(fabric.Object.prototype, {
    /**
     * Indicates whether this object can be erased by {@link fabric.EraserBrush}
     * @type boolean
     * @default true
     */
    erasable: true,

    /**
     *
     * @returns {fabric.Group | null}
     */
    getEraser: function() {
      return this.clipPath && this.clipPath.eraser ? this.clipPath : null;
    },

    /**
     * Returns an object representation of an instance
     * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
     * @return {Object} Object representation of an instance
     */
    toObject: function(additionalProperties) {
      return _toObject.call(this, ["erasable"].concat(additionalProperties));
    },

    /**
     * use <mask> to achieve erasing for svg
     * credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649
     * @param {Function} reviver
     * @returns {string} markup
     */
    eraserToSVG: function(options) {
      var eraser = this.getEraser();
      if (eraser) {
        var fill = eraser._objects[0].fill;
        eraser._objects[0].fill = "white";
        eraser.clipPathId = "CLIPPATH_" + fabric.Object.__uid++;
        var commons = [
          'id="' + eraser.clipPathId + '"'
          /*options.additionalTransform ? ' transform="' + options.additionalTransform + '" ' : ''*/
        ].join(" ");
        var objectMarkup = [
          "<defs>",
          "<mask " + commons + " >",
          eraser.toSVG(options.reviver),
          "</mask>",
          "</defs>"
        ];
        eraser._objects[0].fill = fill;
        return objectMarkup.join("\n");
      }
      return "";
    },

    /**
     * use <mask> to achieve erasing for svg, override <clipPath>
     * @param {string[]} objectMarkup
     * @param {Object} options
     * @returns
     */
    _createBaseSVGMarkup: function(objectMarkup, options) {
      var eraser = this.getEraser();
      if (eraser) {
        var eraserMarkup = this.eraserToSVG(options);
        this.clipPath = null;
        var markup = __createBaseSVGMarkup.call(this, objectMarkup, options);
        this.clipPath = eraser;
        return [
          eraserMarkup,
          markup.replace(">", 'mask="url(#' + eraser.clipPathId + ')" >')
        ].join("\n");
      } else {
        return __createBaseSVGMarkup.call(this, objectMarkup, options);
      }
    }
  });

  var _groupToObject = fabric.Group.prototype.toObject;
  fabric.util.object.extend(fabric.Group.prototype, {
    /**
     * Returns an object representation of an instance
     * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
     * @return {Object} Object representation of an instance
     */
    toObject: function(additionalProperties) {
      return _groupToObject.call(this, ["eraser"].concat(additionalProperties));
    }
  });

  fabric.util.object.extend(fabric.Canvas.prototype, {
    /**
     * Used by {@link #renderAll}
     * @returns boolean
     */
    isErasing: function() {
      return (
        this.isDrawingMode &&
        this.freeDrawingBrush &&
        this.freeDrawingBrush.type === "eraser" &&
        this.freeDrawingBrush._isErasing
      );
    },

    /**
     * While erasing, the brush is in charge of rendering the canvas
     * It uses both layers to achieve diserd erasing effect
     *
     * @returns fabric.Canvas
     */
    renderAll: function() {
      if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) {
        this.clearContext(this.contextTop);
        this.contextTopDirty = false;
      }
      // while erasing the brush is in charge of rendering the canvas so we return
      if (this.isErasing()) {
        this.freeDrawingBrush._render();
        return;
      }
      if (this.hasLostContext) {
        this.renderTopLayer(this.contextTop);
      }
      var canvasToDrawOn = this.contextContainer;
      this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender());
      return this;
    }
  });

  /**
   * EraserBrush class
   * Supports selective erasing meaning that only erasable objects are affected by the eraser brush.
   * In order to support selective erasing all non erasable objects are rendered on the main/bottom ctx
   * while the entire canvas is rendered on the top ctx.
   * Canvas bakground/overlay image/color are handled as well.
   * When erasing occurs, the path clips the top ctx and reveals the bottom ctx.
   * This achieves the desired effect of seeming to erase only erasable objects.
   * After erasing is done the created path is added to all intersected objects' `clipPath` property.
   *
   *
   * @class fabric.EraserBrush
   * @extends fabric.PencilBrush
   */
  fabric.EraserBrush = fabric.util.createClass(
    fabric.PencilBrush,
    /** @lends fabric.EraserBrush.prototype */
    {
      type: "eraser",

      /**
       * Indicates that the ctx is ready and rendering can begin.
       * Used to prevent a race condition caused by {@link fabric.EraserBrush#onMouseMove} firing before {@link fabric.EraserBrush#onMouseDown} has completed
       *
       * @private
       */
      _ready: false,

      /**
       * @private
       */
      _drawOverlayOnTop: false,

      /**
       * @private
       */
      _isErasing: false,

      initialize: function(canvas) {
        this.callSuper("initialize", canvas);
        this._renderBound = this._render.bind(this);
        this.render = this.render.bind(this);
      },

      /**
       * Used to hide a drawable from the rendering process
       * @param {fabric.Object} object
       */
      hideObject: function(object) {
        if (object) {
          object._originalOpacity = object.opacity;
          object.set({ opacity: 0 });
        }
      },

      /**
       * Restores hiding an object
       * {@link fabric.EraserBrush#hideObject}
       * @param {fabric.Object} object
       */
      restoreObjectVisibility: function(object) {
        if (object && object._originalOpacity) {
          object.set({ opacity: object._originalOpacity });
          object._originalOpacity = undefined;
        }
      },

      /**
       * Drawing Logic For background drawables: (`backgroundImage`, `backgroundColor`)
       * 1. if erasable = true:
       *    we need to hide the drawable on the bottom ctx so when the brush is erasing it will clip the top ctx and reveal white space underneath
       * 2. if erasable = false:
       *    we need to draw the drawable only on the bottom ctx so the brush won't affect it
       * @param {'bottom' | 'top' | 'overlay'} layer
       */
      prepareCanvasBackgroundForLayer: function(layer) {
        if (layer === "overlay") {
          return;
        }
        var canvas = this.canvas;
        var image = canvas.get("backgroundImage");
        var color = canvas.get("backgroundColor");
        var erasablesOnLayer = layer === "top";
        if (image && image.erasable === !erasablesOnLayer) {
          this.hideObject(image);
        }
        if (color && color.erasable === !erasablesOnLayer) {
          this.hideObject(color);
        }
      },

      /**
       * Drawing Logic For overlay drawables (`overlayImage`, `overlayColor`)
       * We must draw on top ctx to be on top of visible canvas
       * 1. if erasable = true:
       *    we need to draw the drawable on the top ctx as a normal object
       * 2. if erasable = false:
       *    we need to draw the drawable on top of the brush,
       *    this means we need to repaint for every stroke
       *
       * @param {'bottom' | 'top' | 'overlay'} layer
       * @returns boolean render overlay above brush
       */
      prepareCanvasOverlayForLayer: function(layer) {
        var canvas = this.canvas;
        var image = canvas.get("overlayImage");
        var color = canvas.get("overlayColor");
        if (layer === "bottom") {
          this.hideObject(image);
          this.hideObject(color);
          return false;
        }
        var erasablesOnLayer = layer === "top";
        var renderOverlayOnTop =
          (image && !image.erasable) || (color && !color.erasable);
        if (image && image.erasable === !erasablesOnLayer) {
          this.hideObject(image);
        }
        if (color && color.erasable === !erasablesOnLayer) {
          this.hideObject(color);
        }
        return renderOverlayOnTop;
      },

      /**
       * @private
       */
      restoreCanvasDrawables: function() {
        var canvas = this.canvas;
        this.restoreObjectVisibility(canvas.get("backgroundImage"));
        this.restoreObjectVisibility(canvas.get("backgroundColor"));
        this.restoreObjectVisibility(canvas.get("overlayImage"));
        this.restoreObjectVisibility(canvas.get("overlayColor"));
      },

      /**
       * @private
       * This is designed to support erasing a group with both erasable and non-erasable objects.
       * Iterates over collections to allow nested selective erasing.
       * Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer}
       * to prepare the bottom layer by hiding erasable nested objects
       *
       * @param {fabric.Collection} collection
       */
      prepareCollectionTraversal: function(collection) {
        var _this = this;
        collection.forEachObject(function(obj) {
          if (obj.forEachObject) {
            _this.prepareCollectionTraversal(obj);
          } else {
            if (obj.erasable) {
              _this.hideObject(obj);
            }
          }
        });
      },

      /**
       * @private
       * Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer}
       * to reverse the action of {@link fabric.EraserBrush#prepareCollectionTraversal}
       *
       * @param {fabric.Collection} collection
       */
      restoreCollectionTraversal: function(collection) {
        var _this = this;
        collection.forEachObject(function(obj) {
          if (obj.forEachObject) {
            _this.restoreCollectionTraversal(obj);
          } else {
            _this.restoreObjectVisibility(obj);
          }
        });
      },

      /**
       * @private
       * This is designed to support erasing a group with both erasable and non-erasable objects.
       *
       * @param {'bottom' | 'top' | 'overlay'} layer
       */
      prepareCanvasObjectsForLayer: function(layer) {
        if (layer !== "bottom") {
          return;
        }
        this.prepareCollectionTraversal(this.canvas);
      },

      /**
       * @private
       * @param {'bottom' | 'top' | 'overlay'} layer
       */
      restoreCanvasObjectsFromLayer: function(layer) {
        if (layer !== "bottom") {
          return;
        }
        this.restoreCollectionTraversal(this.canvas);
      },

      /**
       * @private
       * @param {'bottom' | 'top' | 'overlay'} layer
       * @returns boolean render overlay above brush
       */
      prepareCanvasForLayer: function(layer) {
        this.prepareCanvasBackgroundForLayer(layer);
        this.prepareCanvasObjectsForLayer(layer);
        return this.prepareCanvasOverlayForLayer(layer);
      },

      /**
       * @private
       * @param {'bottom' | 'top' | 'overlay'} layer
       */
      restoreCanvasFromLayer: function(layer) {
        this.restoreCanvasDrawables();
        this.restoreCanvasObjectsFromLayer(layer);
      },

      /**
       * Render all non-erasable objects on bottom layer with the exception of overlays to avoid being clipped by the brush.
       * Groups are rendered for nested selective erasing, non-erasable objects are visible while erasable objects are not.
       */
      renderBottomLayer: function() {
        var canvas = this.canvas;
        this.prepareCanvasForLayer("bottom");
        canvas.renderCanvas(
          canvas.getContext(),
          canvas.getObjects().filter(function(obj) {
            return !obj.erasable || obj.isType("group");
          })
        );
        this.restoreCanvasFromLayer("bottom");
      },

      /**
       * 1. Render all objects on top layer, erasable and non-erasable
       *    This is important for cases such as overlapping objects, the background object erasable and the foreground object not erasable.
       * 2. Render the brush
       */
      renderTopLayer: function() {
        var canvas = this.canvas;
        this._drawOverlayOnTop = this.prepareCanvasForLayer("top");
        canvas.renderCanvas(canvas.contextTop, canvas.getObjects());
        this.callSuper("_render");
        this.restoreCanvasFromLayer("top");
      },

      /**
       * Render all non-erasable overlays on top of the brush so that they won't get erased
       */
      renderOverlay: function() {
        this.prepareCanvasForLayer("overlay");
        var canvas = this.canvas;
        var ctx = canvas.contextTop;
        this._saveAndTransform(ctx);
        canvas._renderOverlay(ctx);
        ctx.restore();
        this.restoreCanvasFromLayer("overlay");
      },

      /**
       * @extends @class fabric.BaseBrush
       * @param {CanvasRenderingContext2D} ctx
       */
      _saveAndTransform: function(ctx) {
        this.callSuper("_saveAndTransform", ctx);
        ctx.globalCompositeOperation = "destination-out";
      },

      /**
       * We indicate {@link fabric.PencilBrush} to repaint itself if necessary
       * @returns
       */
      needsFullRender: function() {
        return this.callSuper("needsFullRender") || this._drawOverlayOnTop;
      },

      /**
       *
       * @param {fabric.Point} pointer
       * @param {fabric.IEvent} options
       * @returns
       */
      onMouseDown: function(pointer, options) {
        if (!this.canvas._isMainEvent(options.e)) {
          return;
        }
        this._prepareForDrawing(pointer);
        // capture coordinates immediately
        // this allows to draw dots (when movement never occurs)
        this._captureDrawingPath(pointer);

        this._isErasing = true;
        this.canvas.fire("erasing:start");
        this._ready = true;
        this._render();
      },

      /**
       * Rendering is done in 4 steps:
       * 1. Draw all non-erasable objects on bottom ctx with the exception of overlays {@link fabric.EraserBrush#renderBottomLayer}
       * 2. Draw all objects on top ctx including erasable drawables {@link fabric.EraserBrush#renderTopLayer}
       * 3. Draw eraser {@link fabric.PencilBrush#_render} at {@link fabric.EraserBrush#renderTopLayer}
       * 4. Draw non-erasable overlays {@link fabric.EraserBrush#renderOverlay}
       *
       * @param {fabric.Canvas} canvas
       */
      _render: function() {
        if (!this._ready) {
          return;
        }
        this.isRendering = 1;
        this.renderBottomLayer();
        this.renderTopLayer();
        this.renderOverlay();
        this.isRendering = 0;
      },

      /**
       * @public
       */
      render: function() {
        if (this._isErasing) {
          if (this.isRendering) {
            this.isRendering = fabric.util.requestAnimFrame(this._renderBound);
          } else {
            this._render();
          }
          return true;
        }
        return false;
      },

      /**
       * Adds path to existing clipPath of object
       *
       * @param {fabric.Object} obj
       * @param {fabric.Path} path
       */
      _addPathToObjectEraser: function(obj, path) {
        var clipObject;
        var _this = this;
        //  object is collection, i.e group
        if (obj.forEachObject) {
          obj.forEachObject(function(_obj) {
            if (_obj.erasable) {
              _this._addPathToObjectEraser(_obj, path);
            }
          });
          return;
        }
        if (!obj.getEraser()) {
          var size = obj._getNonTransformedDimensions();
          var rect = new fabric.Rect({
            width: size.x,
            height: size.y,
            clipPath: obj.clipPath,
            originX: "center",
            originY: "center"
          });
          clipObject = new fabric.Group([rect], {
            eraser: true
          });
        } else {
          clipObject = obj.clipPath;
        }

        path.clone(function(path) {
          path.globalCompositeOperation = "destination-out";
          // http://fabricjs.com/using-transformations
          var desiredTransform = fabric.util.multiplyTransformMatrices(
            fabric.util.invertTransform(obj.calcTransformMatrix()),
            path.calcTransformMatrix()
          );
          fabric.util.applyTransformToObject(path, desiredTransform);
          clipObject.addWithUpdate(path);
          obj.set({
            clipPath: clipObject,
            dirty: true
          });
        });
      },

      /**
       * Add the eraser path to canvas drawables' clip paths
       *
       * @param {fabric.Canvas} source
       * @param {fabric.Canvas} path
       * @returns {Object} canvas drawables that were erased by the path
       */
      applyEraserToCanvas: function(path) {
        var canvas = this.canvas;
        var drawables = {};
        [
          "backgroundImage",
          "backgroundColor",
          "overlayImage",
          "overlayColor"
        ].forEach(function(prop) {
          var drawable = canvas[prop];
          if (drawable && drawable.erasable) {
            this._addPathToObjectEraser(drawable, path);
            drawables[prop] = drawable;
          }
        }, this);
        return drawables;
      },

      /**
       * On mouseup after drawing the path on contextTop canvas
       * we use the points captured to create an new fabric path object
       * and add it to every intersected erasable object.
       */
      _finalizeAndAddPath: function() {
        var ctx = this.canvas.contextTop,
          canvas = this.canvas;
        ctx.closePath();
        if (this.decimate) {
          this._points = this.decimatePoints(this._points, this.decimate);
        }

        // clear
        canvas.clearContext(canvas.contextTop);
        this._isErasing = false;

        var pathData =
          this._points && this._points.length > 1
            ? this.convertPointsToSVGPath(this._points).join("")
            : "M 0 0 Q 0 0 0 0 L 0 0";
        if (pathData === "M 0 0 Q 0 0 0 0 L 0 0") {
          canvas.fire("erasing:end");
          // do not create 0 width/height paths, as they are
          // rendered inconsistently across browsers
          // Firefox 4, for example, renders a dot,
          // whereas Chrome 10 renders nothing
          canvas.requestRenderAll();
          return;
        }

        var path = this.createPath(pathData);
        canvas.fire("before:path:created", { path: path });

        // finalize erasing
        var drawables = this.applyEraserToCanvas(path);
        var _this = this;
        var targets = [];
        canvas.forEachObject(function(obj) {
          if (obj.erasable && obj.intersectsWithObject(path, true)) {
            _this._addPathToObjectEraser(obj, path);
            targets.push(obj);
          }
        });

        canvas.fire("erasing:end", {
          path: path,
          targets: targets,
          drawables: drawables
        });

        canvas.requestRenderAll();
        path.setCoords();
        this._resetShadow();

        // fire event 'path' created
        canvas.fire("path:created", { path: path });
      }
    }
  );

  /** ERASER_END */
})();

  元件得引用

  

 <drawers ref="drawers" @sendImg="imageSend"></drawers>

import drawers from "../../components/DrawingBoard/index.vue";

components: {
    drawers,
  },

imageSend(val) {  接收簽字生成得圖片
      this.attendPersonList[this.rowDrawersVal].signImage = val;
      this.isDrawers = false;
    },