Vue3 + 移动端 + 刮刮乐效果

374 阅读11分钟

前言

产品给了一个H5需求就是实现一个类似刮刮乐的效果,所以咱们就来研究研究一下!

踩坑

移动端跟PC端监听鼠标移动是不一样的,PC是mousemove,移动端是touchmove

伪代码

  1. 上面的图层遮盖内容并且可以刮的;
  2. 下面的图层就是对应的内容;
  3. 咱们选择canvas来当做上面的图层;

一、先实现上下两个图层

    <template>
      <div class="container">
        <div class="content">
          谢谢惠顾
        </div>

        <canvas />
      </div>
    </template>
    
 <style scoped lang="less">
  .container {
    position: relative;
    width: max-content;
    
    // 把内容的图层定位,并且把层级调到底部
    .content {
      position: absolute;
      z-index: -1;
      width: 200px;
      height: 150px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      font-size: 24px;
      background-color: skyblue;
    }
  }
</style>

通过上面代码就变成这个效果,因为画布还没填充内容。

image.png

二、给画布填充内容

 <script setup lang="ts">
    import { ref } from 'vue'
    const cancasRef = ref<HTMLCanvasElement>()
    const ctx = ref<CanvasRenderingContext2D | null>()
    
    onMounted(() => {
      if (cancasRef.value) {
         ctx.value = cancasRef.value.getContext('2d')
         
         /** 绘制图层 */
         ctx.value?.beginPath()
         
         /** 填充颜色 */
         ctx.value!.fillStyle = 'red'
         
         /** 设置画布大小 */
         ctx.value?.fillRect(0, 0, 200, 150)
      }
    }
 <script>
    
 <template>
  <div class="container">
    <div class="content">
       谢谢惠顾
    </div>

    <canvas ref="cancasRef" />

  </div>
</template>

<style scoped lang="less">
  .container {
    position: relative;
    width: max-content;
    
    // 把内容的图层定位,并且把层级调到底部
    .content {
      position: absolute;
      z-index: -1;
      width: 200px;
      height: 150px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      font-size: 24px;
      background-color: skyblue;
    }
  }
</style>

给画布填充颜色之后就开始遮盖住下面的内容了,那么咱们下一步就是开始刮画布的图层。

image.png

三、给画布图层添加刮刮乐的效果

  <script setup lang="ts">
    import { ref } from 'vue'
    const cancasRef = ref<HTMLCanvasElement>()
    const ctx = ref<CanvasRenderingContext2D | null>()
    
    /* 获取该元素到可视窗口的距离 */
    function getElementWindowRange(element: HTMLCanvasElement) {
      let left = 0, top = 0

      function get(obj: HTMLCanvasElement){
        left += obj.offsetLeft
        top += obj.offsetTop

        /* 不到最外层就一直调用,直到offsetParent为body*/
        if (obj!.offsetParent!.tagName != 'BODY') {
          get(obj?.offsetParent as HTMLCanvasElement)
        }
        
         return [left, top]
      }

       return get(element)
    }
    
    onMounted(() => {
      if (cancasRef.value) {
         ctx.value = cancasRef.value.getContext('2d')
         
         /** 用于计算鼠标移动的 */
         let offsetX = 0
         let offsetY = 0
         
         /** 绘制图层 */
         ctx.value?.beginPath()
         
         /** 填充颜色 */
         ctx.value!.fillStyle = 'red'
         
         /** 设置画布大小 */
         ctx.value?.fillRect(0, 0, 200, 150)
         
         cancasRef.value.addEventListener("touchstart", (event) => {
            /** 获取滑动位置 */
            const arr = getElementWindowRange(cancasRef.value!)
            offsetX = arr[0]
            offsetY = arr[1]
            /** 橡皮擦效果 */
            ctx.value!.globalCompositeOperation = 'destination-out'
            /* 画笔粗细*/
            ctx.value!.lineWidth = lineWidth
            ctx.value!.beginPath()
            /* 移动画笔原点*/
            ctx.value!.moveTo(event.touches[0].pageX - offsetX, event.touches[0].pageY - offsetY)
          })

      /** 监听鼠标移动事件 */
      cancasRef.value.addEventListener("touchmove", (event) => {
        /* 根据手指移动画线,使之变透明*/
        ctx.value!.lineTo(event.touches[0].pageX - offsetX, event.touches[0].pageY - offsetY)
        /* 填充*/
        ctx.value!.stroke()
      })
      }
    }
 <script>
    
 <template>
  <div class="container">
    <div class="content">
       谢谢惠顾
    </div>

    <canvas ref="cancasRef" />

  </div>
</template>

<style scoped lang="less">
  .container {
    position: relative;
    width: max-content;
    
    // 把内容的图层定位,并且把层级调到底部
    .content {
      position: absolute;
      z-index: -1;
      width: 200px;
      height: 150px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      font-size: 24px;
      background-color: skyblue;
    }
  }
</style>

通过上面代码我们就得到这个效果,这么一看好像有那味儿了。

2-ezgif.com-video-to-gif-converter.gif

四、刮到百分之多少之后清空画布

添加一个监听鼠标松开事件和清空的函数

  <script setup lang="ts">
    import { ref } from 'vue'
    const cancasRef = ref<HTMLCanvasElement>()
    const ctx = ref<CanvasRenderingContext2D | null>()
    
    /* 获取该元素到可视窗口的距离 */
    function getElementWindowRange(element: HTMLCanvasElement) {
      let left = 0, top = 0

      function get(obj: HTMLCanvasElement){
        left += obj.offsetLeft
        top += obj.offsetTop

        /* 不到最外层就一直调用,直到offsetParent为body*/
        if (obj!.offsetParent!.tagName != 'BODY') {
          get(obj?.offsetParent as HTMLCanvasElement)
        }
        
         return [left, top]
      }

       return get(element)
    }
    
    /** 清除涂层 */
    function clear(alpha: number, width: number, height: number) {
      return () => {
        ctx.value!.save()
        /** 使用谈出的效果 */
        ctx.value!.globalCompositeOperation = "source-in"
        ctx.value!.fillStyle = ctx.value!.fillStyle + (alpha -= 1).toString(16)
        ctx.value!.fillRect(0, 0, width, height)
        ctx.value!.restore()
        /** 到210已经看不到涂层了 */
        if (alpha > 210) requestAnimationFrame(clear(alpha, width, height))
      }
    }
    
    onMounted(() => {
      if (cancasRef.value) {
         ctx.value = cancasRef.value.getContext('2d')
         
         /** 用于计算鼠标移动的 */
         let offsetX = 0
         let offsetY = 0
         
         /** 绘制图层 */
         ctx.value?.beginPath()
         
         /** 填充颜色 */
         ctx.value!.fillStyle = 'red'
         
         /** 设置画布大小 */
         ctx.value?.fillRect(0, 0, 200, 150)
         
         cancasRef.value.addEventListener("touchstart", (event) => {
            /** 获取滑动位置 */
            const arr = getElementWindowRange(cancasRef.value!)
            offsetX = arr[0]
            offsetY = arr[1]
            /** 橡皮擦效果 */
            ctx.value!.globalCompositeOperation = 'destination-out'
            /* 画笔粗细*/
            ctx.value!.lineWidth = lineWidth
            ctx.value!.beginPath()
            /* 移动画笔原点*/
            ctx.value!.moveTo(event.touches[0].pageX - offsetX, event.touches[0].pageY - offsetY)
          })

      /** 监听鼠标移动事件 */
      cancasRef.value.addEventListener("touchmove", (event) => {
        /* 根据手指移动画线,使之变透明*/
        ctx.value!.lineTo(event.touches[0].pageX - offsetX, event.touches[0].pageY - offsetY)
           /* 填充*/
           ctx.value!.stroke()
        })
      }
      
      /** 监听鼠标松开事件 */
      cancasRef.value.addEventListener("touchend", () => {
        /** (getImageData)该方法会返回canvas像素点对象 */
        const pixelObj = ctx.value!.getImageData(0, 0, width, height)
        /** 拿到大小用于遍历 */
        const pixel = pixelObj.width * pixelObj.height
        /** 记录已经刮开的像素点 */
        let transparentPixels = 0

        for(let i = 0; i < pixel; i++) {
          /**
           * 1.data属性为一个数组,每4个元素对应一个像素点
           * 2.可以根据像素点的opcity值来判断这个像素点是不是透明,是不是等于0?
          */
          if (pixelObj.data[i * 4 + 3] === 0) {
            transparentPixels++
          }
        }
        
        /** 0.7表示挂到70%就清空画布 */
        if(transparentPixels >= pixel * 0.7) {
          requestAnimationFrame(clear(255, width, height))
        }
      }, false)
    }
 <script>
    
 <template>
  <div class="container">
    <div class="content">
       谢谢惠顾
    </div>

    <canvas ref="cancasRef" />

  </div>
</template>

<style scoped lang="less">
  .container {
    position: relative;
    width: max-content;
    
    // 把内容的图层定位,并且把层级调到底部
    .content {
      position: absolute;
      z-index: -1;
      width: 200px;
      height: 150px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      font-size: 24px;
      background-color: skyblue;
    }
  }
</style>

通过上面的代码就得到这个效果,到这里基本也是一个完整的Demo了。

3-ezgif.com-video-to-gif-converter.gif

五、对组件进行优化

优化内容

  1. 添加填充图片;
  2. 自定义画笔的小大;
  3. 自定义刮到n%就清空画布;
  4. 根据内容的大小给画布填充大小,而不是写死;
  5. 清空完成之后的回调函数;
  6. 是否可以刮;

添加一个TS文件:index.types.ts

export type Props = {
  /**
   * 填充类型
   * (1) 图片
   * (2) 背景颜色
  */
  fillType: 1 | 2;

  /** 如果上面(fillType)选择图片就传图片的url,否则传背景颜色 */
  fillContent: string;

  /** 画笔半径(默认20) */
  lineWidth?: number;

  /** 刮到百分比的时候清空画布(默认50) */
  clearZoneRadius?: number;

  /** 是否可以刮刮乐(默认是ture) */
  isCanDraw?: boolean;
}

export type Emits = {
  (e: 'clearFinish'): void;
}

index.vue

  <script setup lang="ts">
    import { ref, defineProps } from 'vue'
    import { type Props, Emits } from './index.types'
    
    const {
       fillType,
       fillContent,
       lineWidth = 20,
       clearZoneRadius = 50,
       isCanDraw = true
    } = defineProps<Props>()
    
    const emit = defineEmits<Emits>()
    const cancasRef = ref<HTMLCanvasElement>()
    const ctx = ref<CanvasRenderingContext2D | null>()
    const contentRef = ref<HTMLDivElement>()
    
    /* 获取该元素到可视窗口的距离 */
    function getElementWindowRange(element: HTMLCanvasElement) {
      let left = 0, top = 0

      function get(obj: HTMLCanvasElement){
        left += obj.offsetLeft
        top += obj.offsetTop

        /* 不到最外层就一直调用,直到offsetParent为body*/
        if (obj!.offsetParent!.tagName != 'BODY') {
          get(obj?.offsetParent as HTMLCanvasElement)
        }
        
         return [left, top]
      }

       return get(element)
    }
    
    /** 清除涂层 */
    function clear(alpha: number, width: number, height: number) {
      return () => {
        ctx.value!.save()
        /** 使用谈出的效果 */
        ctx.value!.globalCompositeOperation = "source-in"
        ctx.value!.fillStyle = ctx.value!.fillStyle + (alpha -= 1).toString(16)
        ctx.value!.fillRect(0, 0, width, height)
        ctx.value!.restore()
        /** 到210已经看不到涂层了 */
        if (alpha > 210) requestAnimationFrame(clear(alpha, width, height))
        
        /** 模仿清除完成之后的回调,有大佬知道这块怎么优化吗?知道的话帮忙指点一下,谢谢! */
        if (alpha === 211) {
            setTimeout(() => {
              emit('clearFinish')
            }, 200)
        }
      }
    }
    
    onMounted(() => {
      if (cancasRef.value) {
         ctx.value = cancasRef.value.getContext('2d')
         
         /** 用于计算鼠标移动的 */
         let offsetX = 0
         let offsetY = 0
         
         /** 获取底下图层的宽高 */
         const width = contentRef.value?.offsetWidth || 0
         const height = contentRef.value?.offsetHeight || 0
         cancasRef.value.width = width
         cancasRef.value.height = height
         
         /** 绘制图层 */
         ctx.value?.beginPath()
         
         /** 判断一下填充图片还是背景色 */
          if (fillType === 1) {
            const img = new Image()

            img.src = fillContent
            img.onload = function() {
              ctx.value!.drawImage(img, 0, 0, width, height)
            }
          } else {
            /** 填充颜色 */
            ctx.value!.fillStyle = fillContent
          }
         
         /** 设置画布大小 */
         ctx.value?.fillRect(0, 0, 200, 150)
         
         cancasRef.value.addEventListener("touchstart", (event) => {
            /** 获取滑动位置 */
            const arr = getElementWindowRange(cancasRef.value!)
            offsetX = arr[0]
            offsetY = arr[1]
            /** 橡皮擦效果 */
            ctx.value!.globalCompositeOperation = 'destination-out'
            /* 画笔粗细*/
            ctx.value!.lineWidth = lineWidth
            ctx.value!.beginPath()
            /* 移动画笔原点*/
            ctx.value!.moveTo(event.touches[0].pageX - offsetX, event.touches[0].pageY - offsetY)
          })

      /** 监听鼠标移动事件 */
      cancasRef.value.addEventListener("touchmove", (event) => {
        /* 根据手指移动画线,使之变透明*/
        ctx.value!.lineTo(event.touches[0].pageX - offsetX, event.touches[0].pageY - offsetY)
           /* 填充*/
           ctx.value!.stroke()
        })
      }
      
      /** 监听鼠标松开事件 */
      cancasRef.value.addEventListener("touchend", () => {
        /** (getImageData)该方法会返回canvas像素点对象 */
        const pixelObj = ctx.value!.getImageData(0, 0, width, height)
        /** 拿到大小用于遍历 */
        const pixel = pixelObj.width * pixelObj.height
        /** 记录已经刮开的像素点 */
        let transparentPixels = 0

        for(let i = 0; i < pixel; i++) {
          /**
           * 1.data属性为一个数组,每4个元素对应一个像素点
           * 2.可以根据像素点的opcity值来判断这个像素点是不是透明,是不是等于0?
          */
          if (pixelObj.data[i * 4 + 3] === 0) {
            transparentPixels++
          }
        }
        
        if(transparentPixels >= pixel * (clearZoneRadius / 100)) {
          requestAnimationFrame(clear(255, width, height))
        }
      }, false)
    }
 <script>
    
 <template>
  <div class="container">
    <div class="content" ref="contentRef">
       <slot />
    </div>

    <canvas ref="cancasRef" />
    
    <div class="mask" v-if="!isCanDraw"></div>
  </div>
</template>

<style scoped lang="less">
  .container {
    position: relative;
    width: max-content;
    
    // 把内容的图层定位,并且把层级调到底部
    .content {
      position: absolute;
      z-index: -1;
      width: 200px;
      height: 150px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      font-size: 24px;
      background-color: skyblue;
    }
    
    .mask {
      position: absolute;
      left: 0;
      top: 0;
      right: 0;
      bottom: 0;
    }
  }
</style>

使用该组件

<script setup lang="ts">
  import ScratchCard from '@/components/scratch-card/index.vue'

  const clearFinish = () => {
    console.log('....')
  }

</script>

<template>
  <div class="scratch-card">

    <ScratchCard
      @clear-finish="clearFinish"
      :clear-zone-radius="70"
      :fill-type="2"
      fill-content="green">
      <div class="test">
        谢谢惠顾
      </div>
    </ScratchCard>

  </div>
</template>

<style scoped lang="less">
  .scratch-card {
    .test {
      height: 150px;
      width: 200px;
      background-color: skyblue;
      display: flex;
      align-items: center;
      justify-content: center;
      color: red;
      font-size: 24px;
    }
  }
</style>

组件使用没什么问题,那么咱们试一试填充图片看看

4-ezgif.com-video-to-gif-converter.gif

六、填充图片

组件代码还是一样的,这里就只显示使用组件的代码了

    <ScratchCard
      @clear-finish="clearFinish"
      :clear-zone-radius="70"
      :fill-type="1"
      fill-content="https://img1.baidu.com/it/u=1105035751,396112046&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1740157200&t=b8a8c2d988f546ace7562f268c9b48c5">
      <div class="test">
        谢谢惠顾
      </div>
    </ScratchCard>

看看填充图片的效果如何,咱们看见刮是没什么问题,但是会出现一个报错,这个报错也会导致不能清空,那么咱们就解决一下。

5-ezgif.com-video-to-gif-converter.gif

刮刮乐组件代码

加了这行代码 img.crossOrigin = "Anonymous"

  <script setup lang="ts">
    import { ref, defineProps } from 'vue'
    import { type Props, Emits } from './index.types'
    
    const {
       fillType,
       fillContent,
       lineWidth = 20,
       clearZoneRadius = 50,
       isCanDraw = true
    } = defineProps<Props>()
    
    const emit = defineEmits<Emits>()
    const cancasRef = ref<HTMLCanvasElement>()
    const ctx = ref<CanvasRenderingContext2D | null>()
    const contentRef = ref<HTMLDivElement>()
    
    /* 获取该元素到可视窗口的距离 */
    function getElementWindowRange(element: HTMLCanvasElement) {
      let left = 0, top = 0

      function get(obj: HTMLCanvasElement){
        left += obj.offsetLeft
        top += obj.offsetTop

        /* 不到最外层就一直调用,直到offsetParent为body*/
        if (obj!.offsetParent!.tagName != 'BODY') {
          get(obj?.offsetParent as HTMLCanvasElement)
        }
        
         return [left, top]
      }

       return get(element)
    }
    
    /** 清除涂层 */
    function clear(alpha: number, width: number, height: number) {
      return () => {
        ctx.value!.save()
        /** 使用谈出的效果 */
        ctx.value!.globalCompositeOperation = "source-in"
        ctx.value!.fillStyle = ctx.value!.fillStyle + (alpha -= 1).toString(16)
        ctx.value!.fillRect(0, 0, width, height)
        ctx.value!.restore()
        /** 到210已经看不到涂层了 */
        if (alpha > 210) requestAnimationFrame(clear(alpha, width, height))
        
        /** 模仿清除完成之后的回调,有大佬知道这块怎么优化吗?知道的话帮忙指点一下,谢谢! */
        if (alpha === 211) {
            setTimeout(() => {
              emit('clearFinish')
            }, 200)
        }
      }
    }
    
    onMounted(() => {
      if (cancasRef.value) {
         ctx.value = cancasRef.value.getContext('2d')
         
         /** 用于计算鼠标移动的 */
         let offsetX = 0
         let offsetY = 0
         
         /** 获取底下图层的宽高 */
         const width = contentRef.value?.offsetWidth || 0
         const height = contentRef.value?.offsetHeight || 0
         cancasRef.value.width = width
         cancasRef.value.height = height
         
         /** 绘制图层 */
         ctx.value?.beginPath()
         
         /** 判断一下填充图片还是背景色 */
          if (fillType === 1) {
            const img = new Image()
            
            /** 防止画布已被跨源数据污染。 */
            img.crossOrigin = "Anonymous"
            img.src = fillContent
            img.onload = function() {
              ctx.value!.drawImage(img, 0, 0, width, height)
            }
          } else {
            /** 填充颜色 */
            ctx.value!.fillStyle = fillContent
          }
         
         /** 设置画布大小 */
         ctx.value?.fillRect(0, 0, 200, 150)
         
         cancasRef.value.addEventListener("touchstart", (event) => {
            /** 获取滑动位置 */
            const arr = getElementWindowRange(cancasRef.value!)
            offsetX = arr[0]
            offsetY = arr[1]
            /** 橡皮擦效果 */
            ctx.value!.globalCompositeOperation = 'destination-out'
            /* 画笔粗细*/
            ctx.value!.lineWidth = lineWidth
            ctx.value!.beginPath()
            /* 移动画笔原点*/
            ctx.value!.moveTo(event.touches[0].pageX - offsetX, event.touches[0].pageY - offsetY)
          })

      /** 监听鼠标移动事件 */
      cancasRef.value.addEventListener("touchmove", (event) => {
        /* 根据手指移动画线,使之变透明*/
        ctx.value!.lineTo(event.touches[0].pageX - offsetX, event.touches[0].pageY - offsetY)
           /* 填充*/
           ctx.value!.stroke()
        })
      }
      
      /** 监听鼠标松开事件 */
      cancasRef.value.addEventListener("touchend", () => {
        /** (getImageData)该方法会返回canvas像素点对象 */
        const pixelObj = ctx.value!.getImageData(0, 0, width, height)
        /** 拿到大小用于遍历 */
        const pixel = pixelObj.width * pixelObj.height
        /** 记录已经刮开的像素点 */
        let transparentPixels = 0

        for(let i = 0; i < pixel; i++) {
          /**
           * 1.data属性为一个数组,每4个元素对应一个像素点
           * 2.可以根据像素点的opcity值来判断这个像素点是不是透明,是不是等于0?
          */
          if (pixelObj.data[i * 4 + 3] === 0) {
            transparentPixels++
          }
        }
        
        if(transparentPixels >= pixel * (clearZoneRadius / 100)) {
          requestAnimationFrame(clear(255, width, height))
        }
      }, false)
    }
 <script>
    
 <template>
  <div class="container">
    <div class="content" ref="contentRef">
       <slot />
    </div>

    <canvas ref="cancasRef" />
    
    <div class="mask" v-if="!isCanDraw"></div>
  </div>
</template>

<style scoped lang="less">
  .container {
    position: relative;
    width: max-content;
    
    // 把内容的图层定位,并且把层级调到底部
    .content {
      position: absolute;
      z-index: -1;
      width: 200px;
      height: 150px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      font-size: 24px;
      background-color: skyblue;
    }
    
    .mask {
      position: absolute;
      left: 0;
      top: 0;
      right: 0;
      bottom: 0;
    }
  }
</style>

加了上面那行代码之后就不会报错了,这里就不演示了,需要的小伙伴就自行看一下就行。

七、解决警告问题

咱们会发现这里会有三个警告

image.png

警告1和2就再监听事件哪一行代码就行,在监听事件的第三个参数

{ passive: true }

警告3也是一行代码的事

ctx.value = cancasRef.value.getContext('2d', { willReadFrequently: true })

刮刮乐组件的代码

<script setup lang="ts">
  import { ref, defineProps } from 'vue'
  import { type Props, Emits } from './index.types'

  const {
    fillType,
    fillContent,
    lineWidth = 20,
    clearZoneRadius = 50,
    isCanDraw = true
  } = defineProps<Props>()

  const emit = defineEmits<Emits>()
  const cancasRef = ref<HTMLCanvasElement>()
  const ctx = ref<CanvasRenderingContext2D | null>()
  const contentRef = ref<HTMLDivElement>()

  /* 获取该元素到可视窗口的距离 */
  function getElementWindowRange(element: HTMLCanvasElement) {
    let left = 0, top = 0

    function get(obj: HTMLCanvasElement){
      left += obj.offsetLeft
      top += obj.offsetTop

      /* 不到最外层就一直调用,直到offsetParent为body*/
      if (obj!.offsetParent!.tagName != 'BODY') {
        get(obj?.offsetParent as HTMLCanvasElement)
      }
      return [left, top]
    }

    return get(element)
  }

  /** 清除涂层 */
  function clear(alpha: number, width: number, height: number) {
    return () => {
      ctx.value!.save()
      /** 使用谈出的效果 */
      ctx.value!.globalCompositeOperation = "source-in"
      ctx.value!.fillStyle = ctx.value!.fillStyle + (alpha -= 1).toString(16)
      ctx.value!.fillRect(0, 0, width, height)
      ctx.value!.restore()
      /** 到210已经看不到涂层了 */
      if (alpha > 210) requestAnimationFrame(clear(alpha, width, height))
      if (alpha === 211) {
        setTimeout(() => {
          emit('clearFinish')
        }, 200)
      }
    }
  }

  onMounted(() => {

    if (cancasRef.value) {
      ctx.value = cancasRef.value.getContext('2d', { willReadFrequently: true })

      let offsetX = 0
      let offsetY = 0

      /** 获取底下图层的宽高 */
      const width = contentRef.value?.offsetWidth || 0
      const height = contentRef.value?.offsetHeight || 0
      cancasRef.value.width = width
      cancasRef.value.height = height

      /** 绘制图层 */
      ctx.value?.beginPath()

      /** 判断一下填充图片还是背景色 */
      if (fillType === 1) {
        const img = new Image()

        /** 防止画布已被跨源数据污染。 */
        img.crossOrigin = "Anonymous"
        img.src = fillContent
        img.onload = function() {
          ctx.value!.drawImage(img, 0, 0, width, height)
        }
      } else {
        /** 填充颜色 */
        ctx.value!.fillStyle = fillContent
      }

      /** 设置画布大小 */
      ctx.value?.fillRect(0, 0, width, height)

      /** 监听鼠标按下事件 */
      cancasRef.value.addEventListener("touchstart", (event) => {
        /** 获取滑动位置 */
        const arr = getElementWindowRange(cancasRef.value!)
        offsetX = arr[0]
        offsetY = arr[1]

        ctx.value!.globalCompositeOperation = 'destination-out'
        /* 画笔粗细*/
        ctx.value!.lineWidth = lineWidth
        ctx.value!.beginPath()
        /* 移动画笔原点*/
        ctx.value!.moveTo(event.touches[0].pageX - offsetX, event.touches[0].pageY - offsetY)
      }, {
        passive: true
      })

      /** 监听鼠标移动事件 */
      cancasRef.value.addEventListener("touchmove", (event) => {
        /* 根据手指移动画线,使之变透明*/
        ctx.value!.lineTo(event.touches[0].pageX - offsetX, event.touches[0].pageY - offsetY)
        /* 填充*/
        ctx.value!.stroke()
      }, {
        passive: true
      })

      /** 监听鼠标松开事件 */
      cancasRef.value.addEventListener("touchend", () => {
        /** (getImageData)该方法会返回canvas像素点对象 */
        const pixelObj = ctx.value!.getImageData(0, 0, width, height)
        /** 拿到大小用于遍历 */
        const pixel = pixelObj.width * pixelObj.height
        /** 记录已经刮开的像素点 */
        let transparentPixels = 0

        for(let i = 0; i < pixel; i++) {
          /**
           * 1.data属性为一个数组,每4个元素对应一个像素点
           * 2.可以根据像素点的opcity值来判断这个像素点是不是透明,是不是等于0?
          */
          if (pixelObj.data[i * 4 + 3] === 0) {
            transparentPixels++
          }
        }

        if(transparentPixels >= pixel * (clearZoneRadius / 100)) {
          requestAnimationFrame(clear(255, width, height))
        }
      }, false)

    }

  })

</script>

<template>
  <div class="container">
    <div class="content" ref="contentRef">
      <slot />
    </div>

    <canvas ref="cancasRef" />

    <div class="mask" v-if="!isCanDraw"></div>
  </div>
</template>

<style scoped lang="less">
  .container {
    position: relative;
    width: max-content;

    .content {
      position: absolute;
      z-index: -1;
    }

    .mask {
      position: absolute;
      left: 0;
      top: 0;
      right: 0;
      bottom: 0;
    }
  }
</style>

八、借鉴

传送门

结语

到这里就告一段落了,后续会补充一下使用Vue3的TransitionGroup效果,如果有问题或者是需要改正的请帮忙指出来,谢谢!