實現簽字功能
阿新 • • 發佈:2022-05-21
封裝元件,實現簽字畫板功能
<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; },