vue项目使用canvas实现手写板功能
效果图
组件CanvasDialog.vue
直接上代码,下面代码可以当做组件直接引用,根据自己的需求传对应的图片即可,操作图标需要自己替换,保存功能也需要自己实现。
<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>
组件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>
传参格式
图一:
不敢说很全面,但是应该也够用了,也希望帮到有同样需求的你们。