zoukankan      html  css  js  c++  java
  • 基于vue和canvas开发的作业批改,包含画笔、橡皮擦、拖拽到指定位置、保存批改痕迹等功能案例

    前言

    由于业务需求,需要开发一个可以批改作业的组件,网上搜的一些插件不太符合业务需求,没办法>_<只能自己写呗(此处掉头发两根~)。

    其原理是在学生提交的图片上使用画笔批改、橡皮擦、拖拽缩放、旋转、按步骤减分、和其他一些辅助功能操作,期间踩了很多坑,但也是在学习中成长,这里贴出来可以给迷茫的人一个参考,也给自己记录一下。代码写的通俗易懂,我觉得大家只要有点基础都可以看懂,这个案例不是最完美的,但是可以在这个基础上继续完善。

    演示

    整体的功能演示

    整体的功能

    保存生成的批改痕迹-base64文件预览(包含拖拽的内容)

    保存生成的base64文件预览

    补充

    画笔部分可以用canvas平滑曲线优化,亲测效果非常nice

    全部代码

    index.vue

    <template>
      <div class="correction-wrap">
        <div class="header" />
        <div class="main">
          <div class="left-wrap" />
          <div class="center-wrap">
            <canvas-container ref="canvasContRef" :img-url="imgUrl" />
            <canvas-container ref="canvasContRef" :img-url="imgUrl1" />
          </div>
          <div class="right-wrap">
            <div draggable="true" class="step-item step" @dragstart="dragstart($event, 'step' , 5)">5</div>
            <div draggable="true" class="step-item chapter" @dragstart="dragstart($event, 'chapter' , 5)">
              <img :src="require('./chapter.png')" alt="">
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    import CanvasContainer from './canvasContainer'
    export default {
      name: 'Index',
      components: {
        CanvasContainer
      },
      data() {
        return {
          imgUrl: 'https://lwlblog.top/images/demo/el-tabs.jpg',
          imgUrl1: 'https://lwlblog.top/images/demo/el-tabs-top.jpg'
        }
      },
      mounted() {
    
      },
      methods: {
        // 拖拽开始事件 type: 拖拽的对象(step: 按步骤减分,text: 文字批注) value: 值
        dragstart(e, type, value) {
          // e.preventDefault()
          const data = JSON.stringify({ type, value })
          e.dataTransfer.setData('data', data)
        }
      }
    }
    </script>
    
    <style>
    .correction-wrap{
       100%;
      height: 100%;
      display: flex;
      flex-direction: column;
      transform: rotate();
    }
    .header{
       100%;
      height: 100px;
      flex-shrink: 0;
      background-color: #0099CC;
    }
    .main{
      flex: 1;
      display: flex;
    }
    .main .left-wrap{
       200px;
      background-color: #9999FF;
    }
    .main .center-wrap{
      flex: 1;
      height: calc(100vh - 100px);
      position: relative;
      background-color: #CCFFFF;
      overflow: scroll;
    }
    .main .right-wrap{
       300px;
      background-color: #CCCCFF;
    }
    button{
       80px;
      height: 40px;
    }
    .step-item.step{
       80px;
      height: 40px;
      text-align: center;
      line-height: 40px;
      border-radius: 4px;
      background-color: #009999;
    }
    </style>
    

    canvasContainer.vue

    <template>
      <div ref="canvasContRef" class="canvas-container">
        <div v-show="imgIsLoad" ref="canvasWrapRef" class="canvas-wrap" :style="canvasStyle">
          <!-- 用于拖拽内容的截图,不显示 -->
          <drag-step ref="stepWrapRef" :drag-list="dragList" :drag-style="stepWrapStyle" :is-hide="true" />
          <!-- 用于显示拖拽内容 -->
          <drag-step :drag-list="dragList" />
          <!-- 画布 -->
          <canvas ref="canvasRef" @drop="drop" @dragover="dragover" />
        </div>
        <canvas-toolbar @changeTool="changeTool" />
      </div>
    </template>
    
    <script>
    import CanvasToolbar from './canvasToolbar'
    import DragStep from './dragStep'
    import domtoimage from 'dom-to-image'
    import { mixImg } from 'mix-img'
    export default {
      name: 'CanvasContainer',
      components: { CanvasToolbar, DragStep },
      props: {
        imgUrl: {
          type: String,
          require: true,
          default: ''
        }
      },
      data() {
        return {
          canvas: null,
          ctx: null,
          imgIsLoad: false,
          // 所使用的工具名称 drag draw
          toolName: '',
          // 画布的属性值
          canvasValue: {
             0,
            height: 0,
            left: 0,
            top: 0,
            scale: 1,
            rotate: 0,
            cursor: 'default'
          },
          // 拖拽的元素列表
          dragList: [],
          // 记录当前画布的操作(暂时没用)
          imgData: null,
          // 记录每一步操作
          preDrawAry: []
        }
      },
      computed: {
        canvasStyle() {
          const { width, height, left, top, scale, rotate, cursor } = this.canvasValue
          return {
             `${width}px`,
            height: `${height}px`,
            left: `${left}px`,
            top: `${top}px`,
            transform: `rotate(${rotate}deg) scale(${scale})`,
            cursor: cursor
            // backgroundImage: `url(${this.imgUrl})`
          }
        },
        // 上层拖拽样式(用于dom截图)
        stepWrapStyle() {
          const { width, height } = this.canvasValue
          return {
             `${width}px`,
            height: `${height}px`
          }
        }
      },
      mounted() {
        const canvas = this.$refs.canvasRef
        const ctx = canvas.getContext('2d')
    
        this.loadImg(canvas, ctx)
        this.changeTool('drag')
        // 监听窗口发生变化
        window.addEventListener('resize', this.reload)
      },
      beforeDestroy() {
        window.removeEventListener('resize', this.reload)
      },
      methods: {
        // 监听窗口发生变化
        reload() {
          this.$nextTick(() => {
            const canvas = this.$refs.canvasRef
            this.canvasCenter(canvas)
          })
        },
    
        // 加载图片
        loadImg(canvas, ctx) {
          const img = new Image()
          // 图片加载成功
          img.onload = () => {
            console.log('图片加载成功')
            this.imgIsLoad = true
            canvas.width = img.width
            canvas.height = img.height
            this.$set(this.canvasValue, 'width', img.width)
            this.$set(this.canvasValue, 'height', img.height)
    
            canvas.style.backgroundImage = `url(${this.imgUrl})`
            this.canvasCenter(canvas)
    
            // this.loadHistory(ctx)
          }
          // 图片加载失败
          img.onerror = () => {
            console.log('图片加载失败!')
          }
          img.src = this.imgUrl
        },
    
        // 加载历史画笔记录 img是保存的base64格式的画笔轨迹图
        loadHistory(ctx, img) {
          const imgCatch = new Image()
          imgCatch.src = img
          imgCatch.onload = () => {
            ctx.drawImage(imgCatch, 0, 0, imgCatch.width, imgCatch.height)
          }
        },
    
        // 切换工具
        changeTool(name) {
          console.log(name)
          // 清除拖拽的按下事件
          const wrapRef = this.$refs.canvasWrapRef
          wrapRef.onmousedown = null
          const canvas = this.$refs.canvasRef
          const ctx = canvas.getContext('2d')
          switch (name) {
            case 'drag':
              this.dragCanvas(canvas)
              break
            case 'draw':
              this.drawPaint(canvas, ctx)
              break
            case 'eraser':
              this.eraser(canvas, ctx)
              break
            case 'revoke':
              this.revoke(canvas, ctx)
              break
            case 'clear':
              this.clearCanvas(canvas, ctx)
              break
            case 'save':
              this.saveCanvas(canvas)
              break
            case 'rotate':
              this.$set(this.canvasValue, 'rotate', this.canvasValue.rotate + 90)
              break
            case 'enlarge':
              this.$set(this.canvasValue, 'scale', this.canvasValue.scale + 0.2)
              break
            case 'narrow':
              this.$set(this.canvasValue, 'scale', this.canvasValue.scale - 0.2)
              break
            default:
              break
          }
        },
    
        // 拖拽画布
        dragCanvas(canvas) {
          console.log('dragCanvas')
          // 清除上次监听的事件
          const wrapRef = this.$refs.canvasWrapRef
          const container = this.getPosition(this.$refs.canvasContRef)
          let isDown = false
    
          wrapRef.onmousedown = (e) => {
            isDown = true
            this.$set(this.canvasValue, 'cursor', 'move')
            // 算出鼠标相对元素的位置
            const disX = e.clientX - wrapRef.offsetLeft
            const disY = e.clientY - wrapRef.offsetTop
    
            document.onmousemove = (e) => {
              if (!isDown) return
              // 用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
              let left = e.clientX - disX
              let top = e.clientY - disY
    
              // 判断canvas是否在显示范围内,减4是border=2px的原因
              const width = container.width - canvas.width / 2 - 4
              const height = container.height - canvas.height / 2 - 4
              left = Math.min(Math.max(-canvas.width / 2, left), width)
              top = Math.min(Math.max(-canvas.height / 2, top), height)
    
              this.$set(this.canvasValue, 'left', left)
              this.$set(this.canvasValue, 'top', top)
            }
    
            document.onmouseup = (e) => {
              isDown = false
              document.onmousemove = null
              this.$set(this.canvasValue, 'cursor', 'default')
            }
          }
        },
    
        // 画笔
        drawPaint(canvas, ctx) {
          // const wrapRef = this.$refs.canvasWrapRef
          canvas.onmousedown = (e) => {
            this.$set(this.canvasValue, 'cursor', 'crosshair')
            this.imgData = ctx.getImageData(0, 0, canvas.width, canvas.height)
            this.preDrawAry.push(this.imgData)
            ctx.beginPath()
            ctx.lineWidth = 2
            ctx.strokeStyle = 'red'
            ctx.moveTo(e.offsetX, e.offsetY)
    
            canvas.onmousemove = (e) => {
              ctx.lineTo(e.offsetX, e.offsetY)
              ctx.stroke()
            }
          }
    
          // 鼠标抬起取消鼠标移动的事件
          document.onmouseup = (e) => {
            canvas.onmousemove = null
            ctx.closePath()
            this.$set(this.canvasValue, 'cursor', 'default')
          }
    
          // 鼠标移出画布时 移动事件取消
          // document.onmouseout = (e) => {
          //   document.onmousemove = null
          //   ctx.closePath()
          // }
        },
    
        // 橡皮擦
        eraser(canvas, ctx, r = 10) {
          // const wrapRef = this.$refs.canvasWrapRef
          canvas.onmousedown = (e) => {
            this.imgData = ctx.getImageData(0, 0, canvas.width, canvas.height)
            this.preDrawAry.push(this.imgData)
            let x1 = e.offsetX
            let y1 = e.offsetY
    
            // 鼠标第一次点下的时候擦除一个圆形区域,同时记录第一个坐标点
            ctx.save()
            ctx.beginPath()
            ctx.arc(x1, y1, r, 0, 2 * Math.PI)
            ctx.clip()
            ctx.clearRect(0, 0, canvas.width, canvas.height)
            ctx.restore()
    
            canvas.onmousemove = (e) => {
              const x2 = e.offsetX
              const y2 = e.offsetY
              // 获取两个点之间的剪辑区域四个端点
              const asin = r * Math.sin(Math.atan((y2 - y1) / (x2 - x1)))
              const acos = r * Math.cos(Math.atan((y2 - y1) / (x2 - x1)))
    
              // 保证线条的连贯,所以在矩形一端画圆
              ctx.save()
              ctx.beginPath()
              ctx.arc(x2, y2, r, 0, 2 * Math.PI)
              ctx.clip()
              ctx.clearRect(0, 0, canvas.width, canvas.height)
              ctx.restore()
    
              // 清除矩形剪辑区域里的像素
              ctx.save()
              ctx.beginPath()
              ctx.moveTo(x1 + asin, y1 - acos)
              ctx.lineTo(x2 + asin, y2 - acos)
              ctx.lineTo(x2 - asin, y2 + acos)
              ctx.lineTo(x1 - asin, y1 + acos)
              ctx.closePath()
              ctx.clip()
              ctx.clearRect(0, 0, canvas.width, canvas.height)
              ctx.restore()
    
              // 记录最后坐标
              x1 = x2
              y1 = y2
            }
          }
    
          // 鼠标抬起取消鼠标移动的事件
          document.onmouseup = (e) => {
            canvas.onmousemove = null
          }
    
          // 鼠标移出画布时 移动事件取消
          // canvas.onmouseout = (e) => {
          //   canvas.onmousemove = null
          // }
        },
    
        // 撤销
        revoke(canvas, ctx) {
          if (this.preDrawAry.length > 0) {
            const popData = this.preDrawAry.pop()
            ctx.putImageData(popData, 0, 0)
          } else {
            this.clearCanvas(canvas, ctx)
          }
        },
    
        // 清空画布
        clearCanvas(canvas, ctx) {
          ctx.clearRect(0, 0, canvas.width, canvas.height)
        },
    
        // 保存
        saveCanvas(canvas) {
          const wrapRef = this.$refs.stepWrapRef.$el
          const { width, height } = this.canvasValue
          const image = canvas.toDataURL('image/png')
          console.log(this.preDrawAry)
          domtoimage.toPng(wrapRef)
            .then((dataUrl) => {
              console.log(dataUrl)
              const mixConfig = {
                'base': {
                  'backgroundImg': image,
                  'width': width,
                  'height': height,
                  'quality': 0.8,
                  'fileType': 'png'
                },
                'dynamic': [{
                  'type': 1,
                  'size': {
                    'dWidth': width,
                    'dHeight': height
                  },
                  'position': {
                    'x': 0,
                    'y': 0
                  },
                  'imgUrl': dataUrl
                }]
              }
              mixImg(mixConfig).then(res => {
                console.log(res.data.base64)
              })
            })
            .catch((error) => {
              console.error('oops, something went wrong!', error)
            })
        },
    
        // 获取dom元素在页面中的位置与大小
        getPosition(target) {
          const width = target.offsetWidth
          const height = target.offsetHeight
          let left = 0
          let top = 0
          do {
            left += target.offsetLeft || 0
            top += target.offsetTop || 0
            target = target.offsetParent
          } while (target)
          return { width, height, left, top }
        },
    
        // canvas居中显示
        canvasCenter(canvas) {
          const wrap = this.getPosition(this.$refs.canvasContRef)
          const left = (wrap.width - canvas.width) / 2
          const top = (wrap.height - canvas.height) / 2
          this.$set(this.canvasValue, 'left', left)
          this.$set(this.canvasValue, 'top', top)
        },
    
        drop(e) {
          // e.preventDefault()
          const { type, value } = JSON.parse(e.dataTransfer.getData('data'))
          console.log(e.offsetX, e.offsetY)
          this.dragList.push({
            x: e.offsetX,
            y: e.offsetY,
            type,
            value
          })
        },
    
        dragover(e) {
          // 取消默认动作是为了drop事件可以触发
          e.preventDefault()
          // console.log(e)
        }
      }
    }
    </script>
    
    <style scoped>
    .canvas-container{
      position: relative;
       100%;
      height: 400px;
      border: 2px solid #f0f;
      background-color: lightblue;
      box-sizing: border-box;
      overflow: hidden;
    }
    .canvas-container .canvas-wrap{
      position: absolute;
      transition: transform .3s;
      /* background-color: #ff0; */
    }
    .canvas-toolbar{
       720px;
      height: 40px;
      position: absolute;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      background-color: rgba(0, 0, 0, .3);
      user-select: none;
    }
    </style>
    

    canvasToolbar.vue

    <template>
      <div class="canvas-toolbar">
        <button v-for="tool in tools" :key="tool.name" @click="changeTool(tool.code)">{{ tool.name }}</button>
      </div>
    </template>
    
    <script>
    export default {
      props: {
    
      },
      data() {
        return {
          tools: [{
            code: 'drag',
            name: '拖动'
          }, {
            code: 'draw',
            name: '画笔'
          }, {
            code: 'eraser',
            name: '橡皮'
          }, {
            code: 'revoke',
            name: '撤销'
          }, {
            code: 'clear',
            name: '重置'
          }, {
            code: 'save',
            name: '保存'
          }, {
            code: 'rotate',
            name: '旋转'
          }, {
            code: 'enlarge',
            name: '放大'
          }, {
            code: 'narrow',
            name: '缩小'
          }]
        }
      },
      methods: {
        changeTool(name, value) {
          this.$emit('changeTool', name)
        },
    
        changeScale() {
    
        }
      }
    }
    </script>
    
    <style>
    
    </style>
    

    dragStep.vue

    <template>
      <div class="drag-step" :class="{'hide': isHide}" :style="dragStyle">
        <div
          v-for="(step, index) in dragList"
          :key="index"
          class="drag-item"
          :class="step.type"
          :style="{
            left: step.x - 30 + 'px',
            top: step.y - 15 + 'px'
          }"
    
          @click="clickStepItem(step.value)"
        >
          <span v-if="step.type === 'step'">{{ step.value }}</span>
          <img v-if="step.type === 'chapter'" draggable="false" :src="require('./chapter.png')" alt="">
        </div>
      </div>
    </template>
    
    <script>
    export default {
      props: {
        // 是否隐藏在下方(用于domtoimg截图)
        isHide: {
          type: Boolean,
          default: false
        },
        // 拖拽的元素列表
        dragList: {
          type: Array,
          default: () => []
        },
        // 应该与 isHide=true 时使用
        dragStyle: {
          type: Object,
          default: () => ({})
        }
      },
      methods: {
        clickStepItem(value) {
          console.log(value)
        }
      }
    }
    </script>
    
    <style scoped>
    .drag-step.hide{
      position: absolute;
      top: 0;
      left: 0;
      z-index: -1;
    }
    .drag-item{
      position: absolute;
      user-select: none;
    }
    .drag-item.step{
       60px;
      height: 30px;
      text-align: center;
      line-height: 30px;
      color: #fff;
      border-radius: 4px;
      background-color: aquamarine;
    }
    .drag-item.chapter{
    
    }
    </style>
    
    完结~
  • 相关阅读:
    php无法保存cookies问题解决
    织梦(DEDECMS)首页调用相关投票的方法(自动更新)
    php导出任意mysql数据库中的表去excel文件
    学用.NET实现AutoCAD二次开发
    JS自动滚屏程序
    object c 的入门教程
    php如何截取字符串并以零补齐str_pad() 函数
    自己制作软键盘的几个关键技术解析
    php出现php_network_getaddresses的解决方法
    wamp环境下php命令运行时出现错误:无法启动此程序,因为计算机中丢失OCI.dll。尝试重新安装该程序以解决此问题
  • 原文地址:https://www.cnblogs.com/lwlblog/p/14868147.html
Copyright © 2011-2022 走看看