实现签字功能
封装组件,实现签字画板功能
<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; },