基于Vue+Canvas实现的画板绘画以及保存功能
本文内容设计到的画板的js部分内容来源于灵感来源引用地址,然后我在此基础上,根据自己的需求做了修改,增加了其他功能。
下面展示了完整的前后端代码
这里写目录标题
- 基于Vue+Canvas实现的画板绘画以及保存功能
- 1. board-js.js
- 2. 前端的vue文件
- 3. 后端Controller
1. board-js.js
这个代码,接收一个容器参数,创建了一个画板类,里面实现了画板会用到的基本方法,保存到单独的js文件,在vue文件中导入,创建一个画板对象。
Canvas直接使用canvas.toDataURL()保存的图片是没有背景的,因为默认的是png,所以需要开始绘画之前先填充背景,下面代码做出了修改,在init()函数中。
代码中用到的Canvas的原生API的作用,可以参考这里Canvas参考手册
export default class BoardCanvas {constructor(container) {// 容器this.container = container// canvas画布this.canvas = this.createCanvas(container)// 绘制工具this.ctx = this.canvas.getContext('2d')// 起始点位置this.startX = 0this.stateY = 0// 画布历史栈this.pathSegmentHistory = []this.index = 0// 初始化this.init()}// 创建画布createCanvas(container) {const canvas = document.createElement('canvas')canvas.width = container.clientWidthcanvas.height = container.clientHeightcanvas.style.display = 'block'canvas.style.backgroundColor = 'white'container.appendChild(canvas)return canvas}// 初始化init() {this.addPathSegment()this.setContext2DStyle()//下面两行,原文件是没有的,如果没有会导致保存的图片没有背景,只有绘画轨迹this.ctx.fillStyle = "#ffffff";this.ctx.fillRect(0, 0,this.canvas.width, this.canvas.height);this.canvas.addEventListener('contextmenu', e => e.preventDefault())this.canvas.addEventListener('mousedown', this.mousedownEvent.bind(this))window.document.addEventListener('keydown', this.keydownEvent.bind(this))}// 设置画笔样式setContext2DStyle() {this.ctx.strokeStyle = 'black'this.ctx.lineWidth = 3this.ctx.lineCap = 'round'this.ctx.lineJoin = 'round'}// 鼠标事件mousedownEvent(e) {const that = thisconst ctx = this.ctxctx.beginPath()ctx.moveTo(e.offsetX, e.offsetY)ctx.stroke()this.canvas.onmousemove = function (e) {ctx.lineTo(e.offsetX, e.offsetY)ctx.stroke()}this.canvas.onmouseup = this.canvas.onmouseout = function () {that.addPathSegment()this.onmousemove = nullthis.onmouseup = nullthis.onmouseout = null}}// 键盘事件keydownEvent(e) {if(!e.ctrlKey) returnswitch(e.keyCode) {case 90:this.undo()breakcase 89:this.redo()break}}// 添加路径片段addPathSegment() {const data = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)// 删除当前索引后的路径片段,然后追加一个新的路径片段,更新索引this.pathSegmentHistory.splice(this.index + 1)this.pathSegmentHistory.push(data)this.index = this.pathSegmentHistory.length - 1}// 撤销undo() {if(this.index <= 0) returnthis.index--this.ctx.putImageData(this.pathSegmentHistory[this.index], 0, 0)}// 恢复redo() {if(this.index >= this.pathSegmentHistory.length - 1) returnthis.index++this.ctx.putImageData(this.pathSegmentHistory[this.index], 0, 0)}//获取画布内容getImage() {return this.canvas.toDataURL();}//清空画板cleanboard(){this.ctx.fillStyle = "#ffffff";this.ctx.fillRect(0, 0,this.canvas.width, this.canvas.height);}
}
2. 前端的vue文件
<template><el-container direction="vertical" style="height: 100%;width: 100%"><!--头顶布局,用户操作提示语--><el-header height="10%"><h2>在空白处进行绘画</h2></el-header><!--中间布局 --><div style=display:flex;justify-content:center;align-items:center;><!--中间画板--><div class="drawing-board"style="width:66%;height:600px;border: 1px black solid;margin-left: 10px"><div id="container" ref="container" style="width: 100%; height: 100%"></div></div></div><!--底部按钮--><el-footer style="height: 300px;margin-top: 25px"><el-button type="primary" round @click="savedrawing">保存</el-button></el-footer></el-container>
</template>//这里使用的vue的setup语法糖,所以data和method不需要封装,直接用
<script setup>
import { ref, onMounted } from 'vue'
import Board from '@/js/drawing-board.js'
import axios from "axios";const container = ref(null)
let drawboard = null;
onMounted(() => {// 新建一个画板drawboard=new Board(container.value)
})
function savedrawing() {const drawdata = drawboard.getImage(); let formdata = new FormData();const timestamp = (new Date()).valueOf();let filename = timestamp;formdata.append("drawPictureId",filename);formdata.append("drawPictureData",drawdata.substring(22));axios({method:"post",url:"/savedrawdata",baseURL:"http://localhost:9999",data:formdata,contentType:false,processData:false}).then(response=>{if(response.status===200){alert("保存成功!")}}).catch(error=>{console.log(error);})
}
</script>
3. 后端Controller
Controller文件中,前端使用FormData格式传递参数,就相当于是个map键值对,所以在参数这里,使用 @RequestParam() ,取出表单中的值,括号中的字符串,是在前端传入的,表示将该key对应的value,赋值给后面的String 参数。
其次,这里注意Canvas得到的图片是经过base64编码过的,所以先解码成字节数组
@RequestMapping("/savedrawdata")public ResponseEntity<?> savedrawdata(@RequestParam("drawPictureId")String drawPictureId,@RequestParam("drawPictureData")String drawPictureData){try {// 解码前端传过来的base64编码byte[] imageBytes = Base64.decodeBase64(drawPictureData);// 将字节流转为图片缓冲流BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(imageBytes));// 保存为pngFile output = new File("F:\\image\\" + drawPictureId + ".png");ImageIO.write(bufferedImage, "png", output);} catch (IOException e) {e.printStackTrace();}System.out.println(rawPictureId);System.out.println(drawPictureId);return ResponseEntity.ok(HttpStatus.OK);}