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 (image.complete) {_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>

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

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

  1. 文字输入功能在用户输入文字后,如果不再点击别的地方直接点击别的功能按钮的话,最后输入的文字将不会再画布上生成,通过监控输入框的blur事件来在画布上生成文字,避免这个问题。
  2. 文字输入时字体的大小会影响生成文字的位置,这里发现文字的大小和位置有一个偏移量:
    let offset = 0;
    if (_this.fontSize >= 28) {offset = (_this.fontSize / 2) - 3
    } else {offset = (_this.fontSize / 2) - 2
    }

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

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

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

有别的问题以后再补充。

参考:https://www.jianshu.com/p/7172c11c6002

vue下利用canvas实现在线图片标注相关推荐

  1. PyCharm下利用pyqt对话框打开图片,显示

    PyCharm下利用pyqt对话框打开图片,显示 PyCharm下利用pyqt对话框打开图片显示 1安装PyCharm Python PyQt5 Pycharm配置Pyqt 1添加Qtdesigner ...

  2. vue.js 利用canvas绘制仪表盘圆环进度条-带动画

    vue.js 利用canvas绘制简易仪表盘进度条 html代码 因为动画效果比较消耗性能,所以进度条单独canvas绘制 <template><div class="ci ...

  3. vue 中利用canvas 给pdf文件加水印---详细教程(附上完整代码)

    需求:在h5网页中打开pdf文件,要求给文件添加水印 实现技术及插件:vue,vue-pdf,canvas 插件安装: npm i vue-pdf --save npm i pdf-lib --sav ...

  4. linux 扫坏轨工具,linux下利用badblocks程序在线修复坏道

    新的硬盘修复方法,请看这个文章: http://anheng.com/news/html/net_admin_blog/hdparm_repair_hd_bad_blocks.html 现在的硬盘,在 ...

  5. 微信小程序利用canvas生成海报-------图片为网络图片

    根据我们老总的业务需求,迫不得已,我做了这个canvas绘制的海报,感觉基本上可以解决现在海报所遇到的大部分问题了,献给那些没有做过的小伙伴们,话不多说,先上我做的效果 上代码 <style&g ...

  6. js:使用canvas做一个图片标注功能

    canvas相关库的选择 名称 star(2021.3) 文档 备注 fabricjs 18.2k http://fabricjs.com/ -------- konva 6k https://kon ...

  7. Vue中利用canvas添加炫动背景

    1.展示页面 <template><div id="main"><canvas id="myCanvas" style=" ...

  8. vue标签旋转_基于vue下input实现图片上传,压缩,拼接以及旋转的代码详解

    本篇文章给大家带来的内容是关于php队列的实现代码介绍,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 作为一名前端工作人员,相信大家在开发系统的时候,经常有遇到需要这么一种需求,就是 ...

  9. Vue利用Canvas实现逐帧播放图片不闪烁(Vue解决逐帧播放图片闪烁问题)

    Vue利用Canvas实现逐帧播放图片不闪烁 前言 Vue代码实现 实现效果 前言 Vue采用<el-image :src="src"></el-image> ...

最新文章

  1. java.lang.InstantiationException 不能实例化某个对象
  2. Linux命令之初出茅庐
  3. Java操作word文件的工具选择
  4. Windwos命令工作笔记001---route add命令详解
  5. VB.NET工作笔记004---查看电脑已经安装了哪些COM组件,可以用个OleViewer.zip
  6. mexcuda中矩阵数据的传输
  7. JavaScript验证表单大全【自用】
  8. java get请求带参数报错 java.io.IOException: Server returned HTTP response code: 400 for URL
  9. 漫画:图的最短路径问题
  10. 与错误共生,迎接成功-墨菲定律教给我的道理
  11. MyEclipse 2016 的破解激活
  12. 数据中心常说的IDC,EDC,ODC,DC分别指什么类型机房?
  13. 计算机储存容量单位的进率,进制、存储与容量
  14. QML使用ShaderEffect绘制波纹
  15. 服务器修复划痕,【DIY】自己动手修复屏幕划痕及建议
  16. 让机器学习助力医疗领域
  17. 电子线路设计技巧7:UC3843A升压电路中振荡频率和占空比的确定方法
  18. 安卓系统怎么查找手机定位服务器,安卓手机丢了怎么查定位找手机
  19. 【复变函数与积分变换】05. 留数
  20. Python连接维特智能角度传感器JY61/JY901的方法

热门文章

  1. 关于烛光斧影——赵光义是否谋杀赵匡胤,是否合法继位
  2. filterConfig
  3. [转载]关于火星坐标系统
  4. UEFI+GPT更换硬盘GHOST,DD也可以。
  5. tkinter--画布
  6. 关于网络知识一些笔记(个人2022.06)
  7. 忘记Win10电脑密码,修改密码的方法
  8. Non-OK-status: Status(error::Code::INVALID_ARGUMENT, “Unsupported data format“) status: Invalid argu
  9. YAHOO工具库提供的方法
  10. opencv medianBlur均值滤波