zoukankan      html  css  js  c++  java
  • vue组件:canvas实现图片涂鸦功能

    方案背景

    需求

    1. 需要对图片进行标注,导出图片。
    2. 需要标注N多图片最后同时保存。
    3. 需要根据多边形区域数据(区域、颜色、名称)标注。

    对应方案

    1. 用canvas实现涂鸦、圆形、矩形的绘制,最终生成图片base64编码用于上传
    2. 大量图片批量上传很耗时间,为了提高用户体验,改为只实现圆形、矩形绘制,最终保存成坐标,下次显示时根据坐标再绘制。
    3. 多边形区域的显示是根据坐标点绘制,名称显示的位置为多边形质心。

    代码

    <template>
      <div>
        <canvas
          :id="radom"
          :class="{canDraw: 'canvas'}"
          :width="width"
          :height="height"
          :style="{'width':`${width}px`,'height':`${height}px`}"
          @mousedown="canvasDown($event)"
          @mouseup="canvasUp($event)"
          @mousemove="canvasMove($event)"
          @touchstart="canvasDown($event)"
          @touchend="canvasUp($event)"
          @touchmove="canvasMove($event)">
        </canvas>
      </div>
    </template>
    <script>
      // import proxy from './proxy.js'
      const uuid = require('node-uuid')
      export default {
        props: {
          canDraw: { // 图片路径
            type: Boolean,
            default: true
          },
          url: { // 图片路径
            type: String
          },
          info: { // 位置点信息
            type: Array
          },
           { // 绘图区域宽度
            type: String
          },
          height: { // 绘图区域高度
            type: String
          },
          lineColor: { // 画笔颜色
            type: String,
            default: 'red'
          },
          lineWidth: { // 画笔宽度
            type: Number,
            default: 2
          },
          lineType: { // 画笔类型
            type: String,
            default: 'circle'
          }
        },
        watch: {
          info (val) {
            if (val) {
              this.initDraw()
            }
          }
        },
        data () {
          return {
            // 同一页面多次渲染时,用于区分元素的id
            radom: uuid.v4(),
            // canvas对象
            context: {},
            // 是否处于绘制状态
            canvasMoveUse: false,
            // 绘制矩形和椭圆时用来保存起始点信息
            beginRec: {
              x: '',
              y: '',
              imageData: ''
            },
            // 储存坐标信息
            drawInfo: [],
            // 背景图片缓存
            img: new Image()
          }
        },
        mounted () {
          this.initDraw()
        },
        methods: {
          // 初始化绘制信息
          initDraw () {
            // 初始化画布
            const canvas = document.getElementById(this.radom)
            this.context = canvas.getContext('2d')
            // 初始化背景图片
            this.img.setAttribute('crossOrigin', 'Anonymous')
            this.img.src = this.url
            this.img.onerror = () => {
              var timeStamp = +new Date()
              this.img.src = this.url + '?' + timeStamp
            }
            this.img.onload = () => {
              this.clean()
            }
            // proxy.getBase64({imgUrl: this.url}).then((res) => {
            //   if (res.code * 1 === 0) {
            //     this.img.src = 'data:image/jpeg;base64,'+res.data
            //     this.img.onload = () => {
            //       this.clean()
            //     }
            //   }
            // })
            // 初始化画笔
            this.context.lineWidth = this.lineWidth
            this.context.strokeStyle = this.lineColor
          },
          // 鼠标按下
          canvasDown (e) {
            if (this.canDraw) {
              this.canvasMoveUse = true
              // client是基于整个页面的坐标,offset是cavas距离pictureDetail顶部以及左边的距离
              const canvasX = e.clientX - e.target.parentNode.offsetLeft
              const canvasY = e.clientY - e.target.parentNode.offsetTop
              // 记录起始点和起始状态
              this.beginRec.x = canvasX
              this.beginRec.y = canvasY
              this.beginRec.imageData = this.context.getImageData(0, 0, this.width, this.height)
              // 存储本次绘制坐标信息
              this.drawInfo.push({
                x: canvasX / this.width,
                y: canvasY / this.height,
                type: this.lineType
              })
            }
          },
          Area (p0,p1,p2) {
            let area = 0.0 ;
            area = p0.x * p1.y + p1.x * p2.y + p2.x * p0.y - p1.x * p0.y - p2.x * p1.y - p0.x * p2.y;
            return area / 2 ;
          },
          // 计算多边形质心
          getPolygonAreaCenter (points) {
            let sum_x = 0;
            let sum_y = 0;
            let sum_area = 0;
            let p1 = points[1];
            for (var i = 2; i < points.length; i++) {
              let p2 = points[i];
              let area = this.Area(points[0],p1,p2) ;
              sum_area += area ;
              sum_x += (points[0].x + p1.x + p2.x) * area;
              sum_y += (points[0].y + p1.y + p2.y) * area;
              p1 = p2 ;
            }
            return {
              x: sum_x / sum_area / 3,
              y: sum_y / sum_area / 3
            }
          },
          // 根据坐标信息绘制图形
          drawWithInfo () {
            this.info.forEach(item => {
              this.context.beginPath()
              if (!item.type) {
                // 设置颜色
                this.context.strokeStyle = item.regionColor
                this.context.fillStyle = item.regionColor
                // 绘制多边形的边
                if (typeof item.region === 'string') {
                  item.region = JSON.parse(item.region)
                }
                item.region.forEach(point => {
                  this.context.lineTo(point.x * this.width, point.y * this.height)
                })
                this.context.closePath()
                // 在多边形质心标注文字
                let point = this.getPolygonAreaCenter(item.region)
                this.context.fillText(item.areaName, point.x * this.width, point.y * this.height)
              } else if (item.type === 'rec') {
                this.context.rect(item.x * this.width, item.y * this.height, item.w * this.width, item.h * this.height)
              } else if (item.type === 'circle') {
                this.drawEllipse(this.context, (item.x + item.a) * this.width, (item.y + item.b) * this.height, item.a > 0 ? item.a * this.width : -item.a * this.width, item.b > 0 ? item.b * this.height : -item.b * this.height)
              }
              this.context.stroke()
            })
          },
          // 鼠标移动时绘制
          canvasMove (e) {
            if (this.canvasMoveUse && this.canDraw) {
              // client是基于整个页面的坐标,offset是cavas距离pictureDetail顶部以及左边的距离
              let canvasX = e.clientX - e.target.parentNode.offsetLeft
              let canvasY = e.clientY - e.target.parentNode.offsetTop
              if (this.lineType === 'rec') { // 绘制矩形时恢复起始点状态再重新绘制
                this.context.putImageData(this.beginRec.imageData, 0, 0)
                this.context.beginPath()
                this.context.rect(this.beginRec.x, this.beginRec.y, canvasX - this.beginRec.x, canvasY - this.beginRec.y)
                let info = this.drawInfo[this.drawInfo.length - 1]
                info.w = canvasX / this.width - info.x
                info.h = canvasY / this.height - info.y
              } else if (this.lineType === 'circle') { // 绘制椭圆时恢复起始点状态再重新绘制
                this.context.putImageData(this.beginRec.imageData, 0, 0)
                this.context.beginPath()
                let a = (canvasX - this.beginRec.x) / 2
                let b = (canvasY - this.beginRec.y) / 2
                this.drawEllipse(this.context, this.beginRec.x + a, this.beginRec.y + b, a > 0 ? a : -a, b > 0 ? b : -b)
                let info = this.drawInfo[this.drawInfo.length - 1]
                info.a = a / this.width
                info.b = b / this.height
              }
              this.context.stroke()
            }
          },
          // 绘制椭圆
          drawEllipse (context, x, y, a, b) {
            context.save()
            var r = (a > b) ? a : b
            var ratioX = a / r
            var ratioY = b / r
            context.scale(ratioX, ratioY)
            context.beginPath()
            context.arc(x / ratioX, y / ratioY, r, 0, 2 * Math.PI, false)
            context.closePath()
            context.restore()
          },
          // 鼠标抬起
          canvasUp (e) {
            if (this.canDraw) {
              this.canvasMoveUse = false
            }
          },
          // 获取坐标信息
          getInfo () {
            return this.drawInfo
          },
          // 清空画布
          clean () {
            this.context.drawImage(this.img, 0, 0, this.width, this.height)
            this.drawInfo = []
            if (this.info && this.info.length !== 0) this.drawWithInfo()
          }
        }
      }
    </script>
    <style lang="scss" scoped>
      .canvas{
        cursor: crosshair;
      }
    </style>
    
    

    必须传入的参数

    • 图片路径
    url: string
    
    • 绘图区域宽度
     string
    
    • 绘图区域高度
    height: string
    

    选择传入的参数

    • 是否可以绘制,默认true
    canDraw: boolean
    
    • 坐标点信息,不传入则不绘制
    info: string
    
    • 是否可绘制,默认true
    canDraw: boolean
    
    • 绘图颜色,默认red
    lineColor: string
    
    • 绘图笔宽度,默认2
    lineWidth: number
    
    • 绘图笔类型,rec、circle,默认rec
    lineType: string
    

    可以调用的方法

    • 清空画布
    clean()
    
    • 返回坐标点信息
    getInfo()
    

    特殊说明

    • canvas对象不能获得坐标,是通过父元素坐标获取的,所以该组件的父元素以上的层级不能有太多的定位、嵌套,否则绘制坐标会偏移。
    • 域名不同的图片可能存在跨域问题,看过很多资料没有太好的办法,最后项目中是用node服务做了一个图片转为base64的接口,再给canvas绘制解决的。并不一定适用于其他项目,如果有更好的办法解决欢迎分享。
    • 导出坐标点数据只能导出规则图案的坐标点,因为随意涂鸦的坐标点太多时会崩溃的(虽然没试过具体到什么程度会崩溃),如果有高性能的实现方式欢迎分享。
    • 如果涂鸦后保存再请求图片url出现请求不到的情况,是因为CDN缓存的问题,在图片路径后面拼个随机码就可以解决。
    原文地址:https://segmentfault.com/a/1190000016852958
  • 相关阅读:
    Atitit 图像处理30大经典算法attilax总结
    Atitit 图像清晰度 模糊度 检测 识别 评价算法 源码实现attilax总结
    Atitit  rgb yuv  hsv HSL 模式和 HSV(HSB) 图像色彩空间的区别
    Atitit  从 RGB 到 HSL 或 HSV 的转换
    Atitit 图像清晰度 模糊度 检测 识别 评价算法 原理
    Atitit 修改密码的功能流程设计 attilax总结
    atitit 点播系统 概览 v2 qb1.docx
    Atitit dsl exer v3 qb3 新特性
    atitit.TokenService v3 qb1  token服务模块的设计 新特性.docx
    Atitit 异常机制与异常处理的原理与概论
  • 原文地址:https://www.cnblogs.com/lalalagq/p/9900826.html
Copyright © 2011-2022 走看看