taro3-vue3-实现小程序刮刮卡效果

用taro3+vue3实现小程序刮刮卡活动效果

基础原理

利用canvas,先画封面图,然后刮的时候,手指刮到哪就去掉这部分封面。露出下面的底图。

底图使用div 可以放背景图也可以放文字, 刮的区域用canvas

<div class="com-scratchcard">
   <div class="scratchcard-content" :style="state.backImg" v-html="content"
   ></div>
   <canvas
     type="2d"
     class="scratchcard"
     id="scratchcard-canvas"
     @touchStart="touchStart"
     @touchmove="touchMove"
     @touchEnd="touchEnd"
   ></canvas>
 </div>

流程

  • 初始化页面 接口拿到封面信息
  • 使用canvas画封面信息
  • 用户开始刮, 然后刮到指定比例的时候去调接口拿到抽奖结果(并且扣除用户的抽奖机会)并同时lock锁定,避免重复触发抽奖接口。
  • 接口回来后,把结果底图(也就是backImg或content文字)填充好,并在这个时候弹窗提示用户抽奖结果
    在这里插入图片描述
  • 计算比例 是利用cavansContext.getImageData 获取图片像素数据。然后根据刮到的地方去清除计算刮开比例。
  • 中奖后重置canvas为原来的封面,并释放lock锁定标记

遇到的问题

刮开的时候,canvas默认的清除是 矩形的,如果页面这样清除区域的话,会有锯齿。
默认: clearRect(x, y , width, height) 只能清除矩形
在这里插入图片描述

解决方案

就是分多步去清除,虽然会清除很多次重复区域,但能利用1px的步进清理出没有锯齿的近似圆形的区域。

/**
 * (x,y)为要清除的圆的圆心,r为半径,cxt为context
 */
function clearArcFun(x, y, r, cxt) {
  var stepClear = 1; // 每次清除的步进器
  clearArc(x, y, r);
  function clearArc(x, y, radius) {
    var calcWidth = radius - stepClear;
    var calcHeight = Math.sqrt(radius * radius - calcWidth * calcWidth);
    var posX = x - calcWidth;
    var posY = y - calcHeight;

    var widthX = 2 * calcWidth;
    var heightY = 2 * calcHeight;

    if (stepClear <= radius) {
      cxt.clearRect(posX, posY, widthX, heightY);
      stepClear += 1;
      clearArc(x, y, radius);
    }
  }
}

在这里插入图片描述
这样看起来就顺畅多了。

其他注意点

  • 调用接口的时机(如果接口太慢,可能会导致手刮完了结果还没弹)。
  • 如果接口需要用户授权手机号才能调,那需要在调接口前拦截或者在整个组件外层遮罩授权button
  • 如果当前业务是做活动(比如10月1号到10月7号做活动),那就需要考虑做时间校验和用户抽奖机会的校验拦截。

完整代码

<template>
  <div class="com-scratchcard">
    <div
      class="scratchcard-content"
      :style="state.backImg"
      v-html="content"
    ></div>
    <canvas
      type="2d"
      class="scratchcard"
      id="scratchcard-canvas"
      @touchStart="touchStart"
      @touchmove="touchMove"
      @touchEnd="touchEnd"
    >
    </canvas>
  </div>
</template>
<script lang="ts" setup>
import Taro from '@tarojs/taro';
import { isEmpty, throttle } from '@/utils';
import {
  withDefaults,
  defineProps,
  onMounted,
  reactive,
  defineEmits,
  ref,
} from 'vue';

/**
 * 组件的设置信息
 */
interface IProp {
  content: string;
  height: number;
  width: number;
  coverColor: string;
  coverImg: string;
  fontSize: string | number;
  backgroundColor: string;
  ratio: number;
}

const props = withDefaults(defineProps<IProp>(), {
  content: '',
  height: 164, // 高度
  width: 297, // 宽度(375尺寸下)
  coverColor: '#C5C5C5',
  coverImg: '',
  fontSize: 20,
  backgroundColor: '#fff',
  ratio: 0.5, // 刮开的就抽奖的比例
  validator: () => true,
});

const state = reactive<{
  luckcard: any;
  hasPhone: boolean;
  isValidator: boolean;
  backImg: {};
  prizeResult: any;
}>({
  luckcard: null,
  hasPhone: false,
  isValidator: false,
  backImg: {},
  prizeResult: {},
});

let requestStatus: '1' | '2' | '3' = '1'; // 1 还没发接口 2已经发接口了 还没返回,3: 发送接口并返回
let canShowPrize = false;

let drawCanvas: any = null;
const pointInfo: any = { startX: -10, startY: -10 };
// 上锁
const lock = ref<boolean>(false);

let drawCtx: any = null;

const emits = defineEmits(['start', 'end', 'catchMove']);
/**
 * (x,y)为要清除的圆的圆心,r为半径,cxt为context
 */
function clearArcFun(x, y, r, cxt) {
  var stepClear = 1; // 别忘记这一步
  clearArc(x, y, r);
  function clearArc(x, y, radius) {
    var calcWidth = radius - stepClear;
    var calcHeight = Math.sqrt(radius * radius - calcWidth * calcWidth);
    var posX = x - calcWidth;
    var posY = y - calcHeight;

    var widthX = 2 * calcWidth;
    var heightY = 2 * calcHeight;

    if (stepClear <= radius) {
      cxt.clearRect(posX, posY, widthX, heightY);
      stepClear += 1;
      clearArc(x, y, radius);
    }
  }
}
const clearCanvas = () => {
  drawCtx.clearRect(0, 0, props.width, props.height);
};

