我的编程空间,编程开发者的网络收藏夹
学习永远不晚

vue下如何利用canvas实现在线图片标注

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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-button
            v-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.class="lazy" data-src = this.imgPath;
      image.onload = function () {//图片加载完,再draw 和 toDataURL
        if (image.complete) {
          _this.img = image
          let 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.class="lazy" data-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.class="lazy" data-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.class="lazy" data-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.class="lazy" data-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需要先清空文字,再撤销,否则画布上会一直存在一个输入框。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

vue下如何利用canvas实现在线图片标注

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

如何利用canvas实现图片压缩功能

小编给大家分享一下如何利用canvas实现图片压缩功能,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!项目中做身份证识别时,需要传送图片的 base64 格式编码,
2023-06-09

如何利用canvas实现图片下载功能来实现浏览器兼容问题

小编给大家分享一下如何利用canvas实现图片下载功能来实现浏览器兼容问题,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!前言:项目中需要实现图片下载功能,第一个想
2023-06-09

如何在Retrofit中利用Rxjava实现一个图片下载功能

这篇文章将为大家详细讲解有关如何在Retrofit中利用Rxjava实现一个图片下载功能,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。首先我们看一下Retrofit常规的用法,在不使用Rxj
2023-05-31

Vue下如何使用press实现图片放大功能

本篇内容介绍了“Vue下如何使用press实现图片放大功能”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!VuePress 由两部分组成:一个
2023-07-04

如何在Android中利用OkHttp实现一个图片上传功能

本篇文章给大家分享的是有关如何在Android中利用OkHttp实现一个图片上传功能,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。实现方法如下:object UploadFil
2023-05-31

如何在Android中利用Glide实现一个图片圆角功能

如何在Android中利用Glide实现一个图片圆角功能?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。一、简介:介绍两种使用 BitmapTransformation 来实
2023-05-31

如何在Android中利用imageview实现一个图片缩放功能

如何在Android中利用imageview实现一个图片缩放功能?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。Android 自定义imageview实现图片缩放实例详解 觉得
2023-05-31

如何在Android应用中利用phonegap实现从相册中获取图片

今天就跟大家聊聊有关如何在Android应用中利用phonegap实现从相册中获取图片,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。具体方法如下
2023-05-31

如何在Android应用中利用ADT实现一个离线下载功能

今天就跟大家聊聊有关如何在Android应用中利用ADT实现一个离线下载功能,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。Android ADT 离线下载操作步骤可以利用在线安装地址
2023-05-31

如何在Android中利用OkHttp与php实现一个图片上传功能

这篇文章给大家介绍如何在Android中利用OkHttp与php实现一个图片上传功能,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。今天写项目的时候需要多图片上传,就用okhttp简单写一个例子。public class
2023-05-31

在Java项目中如何利用多线程实现文件下载功能

这篇文章将为大家详细讲解有关在Java项目中如何利用多线程实现文件下载功能,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。具体内容如下import java.io.File; import j
2023-05-31

如何在Android应用中利用ImageView实现一个选择本地图片功能

这期内容当中小编将会给大家带来有关如何在Android应用中利用ImageView实现一个选择本地图片功能,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。布局文件:
2023-05-31

如何在java项目中利用ocr实现一个图片文字识别功能

这期内容当中小编将会给大家带来有关如何在java项目中利用ocr实现一个图片文字识别功能,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。最近在开发的时候需要识别图片中的一些文字,网上找了相关资料之后,发现g
2023-05-31

编程热搜

目录