触摸屏+canvas画布实现橡皮和画笔功能

1.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{
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  transform: rotate();
}
.header{
  width: 100%;
  height: 100px;
  flex-shrink: 0;
  background-color: #0099CC;
}
.main{
  flex: 1;
  display: flex;
}
.main .left-wrap{
  width: 200px;
  background-color: #9999FF;
}
.main .center-wrap{
  flex: 1;
  height: calc(100vh - 100px);
  position: relative;
  background-color: #CCFFFF;
  overflow: scroll;
}
.main .right-wrap{
  width: 300px;
  background-color: #CCCCFF;
}
button{
  width: 80px;
  height: 40px;
}
.step-item.step{
  width: 80px;
  height: 40px;
  text-align: center;
  line-height: 40px;
  border-radius: 4px;
  background-color: #009999;
}
</style>

2.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: {
        width: 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: `${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: `${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;
  width: 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{
  width: 720px;
  height: 40px;
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  background-color: rgba(0, 0, 0, .3);
  user-select: none;
}
</style>

3.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>

4.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{
  width: 60px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  color: #fff;
  border-radius: 4px;
  background-color: aquamarine;
}
.drag-item.chapter{
 
}
</style>

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Three.js中实现涂鸦和橡皮擦功能,可以按照以下步骤进行: 1. 在场景中添加一个可绘制的画布,比如一个平面或立方体,在其表面上进行绘画操作,需要使用Three.js的材质和纹理来实现。 2. 创建一个画笔对象和一个橡皮擦对象,分别用于绘制和擦除操作。可以使用Three.js的线条(THREE.Line)或粒子系统(THREE.Points)来实现。 3. 监听用户的鼠标或触摸事件,根据用户的操作在画布上绘制或擦除图形。需要根据用户的绘制和擦除操作来切换画笔橡皮擦对象。 ```javascript var isDrawing = false; var isErasing = false; var mouse = new THREE.Vector2(); function onMouseDown( event ) { if ( isErasing ) { // 使用橡皮擦对象进行擦除操作 erase( event ); } else { // 使用画笔对象进行绘制操作 isDrawing = true; draw( event ); } } function onMouseMove( event ) { if ( isErasing ) { // 使用橡皮擦对象进行擦除操作 erase( event ); } else if ( isDrawing ) { // 使用画笔对象进行绘制操作 draw( event ); } } function onMouseUp( event ) { isDrawing = false; } function draw( event ) { // 获取鼠标位置 mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1; mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; // 将鼠标位置转换为Three.js坐标系中的向量 var vector = new THREE.Vector3( mouse.x, mouse.y, 0.5 ).unproject( camera ); // 使用画笔对象进行绘制 brush.position.copy( vector ); brush.geometry.vertices.push( vector.clone() ); brush.geometry.verticesNeedUpdate = true; } function erase( event ) { // 获取鼠标位置 mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1; mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; // 将鼠标位置转换为Three.js坐标系中的向量 var vector = new THREE.Vector3( mouse.x, mouse.y, 0.5 ).unproject( camera ); // 使用橡皮擦对象进行擦除 var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() ); var intersects = raycaster.intersectObjects( [ canvas ] ); if ( intersects.length > 0 ) { var point = intersects[ 0 ].point; for ( var i = 0; i < brush.geometry.vertices.length; i ++ ) { if ( brush.geometry.vertices[ i ].distanceTo( point ) < brushSize * 0.5 ) { brush.geometry.vertices.splice( i, 1 ); brush.geometry.verticesNeedUpdate = true; break; } } } } ``` 4. 最后,需要添加一些额外的功能,比如清除画布、设置画笔颜色和大小等。 需要注意的是,以上代码只是一个简单的示例,实际应用中还需要根据实际需求进行修改和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值