Vue+Canvas开发技巧分享:打造一个炫酷的图片刮刮乐效果!

Toby2023-04-02Vue分享 Canvas 图片
用摸鱼的时间做了一个图片刮刮乐

案例分析

我们要实现一个简单的图片刮刮乐。
具体来说,我们要先创建一个画布 (canvas), 然后在其中绘制一张图片作为底图,并覆盖一张图片作为涂层。
当鼠标在涂层上移动时,会绘制圆形裁剪区域,并将该区域清空,从而展示出底部的图片。当鼠标抬起或移出画布时,程序会计算已经刮出的面积,并根据阈值决定是否重新绘制底部全图。

具体实现

1、创建画布

首先我们要定义一个 div ,里面有一个 img 和一个 canvas 元素。div 元素用于作为容器,img 元素为底部的图片。
canvas 元素则用于绘制涂层和处理用户的交互行为。

<template>
  <div class="container" id="container">
    <!-- 顶部图片 -->
    <canvas 
      id="canvas" 
      @mousedown="handleMouseDown" 
      @mousemove="handleMouseMove" 
      @mouseup="handleMouseUp" 
      @mouseout="handleMouseOut"
    />
    <!-- 底部图片 -->
    <img :src="baseImg">
  </div>
</template>

<style scoped>
.container {
  position: relative;
  margin: 0 auto;
}

canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
}
</style>

2、创建钩子和引入图片

接下来导入 vue 中的 onMounted、reactive 和 defineProps 方法,用于组件的生命周期钩子和响应式数据管理。
通过 defineProps 定义了两个组件属性:topImg 和 baseImg,分别表示底部图片和涂层图片的地址,并设置了默认值。

<script setup>
import { onMounted, reactive,defineProps, ref } from 'vue';

const props = defineProps({
  topImg: { // 顶部图片
    type:String,
    default:'01.jpg'
  },
  baseImg: { // 底部图片
    type:String,
    default:'02.jpg'
  },
})
</script>

使用 reactive 方法创建了一个响应式数据对象 data,其中包含了一些状态变量和配置参数,用于记录用户操作和控制程序的行为。

const data = reactive({
  isMouseDown: false, // 当前鼠标是否按下
  lastLoc: { x: 0, y: 0 }, // 上一次鼠标位置
  curLoc: { x: 0, y: 0 }, // 当前鼠标位置
  canvasWidth: 0, // canvas 的宽
  canvasHeight: 0, // canvas 的高
  threshold: 0.65 // 添加一个阈值属性
});

3、绘制 canvas 底图

在组件挂载后,会获取到 canvas 元素并设置其宽度和高度,然后获取到画布上下文(Context),并调用 drawCover 方法绘制图片。

let ctx;

onMounted(() => {
  const divElement = document.getElementById('container');
  // 获取div的宽度
  let width = divElement.getBoundingClientRect().width

  // 计算 baseImg 图片在容器中的宽高比例
  const img = new Image();
  img.crossOrigin = 'anonymous'; 
  img.src = props.baseImg;
  img.onload = function () {
    data.canvasWidth = width; // 容器中的宽度
    data.canvasHeight = width/(img.width/img.height); // 容器中的高度
    initialize()
  };
});

// 设置 canvas 的宽高,并将其传递给 drawCover 函数,然后让其绘制图片。
const initialize =()=> {
  const canvas = document.getElementById('canvas')
  canvas.width = data.canvasWidth;
  canvas.height = data.canvasHeight;
  ctx = canvas.getContext('2d');
  drawCover(props.topImg);
}

// 绘制图片
const drawCover = (imgSrc)=> {
  const img = new Image();
  img.crossOrigin = 'anonymous'; 
  img.src = imgSrc;
  img.onload = function () {
    ctx.drawImage(img, 0, 0, data.canvasWidth, data.canvasHeight);
  };
}

4、监听鼠标事件

添加鼠标事件 mousedown、mousemove、mouseup、mouseout

const handleMouseDown = (e)=> {
  e.preventDefault();
  data.isMouseDown = true;
  data.lastLoc = windowToCanvas(e.clientX, e.clientY);
}

const handleMouseMove = (e)=> {
  e.preventDefault();
  if (data.isMouseDown) {
    data.curLoc = windowToCanvas(e.clientX, e.clientY);
    
    // 绘制圆形裁剪区域
    ctx.save();
    ctx.beginPath();
    ctx.arc(data.curLoc.x, data.curLoc.y, 20, 0, Math.PI * 2, false);
    ctx.clip();
    
    // 清空画布
    ctx.clearRect(0, 0, data.canvasWidth, data.canvasHeight);
    
    ctx.restore();
  }
}

const handleMouseUp = (e)=> {
  e.preventDefault();
  data.isMouseDown = false;
  
  // 计算已经刮出的面积
  const imageData = ctx.getImageData(0, 0, data.canvasWidth, data.canvasHeight);
  const pixels = imageData.data;
  let count = 0;
  for (let i = 0; i < pixels.length; i += 4) {
    if (pixels[i + 3] === 0) {
      count++;
    }
  }
  const area = count / (data.canvasWidth * data.canvasHeight);
  
  // 如果被刮出的面积大于阈值,重新绘制底部全图
  if (area > data.threshold) {
    ctx.clearRect(0, 0, data.canvasWidth, data.canvasHeight);
  }
}

const handleMouseOut = (e)=> {
  e.preventDefault();
  if (data.isMouseDown) {
    data.isMouseDown = false;
  }
}

const windowToCanvas = (x, y)=> {
  const canvas = document.getElementById('canvas')
  const bbox = canvas.getBoundingClientRect();
  return {
    x: Math.round(x - bbox.left),
    y: Math.round(y - bbox.top)
  };
}

5、查看效果

更新时间 2024/5/20 14:47:01