vue项目中基于fabric 插件实现涂鸦画布功能
- 一、效果图
- 二、安装依赖
- 三、main.js引入
- 四、主要代码
一、效果图
二、安装依赖
npm install fabric
三、main.js引入
import fabric from 'fabric'
Vue.use(fabric);
四、主要代码
//封装成了一个组件
<template><el-dialogtitle="涂鸦生图":visible="visible"custom-class="doodleDialog"@close="handleClose"@open="openDialog"width="1500px"><div style="display: flex; justify-content: space-between"><div class="rigth"><p style="font-size: 16px">涂鸦区</p><div class="maintenancePlanAdd"><div class="child-panel-title"></div><div class="panel-body"><div class="demo"><canvas id="canvas" :width="width" :height="height"></canvas><div class="draw-btn-group"><div:class="{ active: drawType == '' }"title="自由选择"@click="drawTypeChange('')"><i class="draw-icon icon-mouse"></i></div><div:class="{ active: drawType == 'arrow' }"title="画箭头"@click="drawTypeChange('arrow')"><i class="draw-icon icon-1"></i></div><div:class="{ active: drawType == 'text' }"title="文本输入框"@click="drawTypeChange('text')"><i class="draw-icon icon-2"></i></div><div:class="{ active: drawType == 'ellipse' }"title="画圆"@click="drawTypeChange('ellipse')"><i class="draw-icon icon-3"></i></div><div:class="{ active: drawType == 'rectangle' }"title="画矩形"@click="drawTypeChange('rectangle')"><i class="draw-icon icon-4"></i></div><div:class="{ active: drawType == 'polygon' }"title="画多边形"@click="drawPolygon"><i class="draw-icon icon-6"></i></div><div:class="{ active: drawType == 'pen' }"title="笔画"@click="drawTypeChange('pen')"><i class="draw-icon icon-7"></i></div><div:class="{ active: drawType == 'pentagram' }"title="五角星"@click="drawTypeChange('pentagram')"><i class="draw-icon icon-pentagram"></i></div><div:class="{ active: drawType == 'delete' }"title="删除"@click="drawTypeDelete()"><i style="font-size: 26px" class="el-icon-delete"></i></div><!-- <div @click="uploadImg" title="从文件选择图片上传"><i class="draw-icon icon-img"></i></div><div @click="loadExpImg" title="加载背景图"><i class="draw-icon icon-back"></i></div><div @click="save" title="保存"><i class="draw-icon icon-save"></i></div> --></div></div></div><input type="file" @change="uploadImgChange" id="imgInput" accept="image/*" /><img id="img" :src="imgSrc" /><img id="expImg" src="../../../assets/images/draw/exp.jpg" /></div></div><div class="left" style="width: 600px"><p style="font-size: 16px">生成区</p><div style="border: 1px dashed black"><p style="text-align: center; margin-top: 5px">生成图片如下:</p><div style="width: 598px; height: 400px; margin-top: 18px; margin-bottom: 58px"><imgv-if="resultImg"style="width: 598px; height: 400px; display: inline-block":src="resultImg"alt=""/></div><el-formclass="screenwaper":model="addInnerFrom":rules="addRules"ref="addInnerFrom"label-width="90px"label-position="rigth"><el-form-item label="文字描述:" prop="prompt"><el-inputstyle="width: 500px"v-model="addInnerFrom.prompt"placeholder="请输入"size="small"type="textarea":rows="2"></el-input></el-form-item><el-form-item label="相似度:" prop="similarity"><el-sliderstyle="width: 500px"v-model="addInnerFrom.similarity":format-tooltip="formatTooltip"></el-slider></el-form-item><p style="text-align: center; margin: 20px 0 30px"><el-buttonsize="small"type="primary":loading="loading"@click="handleSureDialog('addInnerFrom')">确定生成</el-button></p></el-form></div></div></div><div style="text-align: center; padding: 20px 0 0"><el-button size="small" @click="handleSure">关 闭</el-button></div></el-dialog>
</template><script>
import { fabric } from 'fabric';
import { doodleImg } from '../api';
export default {name: 'doodleDialog',props: {visible: {type: Boolean,default: false,},},data() {return {addInnerFrom: { prompt: '', imageUploadData: '', similarity: 0 },addRules: {prompt: [{ required: true, message: '请输入标题', trigger: 'blur' }],similarity: [{ required: true, message: '请输入文本描述', trigger: 'blur' }],},loading: false,width: 800,height: 700,rect: [],canvas: {},showMenu: false,x: '',y: '',mouseFrom: {},mouseTo: {},drawType: null, //当前绘制图像的种类canvasObjectIndex: 0,textbox: null,rectangleLabel: 'warning',drawWidth: 2, //笔触宽度color: '#E34F51', //画笔颜色drawingObject: null, //当前绘制对象moveCount: 1, //绘制移动计数器doDrawing: false, // 绘制状态//polygon 相关参数polygonMode: false,pointArray: [],lineArray: [],activeShape: false,activeLine: '',line: {},delectKlass: {},imgFile: {},imgSrc: '',resultImg: '',};},watch: {drawType() {this.canvas.selection = !this.drawType;},width() {this.canvas.setWidth(this.width);},height() {this.canvas.setHeight(this.height);},},methods: {formatTooltip(val) {return val / 100;},openDialog() {this.resultImg = '';this.loading = false;this.addInnerFrom = { prompt: '', imageUploadData: '', similarity: 0 };this.$nextTick(() => {this.canvas = new fabric.Canvas('canvas', {// skipTargetFind: false, //当为真时,跳过目标检测。目标检测将返回始终未定义。点击选择将无效// selectable: false, //为false时,不能选择对象进行修改// selection: false // 是否可以多个对象为一组});this.canvas.selectionColor = 'rgba(0,0,0,0.05)';this.canvas.on('mouse:down', this.mousedown);this.canvas.on('mouse:move', this.mousemove);this.canvas.on('mouse:up', this.mouseup);document.onkeydown = e => {// 键盘 delect删除所选元素if (e.keyCode == 46) {this.deleteObj();}// ctrl+z 删除最近添加的元素if (e.keyCode == 90 && e.ctrlKey) {this.canvas.remove(this.canvas.getObjects()[this.canvas.getObjects().length - 1]);}};});},// 画布下面删除按钮drawTypeDelete() {this.drawType = 'delete';this.canvas.clear();},handleClose() {this.canvas.clear();this.$emit('DialogCancel');},handleSure() {this.handleClose();this.$emit('DialogOk', this.resultImg);},handleSureDialog(From) {this.$refs[From].validate(valid => {if (valid) {this.loading = true;let canvas = document.getElementById('canvas');this.addInnerFrom.imageUploadData = canvas.toDataURL('png');doodleImg({imageFile: this.addInnerFrom.imageUploadData,prompt: this.addInnerFrom.prompt,similarity: this.formatTooltip(this.addInnerFrom.similarity),}).then(({ data }) => {if (data.length) {this.loading = false;this.resultImg = data[0];this.$message({showClose: true,message: `已成功生成图片${data.length}张`,type: 'success',});}});} else {console.log('error submit!!');return false;}});},// 保存当前画布为png图片save() {let canvas = document.getElementById('canvas');let imgData = canvas.toDataURL('png');console.log(imgData, 'wwww');imgData = imgData.replace('image/png', 'image/octet-stream');// 下载后的问题名,可自由指定let filename = 'drawingboard_' + new Date().getTime() + '.' + 'png';this.saveFile(imgData, filename);},saveFile(data, filename) {let save_link = document.createElement('a');save_link.href = data;save_link.download = filename;let event = document.createEvent('MouseEvents');event.initMouseEvent('click',true,false,window,0,0,0,0,0,false,false,false,false,0,null);save_link.dispatchEvent(event);},uploadImg() {document.getElementById('imgInput').click();},// 从已渲染的DOM元素加载图片至canvasloadExpImg() {let imgElement = document.getElementById('expImg'); //声明我们的图片let imgInstance = new fabric.Image(imgElement, {selectable: false,// zIndex:-99,});this.canvas.add(imgInstance);},// 从文件加载图片至canvasuploadImgChange() {// 获取文件let eleImportInput = document.getElementById('imgInput');this.imgFile = eleImportInput.files[0];let imgSrc = '',imgTitle = '';// 从reader中获取选择文件的srcif (/\.(jpe?g|png|gif)$/i.test(this.imgFile.name)) {let reader = new FileReader();let _this = this;reader.addEventListener('load',function () {imgTitle = _this.imgFile.name;_this.imgSrc = this.result;},false);reader.readAsDataURL(this.imgFile);}let imgElement = document.getElementById('img'); //声明我们的图片imgElement.onload = () => {this.width = imgElement.width;this.height = imgElement.height;let imgInstance = new fabric.Image(imgElement, {zIndex: -1,selectable: false,});this.canvas.add(imgInstance);};},// 开始绘制时,指定绘画种类drawTypeChange(e) {this.drawType = e;this.canvas.skipTargetFind = !!e;if (e == 'pen') {// isDrawingMode为true 才可以自由绘画this.canvas.isDrawingMode = true;} else {this.canvas.isDrawingMode = false;}},// 鼠标按下时触发mousedown(e) {// 记录鼠标按下时的坐标let xy = e.pointer || this.transformMouse(e.e.offsetX, e.e.offsetY);this.mouseFrom.x = xy.x;this.mouseFrom.y = xy.y;this.doDrawing = true;if (this.drawType == 'text') {this.drawing();}if (this.textbox) {this.textbox.enterEditing();this.textbox.hiddenTextarea.focus();}// 绘制多边形if (this.drawType == 'polygon') {this.canvas.skipTargetFind = false;try {// 此段为判断是否闭合多边形,点击红点时闭合多边形if (this.pointArray.length > 1) {// e.target.id == this.pointArray[0].id 表示点击了初始红点if (e.target && e.target.id == this.pointArray[0].id) {this.generatePolygon();}}//未点击红点则继续作画if (this.polygonMode) {this.addPoint(e);}} catch (error) {console.log(error);}}},// 鼠标松开执行mouseup(e) {let xy = e.pointer || this.transformMouse(e.e.offsetX, e.e.offsetY);this.mouseTo.x = xy.x;this.mouseTo.y = xy.y;this.drawingObject = null;this.moveCount = 1;if (this.drawType != 'polygon') {this.doDrawing = false;}},//鼠标移动过程中已经完成了绘制mousemove(e) {if (this.moveCount % 2 && !this.doDrawing) {//减少绘制频率return;}this.moveCount++;let xy = e.pointer || this.transformMouse(e.e.offsetX, e.e.offsetY);this.mouseTo.x = xy.x;this.mouseTo.y = xy.y;// 多边形与文字框特殊处理if (this.drawType != 'text' || this.drawType != 'polygon') {this.drawing(e);}if (this.drawType == 'polygon') {if (this.activeLine && this.activeLine.class == 'line') {let pointer = this.canvas.getPointer(e.e);this.activeLine.set({ x2: pointer.x, y2: pointer.y });let points = this.activeShape.get('points');points[this.pointArray.length] = {x: pointer.x,y: pointer.y,zIndex: 1,};this.activeShape.set({points: points,});this.canvas.renderAll();}this.canvas.renderAll();}},deleteObj() {this.canvas.getActiveObjects().map(item => {this.canvas.remove(item);});},transformMouse(mouseX, mouseY) {return { x: mouseX / 1, y: mouseY / 1 };},// 绘制多边形开始,绘制多边形和其他图形不一样,需要单独处理drawPolygon() {this.drawType = 'polygon';this.polygonMode = true;//这里画的多边形,由顶点与线组成this.pointArray = new Array(); // 顶点集合this.lineArray = new Array(); //线集合this.canvas.isDrawingMode = false;},addPoint(e) {let random = Math.floor(Math.random() * 10000);let id = new Date().getTime() + random;let circle = new fabric.Circle({radius: 5,fill: '#ffffff',stroke: '#333333',strokeWidth: 0.5,left: (e.pointer.x || e.e.layerX) / this.canvas.getZoom(),top: (e.pointer.y || e.e.layerY) / this.canvas.getZoom(),selectable: false,hasBorders: false,hasControls: false,originX: 'center',originY: 'center',id: id,objectCaching: false,});if (this.pointArray.length == 0) {circle.set({fill: 'red',});}let points = [(e.pointer.x || e.e.layerX) / this.canvas.getZoom(),(e.pointer.y || e.e.layerY) / this.canvas.getZoom(),(e.pointer.x || e.e.layerX) / this.canvas.getZoom(),(e.pointer.y || e.e.layerY) / this.canvas.getZoom(),];this.line = new fabric.Line(points, {strokeWidth: 2,fill: '#999999',stroke: '#999999',class: 'line',originX: 'center',originY: 'center',selectable: false,hasBorders: false,hasControls: false,evented: false,objectCaching: false,});if (this.activeShape) {let pos = this.canvas.getPointer(e.e);let points = this.activeShape.get('points');points.push({x: pos.x,y: pos.y,});let polygon = new fabric.Polygon(points, {stroke: '#333333',strokeWidth: 1,fill: '#cccccc',opacity: 0.3,selectable: false,hasBorders: false,hasControls: false,evented: false,objectCaching: false,});this.canvas.remove(this.activeShape);this.canvas.add(polygon);this.activeShape = polygon;this.canvas.renderAll();} else {let polyPoint = [{x: (e.pointer.x || e.e.layerX) / this.canvas.getZoom(),y: (e.pointer.y || e.e.layerY) / this.canvas.getZoom(),},];let polygon = new fabric.Polygon(polyPoint, {stroke: '#333333',strokeWidth: 1,fill: '#cccccc',opacity: 0.3,selectable: false,hasBorders: false,hasControls: false,evented: false,objectCaching: false,});this.activeShape = polygon;this.canvas.add(polygon);}this.activeLine = this.line;this.pointArray.push(circle);this.lineArray.push(this.line);this.canvas.add(this.line);this.canvas.add(circle);},generatePolygon() {let points = new Array();this.pointArray.map((point, index) => {points.push({x: point.left,y: point.top,});this.canvas.remove(point);});this.lineArray.map((line, index) => {this.canvas.remove(line);});this.canvas.remove(this.activeShape).remove(this.activeLine);let polygon = new fabric.Polygon(points, {stroke: this.color,strokeWidth: this.drawWidth,fill: 'rgba(255, 255, 255, 0)',opacity: 1,hasBorders: true,hasControls: false,});this.canvas.add(polygon);this.activeLine = null;this.activeShape = null;this.polygonMode = false;this.doDrawing = false;this.drawType = null;},drawing(e) {if (this.drawingObject) {this.canvas.remove(this.drawingObject);}let canvasObject = null;let left = this.mouseFrom.x,top = this.mouseFrom.y,mouseFrom = this.mouseFrom,mouseTo = this.mouseTo;switch (this.drawType) {case 'arrow':{//箭头let x1 = mouseFrom.x,x2 = mouseTo.x,y1 = mouseFrom.y,y2 = mouseTo.y;let w = x2 - x1,h = y2 - y1,sh = Math.cos(Math.PI / 4) * 16;let sin = h / Math.sqrt(Math.pow(w, 2) + Math.pow(h, 2));let cos = w / Math.sqrt(Math.pow(w, 2) + Math.pow(h, 2));let w1 = (16 * sin) / 4,h1 = (16 * cos) / 4,centerx = sh * cos,centery = sh * sin;/*** centerx,centery 表示起始点,终点连线与箭头尖端等边三角形交点相对x,y* w1 ,h1用于确定四个点*/let path = ' M ' + x1 + ' ' + y1;path += ' L ' + (x2 - centerx + w1) + ' ' + (y2 - centery - h1);path += ' L ' + (x2 - centerx + w1 * 2) + ' ' + (y2 - centery - h1 * 2);path += ' L ' + x2 + ' ' + y2;path += ' L ' + (x2 - centerx - w1 * 2) + ' ' + (y2 - centery + h1 * 2);path += ' L ' + (x2 - centerx - w1) + ' ' + (y2 - centery + h1);path += ' Z';canvasObject = new fabric.Path(path, {stroke: this.color,fill: this.color,strokeWidth: this.drawWidth,});}break;case 'pentagram':{//五角星let x1 = mouseFrom.x,x2 = mouseTo.x,y1 = mouseFrom.y,y2 = mouseTo.y;/*** 实现思路 (x1,y1)表示鼠标起始的位置 (x2,y2)表示鼠标抬起的位置* r 表示五边形外圈圆的半径,这里建议自己画个图理解* 正五边形夹角为36度。计算出cos18°,sin18°备用*/let w = Math.abs(x2 - x1),h = Math.abs(y2 - y1),r = Math.sqrt(w * w + h * h);let cos18 = Math.cos((18 * Math.PI) / 180);let sin18 = Math.sin((18 * Math.PI) / 180);/*** 算出对应五个点的坐标转化为路径*/let point1 = [x1, y1 + r];let point2 = [x1 + 2 * r * sin18, y1 + r - 2 * r * cos18];let point3 = [x1 - r * cos18, y1 + r * sin18];let point4 = [x1 + r * cos18, y1 + r * sin18];let point5 = [x1 - 2 * r * sin18, y1 + r - 2 * r * cos18];let path = ' M ' + point1[0] + ' ' + point1[1];path += ' L ' + point2[0] + ' ' + point2[1];path += ' L ' + point3[0] + ' ' + point3[1];path += ' L ' + point4[0] + ' ' + point4[1];path += ' L ' + point5[0] + ' ' + point5[1];path += ' Z';canvasObject = new fabric.Path(path, {stroke: this.color,fill: this.color,strokeWidth: this.drawWidth,// angle:180, //设置旋转角度});}break;case 'ellipse':{//椭圆// 按shift时画正圆,只有在鼠标移动时才执行这个,所以按了shift但是没有拖动鼠标将不会画圆if (e.e.shiftKey) {mouseTo.x - left > mouseTo.y - top? (mouseTo.y = top + mouseTo.x - left): (mouseTo.x = left + mouseTo.y - top);}let radius =Math.sqrt((mouseTo.x - left) * (mouseTo.x - left) +(mouseTo.y - top) * (mouseTo.y - top)) / 2;canvasObject = new fabric.Ellipse({left: (mouseTo.x - left) / 2 + left,top: (mouseTo.y - top) / 2 + top,stroke: this.color,fill: 'rgba(255, 255, 255, 0)',originX: 'center',originY: 'center',rx: Math.abs(left - mouseTo.x) / 2,ry: Math.abs(top - mouseTo.y) / 2,strokeWidth: this.drawWidth,});}break;case 'rectangle':{//长方形// 按shift时画正方型if (e.e.shiftKey) {mouseTo.x - left > mouseTo.y - top? (mouseTo.y = top + mouseTo.x - left): (mouseTo.x = left + mouseTo.y - top);}let path ='M ' +mouseFrom.x +' ' +mouseFrom.y +' L ' +mouseTo.x +' ' +mouseFrom.y +' L ' +mouseTo.x +' ' +mouseTo.y +' L ' +mouseFrom.x +' ' +mouseTo.y +' L ' +mouseFrom.x +' ' +mouseFrom.y +' z';canvasObject = new fabric.Path(path, {left: left,top: top,stroke: this.color,strokeWidth: this.drawWidth,fill: 'rgba(255, 255, 255, 0)',hasControls: false,});}//也可以使用fabric.Rectbreak;case 'text':{//文本框this.textbox = new fabric.Textbox('', {left: mouseFrom.x,top: mouseFrom.y - 10,// width: 150,fontSize: 16,borderColor: this.color,fill: this.color,hasControls: false,});this.canvas.add(this.textbox);this.textbox.enterEditing();this.textbox.hiddenTextarea.focus();}break;default:break;}if (canvasObject) {// canvasObject.index = getCanvasObjectIndex();\this.canvas.add(canvasObject); //.setActiveObject(canvasObject)this.drawingObject = canvasObject;}},},
};
</script><style lang="scss" scope>
.doodleDialog {.el-container {flex-direction: column;}img,input {display: none;}.demo {display: flex;flex-direction: column;align-items: center;}canvas {border: 1px dashed black;}.draw-btn-group {// width: 1270px;margin-top: 10px;display: flex;align-items: center;justify-content: flex-start;& > div {background: #fafafa;cursor: pointer;&:hover {background: #eee;}i {display: flex;background-repeat: no-repeat;background-size: 80%;background-position: 50% 50%;height: 30px;width: 30px;}.icon-1 {background-image: url('../../../assets/images/draw/1.png');}.icon-pentagram {background-image: url('../../../assets/images/draw/pentagram.png');}.icon-2 {background-image: url('../../../assets/images/draw/2.png');}.icon-3 {background-image: url('../../../assets/images/draw/3.png');}.icon-4 {background-image: url('../../../assets/images/draw/4.png');background-size: 75%;}.icon-5 {background-image: url('../../../assets/images/draw/5.png');background-size: 70%;}.icon-6 {background-image: url('../../../assets/images/draw/6.png');}.icon-7 {background-image: url('../../../assets/images/draw/7.png');background-size: 80%;}.icon-del {background-image: url('../../../assets/images/draw/del.png');background-size: 90%;}.icon-img {background-image: url('../../../assets/images/draw/img.png');background-size: 80%;}.icon-back {background-image: url('../../../assets/images/draw/back.png');background-size: 75%;}.icon-save {background-image: url('../../../assets/images/draw/save.png');background-size: 80%;}.icon-mouse {background-image: url('../../../assets/images/draw/mouse.png');background-size: 60%;}}.active {background: #eee;}}
}
</style>
<style lang="scss"></style>
链接: https://www.jianshu.com/p/d6d924eb5cf7
链接: https://github.com/Couy69/vue-fabric-drawingboard