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><textareaid="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 = urlimg.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 = imgthis.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.textColortextPoint = { 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 = typeif (!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 {//ieevt.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.historyImageDatalet newHistoryImageData = this.newHistoryImageDataif(historyImageData.length > 0){let hisImg = historyImageData.pop()newHistoryImageData.push(hisImg)if(historyImageData.length === 0){this.imageData = nullthis.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 = newHisImgthis.context.putImageData(newHisImg,0,0)this.historyImageData.push(newHisImg)}},//保存图片downLoad(){const imgUrl = this.canvas.toDataURL('image/png')const a = document.createElement('a')a.href = imgUrla.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>

图一:

不敢说很全面,但是应该也够用了,也希望帮到有同样需求的你们。

VUE项目开发,使用canvas实现图片签名编辑手写板功能相关推荐

  1. vue项目开发的目录结构

    vue项目开发的目录结构 一.通过命令行工具安装vue(npm install webpack vue-project) build -- 项目构建的相关代码 config -- 配置目录,包括端口. ...

  2. vue项目开发实录--仿去哪儿网App-张鹏-专题视频课程

    vue项目开发实录--仿去哪儿网App-160人已学习 课程介绍         本课程为vue项目开发实录(仿去哪儿网App)其中涉及到知识点有:组件搭建,路由,vuex,axios,webpack ...

  3. 【vue】 vue项目开发卡片展示页面----菜品管理

    vue项目开发卡片展示页面----菜品管理 对话框表单获取父组件数据 子组件dishform 对话框表单样式 methods 中的dataInit方法 获取父组件传来的数据 <script> ...

  4. 用 cooking 搭建一个简单又优雅的 Vue 项目开发环境 (入门篇)

    本文适合 Vue 的初学者,以及对 webpack 不熟悉的同学阅读.前提是你要会用基本的命令行. Node 和 NPM,以及掌握 ES2015 的基础知识.本文都是在 macOS 环境下运行,要求使 ...

  5. Win10 + VSCode踩坑 + vue项目开发:设置vscode终端为管理员权限

    win10系统 如何设置vscode的终端为管理员权限? 一次一次的授权太麻烦! 这里直接更改授权为管理员运行即可. 操作与设置步骤: "桌面找到"VSCode"程序图标 ...

  6. Vue项目开发中使用路由懒加载

    Vue项目开发中使用懒加载 一.使用路由懒加载的好处 当打包项目时,JavaScript包会变的非常大,影响页面加载,要是通过把不同的路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应 ...

  7. Vue项目开发中优雅的切换服务端ip

    Vue项目开发中优雅的切换服务端ip 在进行Vue开发的时候,需要配置项目对应服务端的ip地址,但如果需要在多个服务端间进行切换,通常的做法是:手动修改vue.config.js配置文件中的服务端ip ...

  8. Vue项目开发需要准备和配置些什么环境?

    Vue项目开发需要准备和配置些什么环境? 一.首先需要安装node.js 下载:安装node.js链接 安装在D盘 window+R,输入cmd,打开命令提示符窗口,输入: npm -v 检测是否安装 ...

  9. vue项目开发 实现自定义Transfer穿梭框效果(结合el-tree)

    vue项目开发 实现自定义Transfer穿梭框效果(结合el-tree) 效果图: 直接上代码: // An highlighted block <template><el-dia ...

最新文章

  1. 安全设置之修改远程桌面连接默认3389端口
  2. 【转】Mybatis传多个参数(三种解决方案)
  3. php读取zip文件,php如何读取zip内容?(zip_entry_read函数的使用)
  4. iOS客户端开发流程
  5. Redis使用场景、Redis线程模型、Redis持久化 - 公开课笔记
  6. markdown语法简明笔记
  7. 机器学习自学札记(1)
  8. vue 第七天(循环遍历)
  9. Paste for mac(剪切板管理工具)
  10. 于Linux下s、t、i、a权限
  11. SQL日期格式化处理
  12. python将经纬度标注在地图上_Python 给定的经纬度标注在地图上的实现方法
  13. java 首字母小写转换_java实现将字符串中首字母转换成大写,其它全部转换成小写的方法示例...
  14. MMA8452Q 三轴加速度传感器驱动
  15. RS485通信和Modbus通信协议
  16. ISAC(internet Speech Audio Codec):
  17. vr电力作业安全培训覆盖三大板块,为学员提供高仿真的技能培训
  18. 您只能在Photoshop CS4中做的3酷技巧
  19. 计算机漫游模式,深入理解操作系统之一 —— 计算机系统漫游
  20. python用turtle画一个旋转的风车

热门文章

  1. 《Windows游戏编程大师技巧》(第二版)第2章
  2. 8086CPU中指令RET与IRET区别
  3. 微服务的架构思想(Microservices)— Martin Fowler
  4. 量子计算机替代芯片,量子芯片能否在不久的将来替代传统的芯片?
  5. 正则校验非中文 加长度校验
  6. 怎样免费下载收费网站的资料
  7. [superset] d3 图表 汉化, 国际化
  8. 【光学】基于matlab泊松亮斑仿真【含Matlab源码 1937期】
  9. 卷积神经网络CNN解析
  10. 什么是泛型?- 泛型入门篇