如何在微信小程序中实现刮刮卡效果

390 阅读5分钟

本篇文章介绍如何使用 Canvas 技术,在微信小程序中实现流畅的刮刮卡效果,并且你将学到如何优化性能,提升用户体验。

技术需求:为什么选择Canvas实现刮刮卡效果?

Canvas 是微信小程序中用于绘制图形、图像、动画等的强大工具,尤其适合用来创建动态的交互效果。通过 Canvas,我们可以创建出涂层效果,并响应用户的触摸事件来模拟刮开涂层的过程,从而展示底层的答案或隐藏信息。

本功能需求:

  1. 涂层展示:在 Canvas 上绘制一个涂层,用户可以通过触摸或滑动去刮开涂层。
  2. 答案显示:当涂层被刮开时,底部的内容将逐渐显现,达到一定清除比例(如 70%)时,完整答案会展示出来。
  3. 流畅的交互体验:需要确保刮刮卡效果在不同设备上都能平滑运行,避免卡顿。

实现步骤:从设计到交互

1. 初始化Canvas

首先,我们需要在小程序页面中添加一个 Canvas 元素。这个 Canvas 将作为刮刮卡的画布,用户的触摸事件将在这个区域内进行。

.wxml
<view class="area P1_4">
    <view class="bd">
      <view class="d_ct" wx:for="{{questions}}" wx:key="index">
        <view class="cap3">{{item.question}}
          <view wx:if="{{index==1}}" class="d s_tip">(至少列举三项)</view>
        </view>
        <view class="txt">
          <!-- 添加canvas用于刮刮卡答案区域,显示答案 -->
          <canvas id="scratchCanvas-{{index}}" type="2d" class="scratch-canvas" catchtouchstart="onTouchStart" catchtouchmove="onTouchMove" catchtouchend="onTouchEnd" data-index="{{index}}"></canvas>

          <!-- 显示逐步揭开的答案 -->
          <view wx:if="{{item.answerVisible}}" class="answer">
            <text>{{item.answerVisible}}</text>
          </view>
        </view>
      </view>
    </view>
  </view>

在这段代码中,使用了 catchtouchstartcatchtouchmovecatchtouchend 来绑定触摸事件处理方法。接下来我们将详细解释为什么选择 catch 事件绑定方式,而不是使用 bind

catch 事件绑定 vs bind 事件绑定

在微信小程序中,catchbind 都是用于事件绑定的关键字,它们的主要区别在于事件的传播机制。

  • bind 绑定事件:事件会触发冒泡,即触发元素的事件会向上传播,可能会影响到父级元素的触摸事件响应。
  • catch 绑定事件:事件不会冒泡,而是阻止事件的传播,确保事件只在当前元素上处理,而不会影响到父级元素。

在实现刮刮卡效果时,我们希望触摸事件只在当前的 Canvas 元素上处理,并避免事件冒泡到其他父级元素,以免造成其他页面元素不必要的响应或样式问题。因此,使用 catch 绑定事件是最佳选择。

2. 绘制涂层

我们将在 Canvas 上绘制一个矩形涂层,模拟刮刮卡的覆盖层。用户通过滑动触摸屏,涂层逐渐被清除,露出下方的内容。

