The vue project uses canvas to implement the handwriting pad function
renderings
ComponentCanvasDialog.vue
Upload the code directly. The following code can be directly referenced as a component. You can upload the corresponding image according to your own needs. The operation icon needs to be replaced by yourself, and the saving function also needs to be implemented by yourself.
<template>
<el-dialog
:visible="true"
title="图片编辑"
style="font-size: 18px"
width="1400px"
:close-on-click-modal="false"
@close="closeDialog"
append-to-body
>
<div class="modal-body">
<div class="container">
<canvas height="570"
id="canvas"
ref="canvas"
width="940"></canvas>
<div class="tool-container">
<div class="icon-div icon" @click="isShowDrawPane = !isShowDrawPane">
<!-- 举例子:svg-icon可换成<i class="el-icon-delete"></i> -->
<svg-icon icon-class="draw" scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="filterObject('erase')">
<svg-icon icon-class="erase" scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="filterObject('undefined')">
<svg-icon icon-class="ziyoubi" scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="filterObject('line')">
<svg-icon icon-class="line" scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="filterObject('arrows')">
<svg-icon icon-class="arrows" scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="filterObject('rect')">
<svg-icon icon-class="rect" scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="filterObject('circle')">
<svg-icon icon-class="circle" scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="filterObject('text')">
<svg-icon icon-class="text" scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="clearCanvas()">
<svg-icon icon-class="clear" scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="redo()">
<svg-icon :icon-class="historyImageData.length > 0 ? 'redo' : 'grey-redo' " scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="cancelRedo()">
<svg-icon :icon-class="newHistoryImageData.length > 0 ? 'cancelRedo' : 'grey-cancelRedo' " scale="4"></svg-icon>
</div>
<div class="icon-div icon" @click="downLoad()">
<svg-icon icon-class="download" scale="4"></svg-icon>
</div>
<div class="drawPane" v-show="isShowDrawPane">
<div @click="isShowDrawPane = false">
<svg-icon icon-class="close" class="close-draw-pane icon" scale="3"></svg-icon>
</div>
<div class="colorClass">画笔大小</div>
<input type="range" id="lwRange" min="1" max="10" value="1" @change="LwRangeBtn"/>
<div class="colorClass">画笔颜色</div>
<input type="color" id="lcolor" value="#FF1493" @change="LcolorBtn"/>
</div>
</div>
<textarea
id="textarea"
name="textBox"
cols="9"
rows="1"
class="text-style"
v-show="isShowText"
></textarea>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button plain @click="closeDialog">取 消</el-button>
<el-button type="primary" @click="submitBtn" class="g-background00BCD4" :disable="loading" :loading="loading">保 存</el-button>
</div>
</el-dialog>
</template>
<script>
//画笔颜色选择引入
import pickerColor from './pickerColor'
export default {
props: {
otherParameter: Object,//我这里传了对象是因为我的业务需求,可直接传baseUrl:String
},
components:{pickerColor},
data() {
return {
form: {},
isShowDrawPane: false,
canvas: null,
context: null,
//线宽
lwidth: 2,
//画笔颜色
lcolor: "#FF1493",
textColor:"#FF1493",
//维护绘画状态的数组
paintTypeArr: {
painting: false,
erase: false,
line: false,
arrows: false,
rect: false,
circle: false,
text: false,
},
//最近一次的canvas图片的数据
imageData: null,
//是否显示文字编写框
isShowText: false,
//保存画布图片历史的数据
historyImageData:[],
//保存已被撤销的历史画布图片数据
newHistoryImageData:[],
socket:null,
img: null,
filterType: undefined,
loading: false
};
},
watch: {
color () {
this.context.strokeStyle = this.color;
// this.pickerVisible = false//颜色改变后消失
}
},
mounted() {
let self = this;
self.init()
window.onresize = function () {
self.init()
}
this.listen()
},
methods: {
LwRangeBtn() {
this.lwidth = parseInt(document.getElementById("lwRange").value)
},
LcolorBtn() {
this.context.fillStyle = document.getElementById("lcolor").value
this.context.strokeStyle = document.getElementById("lcolor").value
this.textColor = document.getElementById("lcolor").value
},
closeDialog() {
this.$emit("onClose");
},
dataURLtoFile(dataURI, type) {
let binary = atob(dataURI.split(',')[1]);
let array = [];
for(let i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], {type:type });
},
//初始化画布
init() {
this.$nextTick(()=>{
this.canvas = document.getElementById("canvas")
this.context = this.canvas.getContext('2d')
this.imageData && this.context.putImageData(this.imageData, 0, 0)
let img = new Image()
img.setAttribute('crossOrigin', 'anonymous');
let url = this.otherParameter.base64;//重点之重,这是要编辑的图片base64,如图一
img.src = url
img.onload = () => {
if (img.complete) {
this.canvas.setAttribute('width', img.width)
this.canvas.setAttribute('height', img.height)
this.context .drawImage(img, 0, 0, img.width, img.height)
this.img = img
this.textColor = "#FF1493";
this.context.fillStyle = "#FF1493";
this.context.strokeStyle = "#FF1493";
}
}
})
},
//监听鼠标,用于画笔任意绘制和橡皮擦
listen() {
this.$nextTick(()=>{
let self = this
let lastPoint = { x: undefined, y: undefined }
let rect = self.canvas.getBoundingClientRect()
console.log(rect,"rect")
var scaleX = self.canvas.width / rect.width
var scaleY = self.canvas.height / rect.height
console.log(scaleX,"scaleX")
console.log(scaleY,"scaleY")
let textPoint = { x: undefined, y: undefined }
self.canvas.onmousedown = function (e) {
self.paintTypeArr["painting"] = true
let x1 = e.clientX
let y1 = e.clientY
x1 -= rect.left
y1 -= rect.top
lastPoint = { x: x1 * scaleX, y: y1 * scaleY }
console.log((self.paintTypeArr["text"]))
if (self.paintTypeArr["text"]) {
let textarea = document.getElementById("textarea")
if (self.isShowText) {
let textContent = textarea.value
self.isShowText = false
textarea.value = ""
console.log(textPoint.x, textPoint.y,"textPoint.x, textPoint.y,")
self.drawText(textPoint.x, textPoint.y, textContent)
} else if (!self.isShowText) {
self.isShowText = true
textarea.style.left = lastPoint.x + "px"
textarea.style.top = lastPoint.y + 160 + "px"
textarea.style.color = self.textColor
textPoint = { x: lastPoint.x, y: lastPoint.y }
// textarea.style['z-index'] = 6
}
}
if (self.paintTypeArr["erase"]) {
let ctx = self.context
ctx.save()
ctx.globalCompositeOperation = "destination-out"
ctx.beginPath()
let radius = self.lWidth / 2 > 5 ? self.lWidth / 2 : 5
ctx.arc(lastPoint.x, lastPoint.y, radius, 0, 2 * Math.PI)
ctx.clip()
ctx.clearRect(0, 0, self.canvas.width, self.canvas.height)
ctx.restore()
}
var thee = e ? e : window.event
self.stopBubble(thee)
}
self.canvas.onmousemove = function (e) {
let x2 = e.clientX
let y2 = e.clientY
x2 -= rect.left
y2 -= rect.top
let newPoint = { x: x2 * scaleX, y: y2 * scaleY }
if (self.isPainting()) {
self.drawLine(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)
lastPoint = newPoint
} else if (self.paintTypeArr["erase"]) {
if(!lastPoint.x && !lastPoint.y){return}
self.handleErase(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)
lastPoint = newPoint
} else if (self.paintTypeArr["line"]) {
// self.clearCanvas()
self.imageData && self.context.putImageData(self.imageData, 0, 0)
self.drawLine(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)
} else if (self.paintTypeArr["arrows"]) {
// self.clearCanvas()
self.imageData && self.context.putImageData(self.imageData, 0, 0)
self.drawArrow(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)
} else if (self.paintTypeArr["rect"]) {
// self.clearCanvas()
self.imageData && self.context.putImageData(self.imageData, 0, 0)
self.drawRect(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)
} else if (self.paintTypeArr["circle"]) {
// self.clearCanvas()
self.imageData && self.context.putImageData(self.imageData, 0, 0)
console.log(self.imageData)
self.drawCircle(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y)
}
var thee = e ? e : window.event
self.stopBubble(thee)
}
self.canvas.onmouseup = function () {
lastPoint = { x: undefined, y: undefined }
self.canvasDraw()
console.log(123)
self.filterObject(self.filterType)
}
})
},
//更新绘画类型数组paintTypeArr的状态
filterObject(type) {
this.filterType = type
if (!type) {
for (const key in this.paintTypeArr) {
this.paintTypeArr[key] = false
}
} else {
for (const key in this.paintTypeArr) {
key === type
? (this.paintTypeArr[key] = true)
: (this.paintTypeArr[key] = false)
}
}
},
//阻止事件冒泡
stopBubble(evt) {
if (evt.stopPropagation) {
evt.stopPropagation()
} else {
//ie
evt.cancelBubble = true
}
},
//判断是否是自由绘画模式
isPainting() {
for (let key in this.paintTypeArr) {
if (key !== "painting" && this.paintTypeArr[key]) {
return false
}
}
if (this.paintTypeArr["painting"]) {
return true
}
return false
},
//橡皮擦
handleErase(x1, y1, x2, y2) {
let ctx = this.context
//获取两个点之间的剪辑区域四个端点
var asin = radius * Math.sin(Math.atan((y2 - y1) / (x2 - x1)))
var acos = radius * Math.cos(Math.atan((y2 - y1) / (x2 - x1)))
var x3 = x1 + asin
var y3 = y1 - acos
var x4 = x1 - asin
var y4 = y1 + acos
var x5 = x2 + asin
var y5 = y2 - acos
var x6 = x2 - asin
var y6 = y2 + acos //保证线条的连贯,所以在矩形一端画圆
ctx.save()
ctx.beginPath()
ctx.globalCompositeOperation = "destination-out"
let radius = this.lWidth / 2 > 5 ? this.lWidth / 2 : 5
ctx.arc(x2, y2, radius, 0, 2 * Math.PI)
ctx.clip()
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
ctx.restore() //清除矩形剪辑区域里的像素
ctx.save()
ctx.beginPath()
ctx.globalCompositeOperation = "destination-out"
ctx.moveTo(x3, y3)
ctx.lineTo(x5, y5)
ctx.lineTo(x6, y6)
ctx.lineTo(x4, y4)
ctx.closePath()
ctx.clip()
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
ctx.restore()
},
//画线
drawLine(fromX, fromY, toX, toY) {
let ctx = this.context
ctx.beginPath()
ctx.lineWidth = this.lwidth
ctx.lineCap = "round"
ctx.lineJoin = "round"
ctx.moveTo(fromX, fromY)
ctx.lineTo(toX, toY)
ctx.stroke()
ctx.closePath()
},
//画箭头
drawArrow(fromX, fromY, toX, toY) {
let ctx = this.context
var headlen = 10 //自定义箭头线的长度
var theta = 45 //自定义箭头线与直线的夹角,个人觉得45°刚刚好
var arrowX, arrowY //箭头线终点坐标
// 计算各角度和对应的箭头终点坐标
var angle = (Math.atan2(fromY - toY, fromX - toX) * 180) / Math.PI
var angle1 = ((angle + theta) * Math.PI) / 180
var angle2 = ((angle - theta) * Math.PI) / 180
var topX = headlen * Math.cos(angle1)
var topY = headlen * Math.sin(angle1)
var botX = headlen * Math.cos(angle2)
var botY = headlen * Math.sin(angle2)
ctx.beginPath()
//画直线
ctx.moveTo(fromX, fromY)
ctx.lineTo(toX, toY)
arrowX = toX + topX
arrowY = toY + topY
//画上边箭头线
ctx.moveTo(arrowX, arrowY)
ctx.lineTo(toX, toY)
arrowX = toX + botX
arrowY = toY + botY
//画下边箭头线
ctx.lineTo(arrowX, arrowY)
ctx.stroke()
ctx.closePath()
},
//绘制矩形
drawRect(topLeftX, topLeftY, botRightX, botRightY) {
let ctx = this.context
ctx.strokeRect(
topLeftX,
topLeftY,
Math.abs(botRightX - topLeftX),
Math.abs(botRightY - topLeftY)
)
},
//画圆
drawCircle(circleX, circleY, endX, endY) {
console.log(circleX, circleY, endX, endY)
let ctx = this.context
let radius = Math.sqrt(
(circleX - endX) * (circleX - endX) +
(circleY - endY) * (circleY - endY)
)
ctx.beginPath()
ctx.arc(circleX, circleY, radius, 0, Math.PI * 2, true)
ctx.stroke()
},
//画文字
drawText(startX, startY, content) {
let ctx = this.context
ctx.save()
ctx.beginPath()
ctx.font = "25px orbitron"
ctx.textBaseline = "top"
ctx.fillText(content, parseInt(startX), parseInt(startY))
ctx.restore()
ctx.closePath()
},
//清屏
clearCanvas() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.init()
console.log(this.imageData)
},
//定格画布图片
canvasDraw() {
this.imageData = this.context.getImageData(0,0,this.canvas.width,this.canvas.height)
this.historyImageData.push(this.imageData)
console.log(this.historyImageData)
console.log(this.imageData)
},
//撤销
redo(){
let historyImageData = this.historyImageData
let newHistoryImageData = this.newHistoryImageData
if(historyImageData.length > 0){
let hisImg = historyImageData.pop()
newHistoryImageData.push(hisImg)
if(historyImageData.length === 0){
this.imageData = null
this.clearCanvas()
this.init()
}else{
this.context.putImageData(historyImageData[historyImageData.length - 1],0,0)
}
}
},
//反撤销
cancelRedo(){
if(this.newHistoryImageData.length > 0){
const newHisImg = this.newHistoryImageData.pop()
this.imageData = newHisImg
this.context.putImageData(newHisImg,0,0)
this.historyImageData.push(newHisImg)
}
},
//保存图片
downLoad(){
const imgUrl = this.canvas.toDataURL('image/png')
const a = document.createElement('a')
a.href = imgUrl
a.download = '绘图保存记录' + (new Date).getTime()
a.target = '_blank'
document.body.appendChild(a)
a.click()
document.body.removeChild(a);
console.log(this.imageData)
},
submitBtn() {
//防止多次点击提交
this.loading = true;
setTimeout(()=>{
this.loading = false;
},3000)
let fileObj = {
relativeType: 3,
name:"编辑图片"
}
let canvas = document.getElementById('canvas')
var file = canvas.toDataURL("image/png");
var formData = new FormData();
let blob= this.dataURLtoFile(file, 'image/jpg')
let fileOfBlob = new File([blob], new Date()+'.jpg')
formData.append('file', fileOfBlob);
formData.append('relativeType', 3);
formData.append('name', "编辑图片");
//上传图片后提交保存,根据实际开发需求编写
this.$axios
.postUpload("/uxxxoad", formData)
.then((response) => {
this.$api.creatxxxxRule({taskBreakRule}).then((response)=>{
if(response.success) {
this.$message({
message: "保存成功",
type: "success"
});
this.$emit("onClose",true)
} else {
this.$message({
message: response.info,
type: "error"
});
}
})
});
},
},
};
</script>
<style lang="scss" scoped>
.container {
// width: 100%;
// height: 100%;
// margin: 10px auto;
// overflow: hidden;
}
.tool-container {
width: 580px;
border: 2px solid orange;
border-radius: 10px;
display: flex;
justify-content: center;
position: relative;
}
.drawPane {
padding: 25px 20px;
height: 120px;
position: absolute;
top: -120px;
left: 0px;
border-radius: 5px;
border: 2px solid orangered;
}
.close-draw-pane {
position: absolute;
right: 5px;
top: 5px;
}
.icon-div {
margin: 4px 12px;
}
.icon :hover {
cursor: pointer;
}
input[type="range"] {
-webkit-appearance: none;
width: 180px;
height: 24px;
outline: none;
margin-bottom: 3px;
}
input[type="range"]::-webkit-slider-runnable-track {
background-color: orangered;
height: 4px;
border-radius: 5px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: orange;
cursor: pointer;
margin-top: -4px;
}
.text-style {
float: left;
position: absolute;
font: 25px orbitron;
word-break: break-all;
background-color: transparent;
}
.colorClass {
color: orange;
}
.svg-icon {
font-size: 24px;
}
</style>
Component pickerColor.vue
<template>
<div>
<photoshop-picker v-if="type === 'photoshop'" v-model="colors"></photoshop-picker>
<material-picker v-if="type === 'material'" v-model="colors"></material-picker>
<compact-picker v-if="type === 'compact'" v-model="colors"></compact-picker>
<swatches-picker v-if="type === 'swatches'" v-model="colors"></swatches-picker>
<slider-picker v-if="type === 'slider'" v-model="colors"></slider-picker>
<sketch-picker v-if="type === 'sketch'" v-model="colors"></sketch-picker>
<chrome-picker v-if="type === 'chrome'" v-model="colors"></chrome-picker>
</div>
</template>
<script>
//这些不需要单独引入,vue项目构建会安装了vue-color这个依赖包,在根目录node_modules可以找到vue-color依赖包。
import {
Photoshop,
Material,
Compact,
Swatches,
Slider,
Sketch,
Chrome,
} from "vue-color";
export default {
name: "pickerColor",
props: {
"color": String,
type: {
default: "photoshop",
},
},
components: {
"photoshop-picker": Photoshop,
"material-picker": Material,
"compact-picker": Compact,
"swatches-picker": Swatches,
"slider-picker": Slider,
"sketch-picker": Sketch,
"chrome-picker": Chrome,
},
data () {
return {
colors: "",
};
},
methods: {},
watch: {
colors () {
this.$emit("update:color", this.colors.hex);
},
},
};
</script>
<style></style>
Parameter transmission format
Figure 1: The background image is base64