vue项目中基于fabric 插件实现涂鸦画布功能
- 一、效果图
- 二、安装依赖
- 三、main.js引入
- 四、主要代码
一、效果图
二、安装依赖
npm install fabric
三、main.js引入
import fabric from 'fabric'
Vue.use(fabric);
四、主要代码
//封装成了一个组件
<template>
<el-dialog
title="涂鸦生图"
: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">
<img
v-if="resultImg"
style="width: 598px; height: 400px; display: inline-block"
:src="resultImg"
alt=""
/>
</div>
<el-form
class="screenwaper"
:model="addInnerFrom"
:rules="addRules"
ref="addInnerFrom"
label-width="90px"
label-position="rigth"
>
<el-form-item label="文字描述:" prop="prompt">
<el-input
style="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-slider
style="width: 500px"
v-model="addInnerFrom.similarity"
:format-tooltip="formatTooltip"
></el-slider>
</el-form-item>
<p style="text-align: center; margin: 20px 0 30px">
<el-button
size="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元素加载图片至canvas
loadExpImg() {
let imgElement = document.getElementById('expImg'); //声明我们的图片
let imgInstance = new fabric.Image(imgElement, {
selectable: false,
// zIndex:-99,
});
this.canvas.add(imgInstance);
},
// 从文件加载图片至canvas
uploadImgChange() {
// 获取文件
let eleImportInput = document.getElementById('imgInput');
this.imgFile = eleImportInput.files[0];
let imgSrc = '',
imgTitle = '';
// 从reader中获取选择文件的src
if (/\.(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.Rect
break;
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