失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > vue下利用canvas实现图片标注

vue下利用canvas实现图片标注

时间:2020-04-17 20:03:30

相关推荐

vue下利用canvas实现图片标注

web端实现在线图片标注在此做下记录,功能类似微信截图时的标注,包含画线、框、箭头和文字输入,思路是利用canvas画布,先把要标注的图片使用drawImage方法画在画布上,然后定义画线、框、箭头和文字输入的方法调用,组件代码如下:

<template><div class="draw"><div class="drawTop" ref="drawTop" v-if="lineStep == lineNum"><div><el-button type @click="resetAll">清空</el-button><el-button type @click="repeal">撤销</el-button><el-button type @click="canvasRedo">恢复</el-button><el-button type @click="downLoad">下载</el-button></div><div style="width:22%">选择绘制类型:<el-radio-group v-model="type" size="medium"><el-radio-buttonv-for="(item,index) in typeOption":key="index":label="item.value"@click.native="radioClick(item.value)">{{item.label}}</el-radio-button></el-radio-group></div><div style="width:15%">边框粗细:<el-slider v-model="lineWidth" :min="0" :max="10" :step="1" style="width:70%"></el-slider></div><div>线条颜色:<el-color-picker v-model="strokeStyle"></el-color-picker></div><div>文字颜色:<el-color-picker v-model="fontColor"></el-color-picker></div><div style="width:15%">文字大小:<el-slider v-model="fontSize" :min="14" :max="36" :step="2" style="width:70%"></el-slider></div></div><div style="height: 100%;width: 100%;position:relative;"><div class="content"></div><input v-show="isShow" type="text" @blur="txtBlue" ref="txt" id="txt"style="z-index: 9999;position: absolute;border: 0;background:none;outline: none;"/></div></div></template><script>export default {name: "callout",props: {imgPath: undefined,},data() {return {isShow: false,canvas: "",ctx: "",ctxX: 0,ctxY: 0,lineWidth: 1,type: "L",typeOption: [{label: "线", value: "L"},{label: "矩形", value: "R"},{label: "箭头", value: "A"},{label: "文字", value: "T"},],canvasHistory: [],step: 0,loading: false,fillStyle: "#CB0707",strokeStyle: "#CB0707",lineNum: 2,linePeak: [],lineStep: 2,ellipseR: 0.5,dialogVisible: false,isUnfold: true,fontSize: 24,fontColor: "#CB0707",fontFamily: '微软雅黑',img: new Image(),};},mounted() {let _this = this;let image = new Image();image.setAttribute('crossOrigin', 'anonymous');image.src = this.imgPath;image.onload = function () {//图片加载完,再draw 和 toDataURLif (plete) {_this.img = imagelet content = document.getElementsByClassName("content")[0];_this.canvas = document.createElement("canvas");_this.canvas.height = _this.img.height_this.canvas.width = _this.img.width_this.ctx = _this.canvas.getContext("2d");_this.ctx.globalAlpha = 1;_this.ctx.drawImage(_this.img, 0, 0)_this.canvasHistory.push(_this.canvas.toDataURL());_this.ctx.globalCompositeOperation = _this.type;content.appendChild(_this.canvas);_this.bindEventLisner();}}},methods: {radioClick(item) {if (item != "T") {this.txtBlue()this.resetTxt()}},// 下载画布downLoad() {let _this = this;let url = _this.canvas.toDataURL("image/png");let fileName = "canvas.png";if ("download" in document.createElement("a")) {// 非IE下载const elink = document.createElement("a");elink.download = fileName;elink.style.display = "none";elink.href = url;document.body.appendChild(elink);elink.click();document.body.removeChild(elink);} else {// IE10+下载navigator.msSaveBlob(url, fileName);}},// 清空画布及历史记录resetAll() {this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);this.canvasHistory = [];this.ctx.drawImage(this.img, 0, 0);this.canvasHistory.push(this.canvas.toDataURL());this.step = 0;this.resetTxt();},// 清空当前画布reset() {this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);this.ctx.drawImage(this.img, 0, 0);this.resetTxt();},// 撤销方法repeal() {let _this = this;if (this.isShow) {_this.resetTxt();_this._repeal();} else {_this._repeal();}},_repeal() {if (this.step >= 1) {this.step = this.step - 1;let canvasPic = new Image();canvasPic.src = this.canvasHistory[this.step];canvasPic.addEventListener("load", () => {this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);this.ctx.drawImage(canvasPic, 0, 0);this.loading = true;});} else {this.$message.warning("不能再继续撤销了");}},// 恢复方法canvasRedo() {if (this.step < this.canvasHistory.length - 1) {if (this.step == 0) {this.step = 1;} else {this.step++;}let canvasPic = new Image();canvasPic.src = this.canvasHistory[this.step];canvasPic.addEventListener("load", () => {this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);this.ctx.drawImage(canvasPic, 0, 0);});} else {this.$message.warning("已经是最新的记录了");}},// 绘制历史数组中的最后一个rebroadcast() {let canvasPic = new Image();canvasPic.src = this.canvasHistory[this.step];canvasPic.addEventListener("load", () => {this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);this.ctx.drawImage(canvasPic, 0, 0);this.loading = true;});},// 绑定事件,判断分支bindEventLisner() {let _this = this;let r1, r2; // 绘制圆形,矩形需要this.canvas.onmousedown = function (e) {console.log("onmousedown");if (_this.type == "L") {_this.createL(e, "begin");} else if (_this.type == "R") {r1 = e.layerX;r2 = e.layerY;_this.createR(e, "begin", r1, r2);} else if (_this.type == "A") {_this.drawArrow(e, "begin")} else if (_this.type == "T") {_this.createT(e, "begin")}};this.canvas.onmouseup = function (e) {console.log("onmouseup");if (_this.type == "L") {_this.createL(e, "end");} else if (_this.type == "R") {_this.createR(e, "end", r1, r2);r1 = null;r2 = null;} else if (_this.type == "A") {_this.drawArrow(e, "end")} else if (_this.type == "T") {_this.createT(e, "end")}};},// 绘制线条createL(e, status) {let _this = this;if (status == "begin") {_this.ctx.beginPath();_this.ctx.moveTo(e.layerX, e.layerY);_this.canvas.onmousemove = function (e) {console.log("onmousemove");_this.ctx.lineTo(e.layerX, e.layerY);_this.ctx.strokeStyle = _this.strokeStyle;_this.ctx.lineWidth = _this.lineWidth;_this.ctx.stroke();};} else if (status == "end") {_this.ctx.closePath();_this.step = _this.step + 1;if (_this.step < _this.canvasHistory.length - 1) {_this.canvasHistory.length = _this.step; // 截断数组}_this.canvasHistory.push(_this.canvas.toDataURL());_this.canvas.onmousemove = null;}},// 绘制矩形createR(e, status, r1, r2) {let _this = this;let r;if (status == "begin") {console.log("onmousemove");_this.canvas.onmousemove = function (e) {_this.reset();let rx = e.layerX - r1;let ry = e.layerY - r2;//保留之前绘画的图形if (_this.step !== 0) {let canvasPic = new Image();canvasPic.src = _this.canvasHistory[_this.step];_this.ctx.drawImage(canvasPic, 0, 0);}_this.ctx.beginPath();_this.ctx.strokeRect(r1, r2, rx, ry);_this.ctx.strokeStyle = _this.strokeStyle;_this.ctx.lineWidth = _this.lineWidth;_this.ctx.closePath();_this.ctx.stroke();};} else if (status == "end") {_this.rebroadcast();let interval = setInterval(() => {if (_this.loading) {clearInterval(interval);_this.loading = false;} else {return;}let rx = e.layerX - r1;let ry = e.layerY - r2;_this.ctx.beginPath();_this.ctx.rect(r1, r2, rx, ry);_this.ctx.strokeStyle = _this.strokeStyle;_this.ctx.lineWidth = _this.lineWidth;_this.ctx.closePath();_this.ctx.stroke();_this.step = _this.step + 1;if (_this.step < _this.canvasHistory.length - 1) {_this.canvasHistory.length = _this.step; // 截断数组}_this.canvasHistory.push(_this.canvas.toDataURL());_this.canvas.onmousemove = null;}, 1);}},//绘制箭头drawArrow(e, status) {let _this = this;if (status == "begin") {//获取起始位置_this.arrowFromX = e.layerX;_this.arrowFromY = e.layerY;_this.ctx.beginPath();_this.ctx.moveTo(e.layerX, e.layerY);} else if (status == "end") {//计算箭头及画线let toX = e.layerX;let toY = e.layerY;let theta = 30;let headlen = 10;let _this = this;let fromX = this.arrowFromX;let fromY = this.arrowFromY;// 计算各角度和对应的P2,P3坐标let angle = Math.atan2(fromY - toY, fromX - toX) * 180 / Math.PI,angle1 = (angle + theta) * Math.PI / 180,angle2 = (angle - theta) * Math.PI / 180,topX = headlen * Math.cos(angle1),topY = headlen * Math.sin(angle1),botX = headlen * Math.cos(angle2),botY = headlen * Math.sin(angle2);let arrowX = fromX - topX,arrowY = fromY - topY;_this.ctx.moveTo(arrowX, arrowY);_this.ctx.moveTo(fromX, fromY);_this.ctx.lineTo(toX, toY);arrowX = toX + topX;arrowY = toY + topY;_this.ctx.moveTo(arrowX, arrowY);_this.ctx.lineTo(toX, toY);arrowX = toX + botX;arrowY = toY + botY;_this.ctx.lineTo(arrowX, arrowY);_this.ctx.strokeStyle = _this.strokeStyle;_this.ctx.lineWidth = _this.lineWidth;_this.ctx.stroke();_this.ctx.closePath();_this.step = _this.step + 1;if (_this.step < _this.canvasHistory.length - 1) {_this.canvasHistory.length = _this.step; // 截断数组}_this.canvasHistory.push(_this.canvas.toDataURL());_this.canvas.onmousemove = null;}},//文字输入createT(e, status) {let _this = this;if (status == "begin") {} else if (status == "end") {let offset = 0;if (_this.fontSize >= 28) {offset = (_this.fontSize / 2) - 3} else {offset = (_this.fontSize / 2) - 2}_this.ctxX = e.layerX + 2;_this.ctxY = e.layerY + offset;let index = this.getPointOnCanvas(e);_this.$refs.txt.style.left = index.x + 'px';_this.$refs.txt.style.top = index.y - (_this.fontSize / 2) + 'px';_this.$refs.txt.value = '';_this.$refs.txt.style.height = _this.fontSize + "px";_this.$refs.txt.style.width = _this.canvas.width - e.layerX - 1 + "px",_this.$refs.txt.style.fontSize = _this.fontSize + "px";_this.$refs.txt.style.fontFamily = _this.fontFamily;_this.$refs.txt.style.color = _this.fontColor;_this.$refs.txt.style.maxlength = Math.floor((_this.canvas.width - e.layerX) / _this.fontSize);_this.isShow = true;setTimeout(() => {_this.$refs.txt.focus();})}},//文字输入框失去光标时在画布上生成文字txtBlue() {let _this = this;let txt = _this.$refs.txt.value;if (txt) {_this.ctx.font = _this.$refs.txt.style.fontSize + ' ' + _this.$refs.txt.style.fontFamily;_this.ctx.fillStyle = _this.$refs.txt.style.color;_this.ctx.fillText(txt, _this.ctxX, _this.ctxY);_this.step = _this.step + 1;if (_this.step < _this.canvasHistory.length - 1) {_this.canvasHistory.length = _this.step; // 截断数组}_this.canvasHistory.push(_this.canvas.toDataURL());_this.canvas.onmousemove = null;}},//计算文字框定位位置getPointOnCanvas(e) {let cs = this.canvas;let content = document.getElementsByClassName("content")[0];return {x: e.layerX + (content.clientWidth - cs.width) / 2,y: e.layerY};},//清空文字resetTxt() {let _this = this;_this.$refs.txt.value = '';_this.isShow = false;}}};</script><style scope>* {box-sizing: border-box;}body,html,#app {overflow: hidden;}.draw {height: 100%;min-width: 420px;display: flex;flex-direction: column;}.content {flex-grow: 1;height: 100%;width: 100%;}.drawTop {display: flex;justify-content: flex-start;align-items: center;padding: 5px;height: 52px;}.drawTop > div {display: flex;align-items: center;padding: 5px 5px;}div.drawTopContrllor {display: none;}@media screen and (max-width: 1200px) {.drawTop {position: absolute;background-color: white;width: 100%;flex-direction: column;align-items: flex-start;height: 30px;overflow: hidden;}.drawTopContrllor {display: flex !important;height: 30px;width: 100%;justify-content: center;align-items: center;padding: 0 !important;}}</style>

然后在页面中引入组件,传入图片链接。

在开发过程中遇到的问题:

文字输入功能在用户输入文字后,如果不再点击别的地方直接点击别的功能按钮的话,最后输入的文字将不会再画布上生成,通过监控输入框的blur事件来在画布上生成文字,避免这个问题。文字输入时字体的大小会影响生成文字的位置,这里发现文字的大小和位置有一个偏移量:

let offset = 0;if (_this.fontSize >= 28) {offset = (_this.fontSize / 2) - 3} else {offset = (_this.fontSize / 2) - 2}

在画布上生成文字的时候需要加上这个偏移量,这里字体范围是14~36,别的字体大小没有校验,不一定适用这个计算方式。

绘制矩形的时候需要先清空画布,在清空之前先保存一次画布然后再清空再重新画一下画布,负责矩形框会不停的出现轨迹,并且之前画的元素会消失。

撤销的时候需要考虑文字输入,判断input得v-show是否为true,如果是true需要先清空文字,再撤销,否则画布上会一直存在一个输入框。

有别的问题以后再补充。

参考:/p/7172c11c6002

如果觉得《vue下利用canvas实现图片标注》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。