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.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.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需要先清空文字,再撤销,否则画布上会一直存在一个输入框。

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

(0)

相关推荐

  • vue移动端使用canvas签名的实现

    效果 canvas画板移动端 .gif 需求   在一些项目业务中,经常会使用到画板,让用户自己去写/画一些东西做标示,比如说在线签电子合约.签名等,如果不用插件,那么如何使用h5的canvas画布来实现这一需求呢? [本篇只讨论移动端,PC端请看上篇] 分析   很明显,我们需要一个canvas,关于canvas的一些基本操作可以在w3school或者别的一些平台上熟悉一下,其实本例也是基础操作.本案例在vue中完成.(脱离vue也一样.) 首先,需要一个canvas画布 其次,考虑逻辑 把逻

  • Vue中使用canvas方法总结

    下面就是小编带给大家的Vue中怎么使用canvas方法操作,希望能够给你们带来一定的帮助,谢谢大家的观看. 1.如果数组中都是canvas对象, vue如何能吧canvas对象渲染到页面.v-html只能渲染出一个字符串, 没办法像appendChild那样插入canvas对象. 2.项目架构是vue-cli的单页应用,如果在index.html入口主文件里面引入<script src='html2canvas.js'></script>: 3.这样每个组件都会加载此js,造成资源

  • Vue使用Canvas绘制图片、矩形、线条、文字,下载图片

    1 前言 1.1 业务场景 图片储存在后台中,根据图片的地址,在vue页面中,查看图片,并根据坐标标注指定区域. 由于浏览器的机制,使用 window.location.href 下载图片时,并不会保存到本地,会在浏览器打开. 2 实现原理 2.1 绘制画布 <el-dialog title="查看图片" :visible.sync="dialogJPG" append-to-body> <canvas id="mycanvas"

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

    目录 组件代码如下 在开发过程中遇到的问题 web端实现在线图片标注在此做下记录,功能类似微信截图时的标注,包含画线.框.箭头和文字输入,思路是利用canvas画布,先把要标注的图片使用drawImage方法画在画布上,然后定义画线.框.箭头和文字输入的方法调用 组件代码如下 <template>   <div class="draw">     <div class="drawTop" ref="drawTop"

  • vue下canvas裁剪图片实例讲解

    由于时间关系 代码没有做整理大家有什么不懂得可以留言: 代码的主题思路备注中都有 大家可以看看 我的博客中还有关于canvas绘制矩形的文章有需要的可以看一下: HTML代码: 第一行的canvas为裁剪后展示用:div中的canvas存放原有尺寸的图片 <canvas id="canvasImg1" style=" position: absolute; margin: 2px 0 0 0"></canvas> <div id=&qu

  • 基于cropper.js封装vue实现在线图片裁剪组件功能

    效果图如下所示, github:demo下载 cropper.js github:cropper.js 官网(demo) cropper.js 安装 npm或bower安装 npm install cropper # or bower install cropper clone下载:下载地址 git clone https://github.com/fengyuanchen/cropper.git 引用cropper.js 主要引用cropper.js跟cropper.css两个文件 <scri

  • Js利用Canvas实现图片压缩功能

    最近做的APP项目涉及到手机拍照上传图片,因为手机拍照的图片通常都比较大,所以上传的时候就会很慢.为此,需要对图片进行压缩处理来优化上传功能.以下是具体实现: /* * 图片压缩 * img 原始图片 * width 压缩后的宽度 * height 压缩后的高度 * ratio 压缩比率 */ function compress(img, width, height, ratio) { var canvas, ctx, img64; canvas = document.createElement

  • Vue利用canvas实现移动端手写板的方法

    本文介绍了Vue利用canvas实现移动端手写板的方法,分享给大家,具体如下: <template> <div class="hello"> <!--touchstart,touchmove,touchend,touchcancel 这--> <button type="" v-on:click="clear">清除</button> <button v-on:click=&quo

  • 微信小程序利用Canvas绘制图片和竖排文字详解

    前言 闲暇时间抽个空写了个三国杀武将手册的小程序,中间有个需求设计的是合成武将皮肤图.竖排的武将姓名.以及小程序码,然后提供保存图片到相册,最终让用户可以分享到朋友圈或其他平台.合成图片应该按照 Canvas 的文档来做都没什么问题,主要是有个竖排文字的需求,这里和大家分享一下. 正文 首先放一张最终保存到相册的图片吧~ 自我感觉良好,至少达到了我自己的预期吧~~~ 下面让我们一步一步来看看如何实现的吧. 整个图片分为三个部分: 武将图片 小程序码 武将文字信息 先来看一下 wxml 里面的代码

  • JS利用Canvas实现文字水印和图片水印合成

    目录 介绍 文字水印 图片水印 介绍 给图片添加水印可以帮助网站或作者保护自己的版权,或防止内容被别人利用.给图片添加水印分为添加文字水印和添加图片水印,水印一般都做成半透明的,这样不至于影响原图内容的浏览.Canvas 图片水印合成与 Canvas 实现图片压缩 原理基本相同: CanvasRenderingContext2D.drawImage(image, dx, dy, dWidth, dHeight) 方法可以从页面 DOM 元素作为图像源来根据坐标和大小重新绘制该图像. HTMLCa

  • vue 使用html2canvas将DOM转化为图片的方法

    一.前言 我发现将DOM转化为图片是一个非常常见的需求,而自己手动转是非常麻烦的,于是找到了html2canvas这个插件,既是用得比较多的也是维护得比较好的一个插件. 注意:版本比较多,这里介绍最新版 二.代码 1. 安装 npm install html2canvas --save 现在最新的版本应该是1.0.0,另外还有一个比较经典的版本是0.5.0,网上有许多关于这个版本的bug说明. 2. 使用 <div class="imageWrapper" ref="i

  • vue下拉刷新组件的开发及slot的使用详解

    "下拉刷新"和"上滑加载更多"功能在前端.尤其是移动端项目中非常重要,这里笔者由曾经做过的vue项目中的"blink"功能和各位探讨下[下拉刷新]组件的开发: 正式开篇 在前端项目的 components 文件夹下新建 pullRefreshView 文件夹,在其中新建组件 index.vue:(它代表"整个屏幕",通过slot插入页面其他内容而不是传统的设置遮罩层触发下拉刷新) 首先需要编写下拉刷新组件的 template,

  • 如何利用原生JS实现图片预览加上传(前后端交互)

    目录 前言 效果大致如下 前端代码 后端代码 总结 前言 最近在写vue项目的时候发现了个Vant的一个upload的图片上传的组件,就好奇了一下下,于是萌生了一个自己手写一个图片上传的组件的想法,您猜怎么着,还真给我实现了,那今天就和大家分享一下,大家有兴趣的可以了解一下啦,写进项目中可能会是个加分点哦!! 我们知道文件上传是需要前后端交互的,所以我这边给出前后端代码. 文件上传大致分为以下几个步骤 前端文件选择上传的文件类型 拿到文件信息 将选择的文件(视频或图片)在前端页面预览出来 将文件

随机推荐