.js
Page({
  data: {
    questions: [{
        question: '1、元宵节是中国的哪个传统节日?',
        answer: '答:元宵节是中国的农历正月十五的节日,又称上元节或灯节。',
        clearedArea: 0,
        isDrawing: false,
        answerVisible: ''
      },
      {
        question: '2、元宵节的主要习俗有哪些?',
        answer: '答:元宵节的主要习俗包括赏花灯、吃元宵(或汤圆)、猜灯谜等。',
        clearedArea: 0,
        isDrawing: false,
        answerVisible: ''
      },
      {
        question: '3、“元宵”和“汤圆”有什么区别?',
        answer: '答:元宵是“滚”出来的,以馅为基础,在糯米粉中滚成球形;而汤圆则是“包”出来的,类似于包饺子,将糯米粉团皮包上馅料后捏合成型。',
        clearedArea: 0,
        isDrawing: false,
        answerVisible: '' // 用于逐步显示答案
      },
    ],
    imgSrc: 'https://open.weixin.qq.com/zh_CN/htmledition/res/assets/res-design-download/icon64_wx_logo.png'
  },
  onReady() {
    this.data.questions.forEach((item, index) => {
      this.initCanvas(index); // 初始化每个 canvas
    });
  },
  initCanvas(index) {
    const query = wx.createSelectorQuery().in(this);
    query.select('#scratchCanvas-' + index)
      .fields({
        node: true,
        size: true
      })
      .exec((res) => {
        const canvas = res[0].node
        const ctx = canvas.getContext('2d')
        const dpr = wx.getSystemInfoSync().pixelRatio

        // 设置canvas的宽高
        canvas.width = res[0].width * dpr
        canvas.height = res[0].height * dpr
        ctx.scale(dpr, dpr)

        // 填充背景色(遮罩)
        ctx.fillStyle = "#f90";
        ctx.fillRect(0, 0, canvas.width, canvas.height)

        // 在图片后面绘制文字
        ctx.font = '20rpx Arial'; // 设置字体大小和字体
        ctx.fillStyle = 'black'; // 设置文字颜色
        ctx.textAlign = 'center'; // 设置文字居中
        ctx.textBaseline = 'middle'; // 设置文字基线为中间

        // 计算文字的位置
        const textX = res[0].width / 2;
        const textY = 10; // 文字位置在图片上方

        // 绘制文字
        ctx.fillText('呱呱查看答案', textX, textY);

        // 图片对象
        const image = canvas.createImage()
        // 设置图片src
        image.src = this.data.imgSrc
        // 图片加载完成回调
        image.onload = () => {
          // 获取图片的原始宽高
          const imgWidth = 64;
          const imgHeight = 64;
          const x = (res[0].width - imgWidth) / 2;
          const Y = (res[0].height - imgHeight) / 2;

          // 将图片绘制到 canvas 上
          ctx.drawImage(image, x, Y)
        }

        // 初始化状态
        this.setData({
          [`questions[${index}].clearedArea`]: 0, // 清除的面积
          [`questions[${index}].isDrawing`]: false, // 是否在绘制
          [`questions[${index}].answerVisible`]: '' // 初始化答案可见部分
        });

      })
  },
  onTouchStart(e) {
    const index = e.currentTarget.dataset.index;
    this.setData({
      [`questions[${index}].isDrawing`]: true,
    });

    this.clearCanvas(e, index);
  },
  onTouchMove(e) {
    const index = e.currentTarget.dataset.index;
    if (this.data.questions[index].isDrawing) {
      this.clearCanvas(e, index);
    }
  },
  onTouchEnd(e) {
    const index = e.currentTarget.dataset.index;
    this.setData({
      [`questions[${index}].isDrawing`]: false,
    });
  },
  // 清除遮罩并计算清除区域
  clearCanvas(e, index) {
    const query = wx.createSelectorQuery().in(this);
    query.select(`#scratchCanvas-${index}`).fields({
      node: true,
      size: true
    }).exec((res) => {
      const canvas = res[0].node;
      const ctx = canvas.getContext('2d');
      // 获取触摸点的坐标
      const touch = e.touches[0];
      const x = touch.x;
      const y = touch.y;
      const radius = 30;

      // 清除圆形区域
      ctx.globalCompositeOperation = 'destination-out'; // 设置清除模式为擦除
      ctx.beginPath();
      ctx.arc(x, y, radius, 0, 2 * Math.PI); // 画圆
      ctx.fill();
      // 更新清除的区域
      this.updateClearedArea(index, x, y, canvas.width, canvas.height);
    });
  },
  // 更新清除的区域,并计算清除比例
  updateClearedArea(index, x, y, canvasWidth, canvasHeight) {
    const clearedArea = this.data.questions[index].clearedArea || 0; // 获取当前清除区域的面积
    const radius = 15 * wx.getSystemInfoSync().pixelRatio; // 清除区域的半径
    const clearSize = Math.PI * Math.pow(radius, 2); // 计算每次清除的圆形区域面积

    // 计算新的清除面积
    const newClearedArea = clearedArea + clearSize;

    // 计算清除比例
    const totalArea = canvasWidth * canvasHeight;
    const clearedPercentage = (newClearedArea / totalArea) * 100;

    // 更新数据,保存新的清除区域面积
    this.setData({
      [`questions[${index}].clearedArea`]: newClearedArea,
    });
    // 逐步显示答案
    this.updateAnswerVisible(index, clearedPercentage);
    // 如果清除的区域超过70%,显示答案
    if (clearedPercentage >= 70) {
      this.showFullAnswer(index);
    }
  },
  // 逐步显示答案
  updateAnswerVisible(index, clearedPercentage) {
    const question = this.data.questions[index];
    const answer = question.answer;

    // 根据清除比例更新显示的答案
    const visibleLength = Math.floor((clearedPercentage / 100) * answer.length);
    this.setData({
      [`questions[${index}].answerVisible`]: answer.substring(0, visibleLength)
    });
  },
  // 显示完整答案
  showFullAnswer(index) {
    this.setData({
      [`questions[${index}].answerVisible`]: this.data.questions[index].answer // 完整显示答案
    });
  },
})

3. 响应用户触摸事件

通过监听用户的触摸事件,我们能够获取滑动的轨迹,实时更新涂层的透明度,模拟刮刮卡效果。

  • touchstart:开始触摸,启用涂层刮开模式。
  • touchmove:触摸滑动,清除相应位置的涂层。
  • touchend:触摸结束,停止清除涂层。

4. 展示效果:刮开涂层,揭示答案

通过上述代码,用户将能够在小程序中实现刮刮卡效果,并通过滑动手指刮开涂层,看到隐藏的答案。

11.gif

微信小程序代码片段链接

github代码

关键词

  • 微信小程序
  • Canvas
  • 刮刮卡
  • 互动体验
  • 动态涂层