Skip to content
微信小程序实现刮一刮效果

wxml代码

html
<view class="container">
  <view class="scratch-wrapper">
    <view class="zj" wx:if="{{prizeShow}}">
    恭喜你中奖了
    </view>
    <canvas type="2d" id="scratchCanvas" class="scratch-canvas" disable-scroll="{{true}}" bindtouchstart="onTouchStart" bindtouchmove="onTouchMove" bindtouchend="onTouchEnd" />
  </view>
  <button bindtap="reset">重置</button>
</view>

wxss代码

css
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 100rpx;
}

.scratch-wrapper {
  position: relative;
  width: 600rpx;
  height: 400rpx;
}

.prize-text {
  position: absolute;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 40rpx;
  color: #ff4444;
  font-weight: bold;
  background: #fff3cd;
}

.scratch-canvas {
  position: absolute;
  width: 100%;
  height: 100%;
  z-index: 10;
}

.zj{
  width: 100%;
  height: 100%;
  background-color: red;
  text-align: center;
  line-height: 400rpx;
  color: #fff;
  position: absolute;
  top: 0;
  left: 0;
}

js代码

js
Page({
  data: {
    // ===== 响应式数据 =====
    scratchImageSrc: 'https://dop.x.digitalyili.com/shortVideo/img/bg/guajiang.png', // 刮层图地址,不传地址会有默认显示
    ready: false, // canvas 是否可用
    threshold: 0.5, // 40% 区域就全部展示
    checked: false, // 避免重复统计
    width: 0, // 画布宽(px)
    height: 0, // 画布高(px)
    brushSize: 10, // ← 笔粗细(像素)
    prizeShow: false, // 控制奖品显示,防止一直点重置导致底下奖品闪烁
  },

  // ===== 非响应式原生引用 =====
  canvas: null,
  ctx: null,

  onReady() {
    this.initCanvas();
  },

  initCanvas() {
    wx.createSelectorQuery()
      .select('#scratchCanvas')
      .fields({
        node: true,
        size: true
      })
      .exec((res) => {
        if (!res[0]) return;
        const canvas = res[0].node;
        const ctx = canvas.getContext('2d');
        const dpr = wx.getSystemInfoSync().pixelRatio;

        const width = res[0].width;
        const height = res[0].height;
        canvas.width = width * dpr;
        canvas.height = height * dpr;
        ctx.scale(dpr, dpr);

        /* 刮层逻辑:有图用图,无图自画 */
        const src = this.data.scratchImageSrc || '';
        if (src) {
          const img = canvas.createImage();
          img.src = src;
          img.onload = () => {
            ctx.drawImage(img, 0, 0, width, height);
            this.setData({
              ready: true
            });
          };
        } else {
          ctx.fillStyle = '#888';
          ctx.fillRect(0, 0, width, height);
          ctx.fillStyle = '#fff';
          ctx.font = 'bold 32rpx sans-serif';
          ctx.textAlign = 'center';
          ctx.fillText('刮开有惊喜', width / 2, height / 2 + 10);
          this.setData({
            ready: true
          });
        }

        // 保存原生引用
        this.canvas = canvas;
        this.ctx = ctx;
        this.setData({
          width,
          height,
          checked: false,
        });
      });
  },

  onTouchStart(e) {
    if (!this.data.ready) return;
    // 显示底部奖品
    this.setData({
      prizeShow: true
    })
    const p = this.getPos(e);
    this.scratch(p.x, p.y);
  },

  onTouchMove(e) {
    if (!this.data.ready) return;
    const p = this.getPos(e);
    this.scratch(p.x, p.y);
  },

  onTouchEnd() {
    if (!this.data.ready || this.data.checked) return;
    setTimeout(() => this.checkArea(), 150);
  },

  getPos(e) {
    return {
      x: e.touches[0].x,
      y: e.touches[0].y
    };
  },

  scratch(x, y) {
    this.ctx.globalCompositeOperation = 'destination-out';
    this.ctx.beginPath();
    this.ctx.arc(x, y, this.data.brushSize, 0, Math.PI * 2); // 用变量
    this.ctx.fill();
  },

  checkArea() {
    if (this.data.checked) return;
    this.setData({
      checked: true
    });

    const ctx = this.ctx;
    const width = ~~this.data.width; // 强制整数
    const height = ~~this.data.height;
    const imageData = ctx.getImageData(0, 0, width, height);

    const pixels = imageData.data;
    let transparent = 0;
    for (let i = 3; i < pixels.length; i += 4) {
      if (pixels[i] === 0) transparent++;
    }
    const ratio = transparent / (width * height);
    if (ratio >= this.data.threshold) {
      ctx.clearRect(0, 0, width, height);
    } else {
      this.setData({
        checked: false
      });
    }
  },

  reset() {
    this.setData({
      ready: false,
      prizeShow: false
    });
    this.initCanvas();
  }
});