function _forEach(items, callback) {
  return Array.prototype.forEach.call(items, function (item, idx) {
    callback(item, idx);
  });
}

// 活动结束事件
const activeStop = () => {
  emits('end', () => {
    clearCanvas();
    initCanvas();
  });
};

// 游戏触发调用接口
const handleClick = () => {
  requestStatus = '2';
  emits(
    'start',
    (prizeResult: any) => {
      // 游戏开始
      state.backImg = {
        background: `url(${prizeResult.activityRewardImageUrl}) center center no-repeat; background-size: contain;`,
      };
      state.prizeResult = prizeResult;
      requestStatus = '3';
      if (canShowPrize) {
        activeStop();
      }
    },
    (err: string) => {
      // 当事件需要停止,返回 err
      console.error(err);
      state.backImg = {};
      lock.value = false;
      state.prizeResult = null;
      requestStatus = `1`;
      canShowPrize = false;
      console.log('2oo2o2', 1111);
      let timer: any = setTimeout(() => {
        clearTimeout(timer);
        timer = undefined;
        initCanvas();
      }, 1500);
    },
  );
};

// 计算刮开区域占整个刮卡区域的百分比(使用节流,避免触发频率过高)
const calcArea = throttle((ratio = props.ratio) => {
  const pixels = drawCtx.getImageData(0, 0, props.width, props.height);
  const transPixels: any[] = [];
  _forEach(pixels.data, function (_, i) {
    const pixel = pixels.data[i + 3];
    if (pixel === 0) {
      transPixels.push(pixel);
    }
  });
  const temp = transPixels.length / pixels.data.length;
  console.log('ratio', ratio, '当前比例:', temp, 'lock.value', lock.value);
  if (!lock.value && temp > ratio - 0.1) { // -0.1是为了触发接口
    lock.value = true;
    handleClick();
  }
  if (temp > ratio) {
    console.log('刮的面积够了就重置刮刮卡');
    clearCanvas();
  }
}, 600);

/** 手指抬起 */
const touchEnd = () => {
  // 计算一下是否达到指定面积比例
  calcArea();
  // 有接口返回值
  if (requestStatus === '3' && state.prizeResult) {
    activeStop();
    //发送接口,没返回值啥
  } else if (requestStatus === '2') {
    canShowPrize = true;
  }
};

const touchStart = (e) => {
  let { x, y } = e.changedTouches[0];
  console.log('xy:', x, y);
  // xy都 -10 是为了擦除的中心在点击的中心(根据分辨率比例再考虑)
  pointInfo.startX = x - 10;
  pointInfo.startY = y - 10;
};

const touchMove = (e) => {
  let { x, y } = e.changedTouches[0];

  clearArcFun(x - 10, y - 10, 20, drawCtx);
  pointInfo.startX = x - 10;
  pointInfo.startY = y - 10;
  calcArea();
};

const findCanvas = function () {
  return new Promise((resolve) => {
    Taro.createSelectorQuery()
      .select('#scratchcard-canvas')
      .fields({ node: true, size: true })
      .exec((res) => {
        if (isEmpty(res) || isEmpty(res[0])) {
          resolve({ node: null });
        } else {
          const { node } = res[0];
          resolve(node);
        }
      });
  });
};
// 初始化
const initCanvas = () => {
  let left = 0;
  let top = 0;
  if (props.coverImg) {
    var coverImg = drawCanvas.createImage();
    coverImg.className = 'cover-image';
    coverImg.src = props.coverImg;
    coverImg.onload = function () {
      drawCtx.drawImage(coverImg, 0, 0, props.width, props.height);
    };
  } else {
    drawCtx.moveTo(left, top);
    drawCtx.lineTo(left + props.width, top);
    drawCtx.lineTo(left + props.width, props.height);
    drawCtx.lineTo(left, props.height);
    drawCtx.stroke();
    drawCtx.fillStyle = '#ddd';
    drawCtx.fill();
  }
};

const initDraw = async () => {
  drawCanvas = await findCanvas();
  drawCtx = drawCanvas.getContext('2d');
  drawCanvas.width = props.width;
  drawCanvas.height = props.height;
  const timer: any = setTimeout(() => {
    clearTimeout(timer);
    initCanvas();
  }, 300);
};

onMounted(() => {
  initDraw();
});

defineExpose({
  reset: () => {
    state.backImg = {};
    lock.value = false;
    state.prizeResult = null;
    requestStatus = `1`;
    canShowPrize = false;
    initCanvas();
  },
});
</script>

<style lang="scss">
.com-scratchcard {
  height: 328px;
  width: 594px;
  position: absolute;
  top: 566px;
  left: 78px;

  .scratchcard {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 1;
    width: 594px;
    height: 328px;
  }

  .scratchcard-content {
    width: 594px;
    height: 328px;
    text-align: center;
    background-size: 100% 100%;
    // 这里可以自定义默认背景图
  }
}

</style>

结语

刮刮卡效果很常见,本案例也是参考的京东nutui封装组件。因为不支持小程,才改写了。如果你也在开发小程序时用到刮刮卡效果,可以参考以上代码。

还遗留一个小问题: 就是高清图片画到高清屏幕手机的canvas上晰度会降低。因为开发过程中没有计算分辨率。

nutui-抽奖组件文档

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值