美文网首页
vue组件 canvas 实现图片涂鸦

vue组件 canvas 实现图片涂鸦

作者: 轩_7ca0 | 来源:发表于2021-04-08 16:01 被阅读0次
    效果图

    方案背景

    需求

    1 需要对图片进行标注,导出图片。

    2 需要标注N多图片最后同时保存。

    3 需要根据多边形区域数据(区域、颜色、名称)标注。

    对应方案

    1 用canvas实现涂鸦、圆形、矩形的绘制,最终生成图片base64编码用于上传

    2 大量图片批量上传很耗时间,为了提高用户体验,改为只实现圆形、矩形绘制,最终保存成坐标,下次显示时根据坐标再绘制。

    3 多边形区域的显示是根据坐标点绘制,名称显示的位置为多边形质心。

    代码

    <template>

    <!-- canvas图片区域选择画布 -->

      <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,

        default:'/img/quyu_bg.jpg'

          },

          info: { // 位置点信息

            type: Array

          },

          width: { // 绘图区域宽度

            type: String,

        default:'600'

          },

          height: { // 绘图区域高度

            type: String,

        default:'400'

          },

          lineColor: { // 画笔颜色

            type: String,

            default: 'red'

          },

          lineWidth: { // 画笔宽度

            type: Number,

            default: 2

          },

          lineType: { // 画笔类型

            type: String,

            default: 'rec'

          }

        },

        watch: {

          info (val) {

          console.log(val)

            if (val) {

              this.initDraw()

            }

          }

        },

        data () {

          return {

            // 同一页面多次渲染时,用于区分元素的id

            radom: 'canvas1',

            // 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 = '/img/quyu_bg.jpg'

            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) {

            //清空画布

            this.clean()

            if (this.canDraw) {

              this.canvasMoveUse = true

              // client是基于整个页面的坐标,layer是基于画布坐标,offset是cavas距离pictureDetail顶部以及左边的距离

              const canvasX = e.layerX - e.target.parentNode.offsetLeft

              const canvasY = e.layerY - 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是基于整个页面的坐标,layer是基于画布坐标,offset是cavas距离pictureDetail顶部以及左边的距离

              let canvasX = e.layerX - e.target.parentNode.offsetLeft

              let canvasY = e.layerY - 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

              this.$emit('getimgcoordinate',this.drawInfo)

            }

          },

          // 获取坐标信息

          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

        绘图区域宽度 width: 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

    相关文章

      网友评论

          本文标题:vue组件 canvas 实现图片涂鸦

          本文链接:https://www.haomeiwen.com/subject/walvkltx.html